From ae87aea2703cbd5be5becbc8a45e4c235c98aa0d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 4 Jun 2022 17:08:35 -0700 Subject: [PATCH] Move servers to separate feature/route - Add deviceId to localstorage - Add individual server auth to localstorage --- src/renderer/api/authApi.ts | 11 +-- src/renderer/api/index.ts | 1 - src/renderer/api/serversApi.ts | 8 +- src/renderer/api/types.ts | 69 +++++++++---- .../features/auth/queries/useLogin.ts | 22 ++++- .../auth/routes/LoginRoute.module.scss | 2 +- .../features/auth/routes/LoginRoute.tsx | 13 +-- .../servers/components/AddServerModal.tsx | 85 ++++++++++++++++ .../servers/components/EditServerModal.tsx | 63 ++++++++++++ .../servers/components/ServerList.module.scss | 8 ++ .../servers/components/ServerList.tsx | 84 ++++++++++++++++ src/renderer/features/servers/index.ts | 5 + .../queries/useCreateServer.ts} | 4 +- .../features/servers/queries/useServers.ts | 77 +++++++++++++++ .../features/servers/routes/ServersRoute.tsx | 11 +++ src/renderer/features/user-menu/UserMenu.tsx | 4 +- .../servers/components/ServerList.module.scss | 3 - .../servers/components/ServerList.tsx | 23 ----- .../servers/components/ServersModal.tsx | 97 ------------------- .../user-menu/servers/queries/useServers.ts | 18 ---- src/renderer/store/authSlice.ts | 4 + src/renderer/utils/getServerUrl.ts | 7 -- src/renderer/utils/index.ts | 2 +- src/renderer/utils/normalizeServerUrl.ts | 4 + 24 files changed, 428 insertions(+), 197 deletions(-) create mode 100644 src/renderer/features/servers/components/AddServerModal.tsx create mode 100644 src/renderer/features/servers/components/EditServerModal.tsx create mode 100644 src/renderer/features/servers/components/ServerList.module.scss create mode 100644 src/renderer/features/servers/components/ServerList.tsx create mode 100644 src/renderer/features/servers/index.ts rename src/renderer/features/{user-menu/servers/queries/useCreateServers.ts => servers/queries/useCreateServer.ts} (95%) create mode 100644 src/renderer/features/servers/queries/useServers.ts create mode 100644 src/renderer/features/servers/routes/ServersRoute.tsx delete mode 100644 src/renderer/features/user-menu/servers/components/ServerList.module.scss delete mode 100644 src/renderer/features/user-menu/servers/components/ServerList.tsx delete mode 100644 src/renderer/features/user-menu/servers/components/ServersModal.tsx delete mode 100644 src/renderer/features/user-menu/servers/queries/useServers.ts delete mode 100644 src/renderer/utils/getServerUrl.ts create mode 100644 src/renderer/utils/normalizeServerUrl.ts diff --git a/src/renderer/api/authApi.ts b/src/renderer/api/authApi.ts index 30aba46..4b72d02 100644 --- a/src/renderer/api/authApi.ts +++ b/src/renderer/api/authApi.ts @@ -1,17 +1,15 @@ import { axios } from 'renderer/lib'; -import { getServerUrl } from 'renderer/utils'; import { PingResponse, UserResponse } from './types'; const login = async ( - server: string, + serverUrl: string, body: { password: string; username: string; } ) => { - const serverUrl = getServerUrl(server); const { data } = await axios.post( - `${serverUrl}/auth/login`, + `${serverUrl}/api/auth/login`, body, { withCredentials: true, @@ -21,9 +19,8 @@ const login = async ( return data; }; -const ping = async (server: string) => { - const serverUrl = getServerUrl(server); - const { data } = await axios.get(`${serverUrl}/auth/ping`, { +const ping = async (serverUrl: string) => { + const { data } = await axios.get(`${serverUrl}/api/auth/ping`, { timeout: 2000, }); diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index 5338d72..07082b6 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -3,4 +3,3 @@ export * from './usersApi'; export * from './serversApi'; export * from './queries/useAlbum'; export * from './queryKeys'; -export * from './types'; diff --git a/src/renderer/api/serversApi.ts b/src/renderer/api/serversApi.ts index 547a59e..7e3551d 100644 --- a/src/renderer/api/serversApi.ts +++ b/src/renderer/api/serversApi.ts @@ -1,12 +1,12 @@ import { axios } from 'renderer/lib'; -import { ServerResponse, ServersResponse } from './types'; +import { ServerResponse } from './types'; const getServers = async () => { - const { data } = await axios.get('/servers'); + const { data } = await axios.get('/servers'); return data; }; -const create = async (body: { +const createServer = async (body: { name: string; remoteUserId: string; token: string; @@ -18,6 +18,6 @@ const create = async (body: { }; export const serversApi = { - create, + createServer, getServers, }; diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 961a7e6..905c327 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -5,30 +5,20 @@ export type BaseResponse = { statusCode: number; }; -export type ServerResponse = Server; - -export type ServersResponse = Server[]; - -export type UserResponse = User; - -export type UsersResponse = User[]; - -export type PingResponse = Ping; - -export interface Server { +export type ServerResponse = { createdAt: string; id: number; name: string; remoteUserId: string; - serverFolder?: ServerFolder[]; + serverFolder?: ServerFolderResponse[]; serverType: string; token: string; updatedAt: string; url: string; username: string; -} +}; -export interface ServerFolder { +export type ServerFolderResponse = { createdAt: string; enabled: boolean; id: number; @@ -37,9 +27,9 @@ export interface ServerFolder { remoteId: string; serverId: number; updatedAt: string; -} +}; -export interface User { +export type UserResponse = { createdAt: string; enabled: boolean; id: number; @@ -47,10 +37,51 @@ export interface User { password: string; updatedAt: string; username: string; -} +}; -export interface Ping { +export type PingResponse = { description: string; name: string; version: string; -} +}; + +export type GenreResponse = { + createdAt: string; + id: number; + name: string; + updatedAt: string; +}; + +export type AlbumResponse = { + albumArtistId: number; + createdAt: string; + date: string; + genres: GenreResponse[]; + id: number; + name: string; + remoteCreatedAt: string; + remoteId: string; + serverFolderId: number; + songs: SongResponse[]; + updatedAt: string; + year: number; +}; + +export type SongResponse = { + albumId: number; + artistName: null; + bitRate: number; + container: string; + createdAt: string; + date: string; + disc: number; + duration: number; + id: number; + name: string; + remoteCreatedAt: string; + remoteId: string; + serverFolderId: number; + track: number; + updatedAt: string; + year: number; +}; diff --git a/src/renderer/features/auth/queries/useLogin.ts b/src/renderer/features/auth/queries/useLogin.ts index 75a9a97..1bda7b7 100644 --- a/src/renderer/features/auth/queries/useLogin.ts +++ b/src/renderer/features/auth/queries/useLogin.ts @@ -1,19 +1,35 @@ +import md5 from 'md5'; +import { nanoid } from 'nanoid'; import { useMutation } from 'react-query'; import { authApi } from 'renderer/api'; +import { useAppDispatch } from 'renderer/hooks'; +import { login } from 'renderer/store/authSlice'; export const useLogin = ( - server: string, + serverUrl: string, body: { password: string; username: string; } ) => { + const dispatch = useAppDispatch(); + return useMutation({ - mutationFn: () => authApi.login(server, body), + mutationFn: () => authApi.login(serverUrl, body), onSuccess: () => { + dispatch(login(serverUrl)); + + if (!localStorage.getItem('device_id')) { + localStorage.setItem('device_id', nanoid()); + } + localStorage.setItem( 'authentication', - JSON.stringify({ isAuthenticated: true, serverUrl: server }) + JSON.stringify({ + isAuthenticated: true, + key: md5(serverUrl), + serverUrl, + }) ); }, }); diff --git a/src/renderer/features/auth/routes/LoginRoute.module.scss b/src/renderer/features/auth/routes/LoginRoute.module.scss index 67d272e..fb74ec8 100644 --- a/src/renderer/features/auth/routes/LoginRoute.module.scss +++ b/src/renderer/features/auth/routes/LoginRoute.module.scss @@ -4,7 +4,7 @@ margin: auto; padding: 3rem; background: rgba(50, 50, 50, 0.4); - border-radius: 5%; + border-radius: 5px; } .button { diff --git a/src/renderer/features/auth/routes/LoginRoute.tsx b/src/renderer/features/auth/routes/LoginRoute.tsx index e20d95a..556c955 100644 --- a/src/renderer/features/auth/routes/LoginRoute.tsx +++ b/src/renderer/features/auth/routes/LoginRoute.tsx @@ -12,16 +12,13 @@ import { useDebouncedValue } from '@mantine/hooks'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { AlertCircle, CircleCheck } from 'tabler-icons-react'; -import { useAppDispatch } from 'renderer/hooks'; -import { login } from 'renderer/store/authSlice'; -import { getServerUrl } from 'renderer/utils'; +import { normalizeServerUrl } from 'renderer/utils'; import { useLogin } from '../queries/useLogin'; import { usePingServer } from '../queries/usePingServer'; import styles from './LoginRoute.module.scss'; export const LoginRoute = () => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const [searchParams] = useSearchParams(); const [username, setUsername] = useState(searchParams.get('username') || ''); @@ -35,7 +32,7 @@ export const LoginRoute = () => { mutate: handleLogin, isLoading, isError, - } = useLogin(server, { + } = useLogin(normalizeServerUrl(server), { password, username, }); @@ -44,7 +41,7 @@ export const LoginRoute = () => { isLoading: isCheckingServer, isSuccess: isValidServer, isFetched, - } = usePingServer(debouncedServer); + } = usePingServer(normalizeServerUrl(debouncedServer)); return (
@@ -54,9 +51,7 @@ export const LoginRoute = () => { e.preventDefault(); handleLogin(undefined, { onError: () => {}, - onSuccess: () => { - dispatch(login(getServerUrl(server))); - }, + onSuccess: () => {}, }); }} > diff --git a/src/renderer/features/servers/components/AddServerModal.tsx b/src/renderer/features/servers/components/AddServerModal.tsx new file mode 100644 index 0000000..7808c3e --- /dev/null +++ b/src/renderer/features/servers/components/AddServerModal.tsx @@ -0,0 +1,85 @@ +import { + Button, + Checkbox, + Modal, + ModalProps, + PasswordInput, + SegmentedControl, + Stack, + TextInput, +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useTranslation } from 'react-i18next'; +import { useCreateServer, validateServer } from '../queries/useCreateServer'; + +export const AddServerModal = ({ ...rest }: ModalProps) => { + const { t } = useTranslation(); + + const form = useForm({ + initialValues: { + legacyAuth: false, + name: '', + password: '', + serverType: 'jellyfin', + url: 'http://', + username: '', + }, + }); + + const createServerMutation = useCreateServer(); + + return ( + +
{ + const res = await validateServer(values); + + if (res?.token) { + createServerMutation.mutateAsync({ + ...values, + remoteUserId: res.userId, + token: res.token, + }); + } + })} + > + + + + + + + {form.getInputProps('serverType').value === 'subsonic' && ( + + )} + + +
+
+ ); +}; diff --git a/src/renderer/features/servers/components/EditServerModal.tsx b/src/renderer/features/servers/components/EditServerModal.tsx new file mode 100644 index 0000000..c9451db --- /dev/null +++ b/src/renderer/features/servers/components/EditServerModal.tsx @@ -0,0 +1,63 @@ +import { + Button, + Checkbox, + ModalProps, + PasswordInput, + SegmentedControl, + Stack, + TextInput, +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useTranslation } from 'react-i18next'; +import { ServerResponse } from 'renderer/api/types'; + +interface EditServerModalProps extends ModalProps { + server: ServerResponse | undefined; +} + +export const EditServerModal = ({ server }: EditServerModalProps) => { + const { t } = useTranslation(); + + const form = useForm({ + initialValues: { + legacyAuth: false, + name: server?.name, + password: '', + serverType: server?.serverType, + url: server?.url, + username: server?.username, + }, + }); + + return ( +
{})}> + + + + + + + {form.getInputProps('serverType').value === 'subsonic' && ( + + )} + + +
+ ); +}; diff --git a/src/renderer/features/servers/components/ServerList.module.scss b/src/renderer/features/servers/components/ServerList.module.scss new file mode 100644 index 0000000..0a89ed7 --- /dev/null +++ b/src/renderer/features/servers/components/ServerList.module.scss @@ -0,0 +1,8 @@ +.item { + display: flex; + justify-content: space-between; + max-width: 50vw; + margin: 1rem; + padding: 1rem; + outline: 1px #fff solid; +} diff --git a/src/renderer/features/servers/components/ServerList.tsx b/src/renderer/features/servers/components/ServerList.tsx new file mode 100644 index 0000000..de46563 --- /dev/null +++ b/src/renderer/features/servers/components/ServerList.tsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from 'react'; +import { Text } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { EditCircle } from 'tabler-icons-react'; +import { ServerResponse } from 'renderer/api/types'; +import { IconButton } from 'renderer/components'; +import { useServers } from '../queries/useServers'; +import { EditServerModal } from './EditServerModal'; +import styles from './ServerList.module.scss'; + +export const ServerList = () => { + const { data: servers } = useServers(); + const [editServerModal, editServerHandlers] = useDisclosure(false); + const [opened, setOpened] = useState(false); + + const [selectedServer, setSelectedServer] = useState< + ServerResponse | undefined + >(); + + const handleClickServer = (server: ServerResponse) => { + setSelectedServer(server); + editServerHandlers.open(); + }; + + const handleKeyDownServer = ( + e: React.KeyboardEvent, + server: ServerResponse + ) => { + if (e.key === ' ' || e.key === 'Enter') { + setSelectedServer(server); + editServerHandlers.open(); + } + }; + + const handleCloseModal = () => { + setOpened(false); + editServerHandlers.close(); + }; + + useEffect(() => { + if (editServerModal === true) { + setTimeout(() => setOpened(true)); + } else { + setTimeout(() => setSelectedServer(undefined), 100); + } + }, [editServerModal]); + + console.log('opened', opened); + + return ( + <> + {servers && + servers.map((server) => ( + <> +
handleClickServer(server)} + onKeyDown={(e) => handleKeyDownServer(e, server)} + > +
+ {server.name} + Hello +
+ } + onClick={() => editServerHandlers.toggle()} + > + Edit + +
+ {selectedServer && ( + + )} + + ))} + + ); +}; diff --git a/src/renderer/features/servers/index.ts b/src/renderer/features/servers/index.ts new file mode 100644 index 0000000..c393fff --- /dev/null +++ b/src/renderer/features/servers/index.ts @@ -0,0 +1,5 @@ +export * from './routes/ServersRoute'; +export * from './queries/useCreateServer'; +export * from './queries/useServers'; +export * from './components/AddServerModal'; +export * from './components/ServerList'; diff --git a/src/renderer/features/user-menu/servers/queries/useCreateServers.ts b/src/renderer/features/servers/queries/useCreateServer.ts similarity index 95% rename from src/renderer/features/user-menu/servers/queries/useCreateServers.ts rename to src/renderer/features/servers/queries/useCreateServer.ts index dd74416..91c56fa 100644 --- a/src/renderer/features/user-menu/servers/queries/useCreateServers.ts +++ b/src/renderer/features/servers/queries/useCreateServer.ts @@ -61,9 +61,9 @@ export const validateServer = async (options: { return null; }; -export const useCreateServers = () => { +export const useCreateServer = () => { return useMutation({ - mutationFn: serversApi.create, + mutationFn: serversApi.createServer, onError: (e) => console.log(e), onSuccess: (e) => console.log(e), }); diff --git a/src/renderer/features/servers/queries/useServers.ts b/src/renderer/features/servers/queries/useServers.ts new file mode 100644 index 0000000..402d9b8 --- /dev/null +++ b/src/renderer/features/servers/queries/useServers.ts @@ -0,0 +1,77 @@ +import md5 from 'md5'; +import { useQuery } from 'react-query'; +import { queryKeys, serversApi } from 'renderer/api'; +import { ServerFolderResponse } from 'renderer/api/types'; + +export interface ServerFolderAuth { + id: number; + locked: boolean; + serverId: number; + token: string; + type: string; + url: string; + userId: string; + username: string; +} + +export const useServers = () => { + return useQuery({ + onSuccess: (servers) => { + const { serverUrl } = JSON.parse( + localStorage.getItem('authentication') || '{}' + ); + const storedServersKey = `servers_${md5(serverUrl)}`; + const serversFromLocalStorage = localStorage.getItem(storedServersKey); + + // If a custom account/token is set for a server, use that instead of the default one + if (serversFromLocalStorage) { + const existingServers = JSON.parse(serversFromLocalStorage); + + // The 'locked' property determines whether or not to skip updating the server auth + const skipped = existingServers.filter( + (server: ServerFolderAuth) => server.locked + ); + + const store = servers.flatMap((server) => + server.serverFolder?.map((serverFolder: ServerFolderResponse) => { + if (skipped.includes(serverFolder.id)) { + return existingServers.find( + (s: ServerFolderAuth) => s.id === serverFolder.id + ); + } + + return { + id: serverFolder.id, + locked: false, + serverId: server.id, + token: server.token, + type: server.serverType, + url: server.url, + userId: server.remoteUserId, + username: server.username, + }; + }) + ); + + return localStorage.setItem(storedServersKey, JSON.stringify(store)); + } + + const store = servers.flatMap((server) => + server.serverFolder?.map((serverFolder: ServerFolderResponse) => ({ + id: serverFolder.id, + locked: false, + serverId: server.id, + token: server.token, + type: server.serverType, + url: server.url, + userId: server.remoteUserId, + username: server.username, + })) + ); + + return localStorage.setItem(storedServersKey, JSON.stringify(store)); + }, + queryFn: () => serversApi.getServers(), + queryKey: queryKeys.servers, + }); +}; diff --git a/src/renderer/features/servers/routes/ServersRoute.tsx b/src/renderer/features/servers/routes/ServersRoute.tsx new file mode 100644 index 0000000..51c3c63 --- /dev/null +++ b/src/renderer/features/servers/routes/ServersRoute.tsx @@ -0,0 +1,11 @@ +import { Title } from '@mantine/core'; +import { ServerList } from '../components/ServerList'; + +export const ServersRoute = () => { + return ( +
+ Servers + +
+ ); +}; diff --git a/src/renderer/features/user-menu/UserMenu.tsx b/src/renderer/features/user-menu/UserMenu.tsx index 741b7bf..c94d5ff 100644 --- a/src/renderer/features/user-menu/UserMenu.tsx +++ b/src/renderer/features/user-menu/UserMenu.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router'; import { Logout, Server, Settings } from 'tabler-icons-react'; import { useAppDispatch } from 'renderer/hooks'; import { logout } from 'renderer/store/authSlice'; -import { ServersModal } from './servers/components/ServersModal'; +import { AddServerModal } from '../servers/components/AddServerModal'; import styles from './UserMenu.module.scss'; export const UserMenu = () => { @@ -42,7 +42,7 @@ export const UserMenu = () => { Logout - addServerHandlers.close()} /> diff --git a/src/renderer/features/user-menu/servers/components/ServerList.module.scss b/src/renderer/features/user-menu/servers/components/ServerList.module.scss deleted file mode 100644 index 9165b8e..0000000 --- a/src/renderer/features/user-menu/servers/components/ServerList.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.container { - padding: 10px; -} diff --git a/src/renderer/features/user-menu/servers/components/ServerList.tsx b/src/renderer/features/user-menu/servers/components/ServerList.tsx deleted file mode 100644 index afc4083..0000000 --- a/src/renderer/features/user-menu/servers/components/ServerList.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Grid } from '@mantine/core'; -import { Server } from 'server/types/types'; -import styles from './ServerList.module.scss'; - -interface ServerListProps { - servers: Server[] | undefined; -} - -export const ServerList = ({ servers }: ServerListProps) => { - return ( - <> - {servers && - servers.map((server) => ( -
- - {server.name || server.url} - - -
- ))} - - ); -}; diff --git a/src/renderer/features/user-menu/servers/components/ServersModal.tsx b/src/renderer/features/user-menu/servers/components/ServersModal.tsx deleted file mode 100644 index a89b7bc..0000000 --- a/src/renderer/features/user-menu/servers/components/ServersModal.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { - Button, - Checkbox, - Modal, - ModalProps, - PasswordInput, - SegmentedControl, - Stack, - TextInput, -} from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useBooleanToggle } from '@mantine/hooks'; -import { useTranslation } from 'react-i18next'; -import { useCreateServers, validateServer } from '../queries/useCreateServers'; -import { useServers } from '../queries/useServers'; -import { ServerList } from './ServerList'; - -export const ServersModal = ({ ...rest }: ModalProps) => { - const { t } = useTranslation(); - const [createNew, toggleCreateNew] = useBooleanToggle(false); - - const form = useForm({ - initialValues: { - legacyAuth: false, - name: '', - password: '', - serverType: 'jellyfin', - url: 'http://', - username: '', - }, - }); - - const { data } = useServers({}); - - const createServerMutation = useCreateServers(); - - return ( - - - - {createNew && ( -
{ - const res = await validateServer(values); - - if (res?.token) { - createServerMutation.mutateAsync({ - ...values, - remoteUserId: res.userId, - token: res.token, - }); - } - })} - > - - - - - - - {form.getInputProps('serverType').value === 'subsonic' && ( - - )} - - -
- )} -
- ); -}; diff --git a/src/renderer/features/user-menu/servers/queries/useServers.ts b/src/renderer/features/user-menu/servers/queries/useServers.ts deleted file mode 100644 index d39358d..0000000 --- a/src/renderer/features/user-menu/servers/queries/useServers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useQuery } from 'react-query'; -import { queryKeys } from 'renderer/api'; -import { serversApi } from 'renderer/api/serversApi'; -import { ExtractFnReturnType, QueryConfig } from 'renderer/lib'; - -type UseServersOptions = { - config?: QueryConfig; -}; - -type QueryFnType = typeof serversApi.getServers; - -export const useServers = ({ config }: UseServersOptions) => { - return useQuery>({ - ...config, - queryFn: () => serversApi.getServers(), - queryKey: queryKeys.servers, - }); -}; diff --git a/src/renderer/store/authSlice.ts b/src/renderer/store/authSlice.ts index 6e06bb1..adf62e5 100644 --- a/src/renderer/store/authSlice.ts +++ b/src/renderer/store/authSlice.ts @@ -1,7 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import md5 from 'md5'; export interface AuthState { isAuthenticated: boolean; + key: string; serverUrl: string; } @@ -11,6 +13,7 @@ const persistedAuthState = JSON.parse( const initialState: AuthState = { isAuthenticated: persistedAuthState.isAuthenticated, + key: persistedAuthState.key, serverUrl: persistedAuthState.serverUrl, }; @@ -21,6 +24,7 @@ export const authSlice = createSlice({ login: (state: AuthState, action: PayloadAction) => { state.isAuthenticated = true; state.serverUrl = action.payload; + state.key = md5(action.payload); }, logout: (state: AuthState) => { diff --git a/src/renderer/utils/getServerUrl.ts b/src/renderer/utils/getServerUrl.ts deleted file mode 100644 index c49c337..0000000 --- a/src/renderer/utils/getServerUrl.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const getServerUrl = (url: string) => { - if (url[url.length - 1] === '/') { - return `${url}api`; - } - - return `${url}/api`; -}; diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index 2afeccb..66c17f9 100644 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -1,2 +1,2 @@ export * from './randomString'; -export * from './getServerUrl'; +export * from './normalizeServerUrl'; diff --git a/src/renderer/utils/normalizeServerUrl.ts b/src/renderer/utils/normalizeServerUrl.ts new file mode 100644 index 0000000..1601a2b --- /dev/null +++ b/src/renderer/utils/normalizeServerUrl.ts @@ -0,0 +1,4 @@ +export const normalizeServerUrl = (url: string) => { + // Remove trailing slash + return url.endsWith('/') ? url.slice(0, -1) : url; +};