import isElectron from 'is-electron'; import { nanoid } from 'nanoid/non-secure'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate } from 'react-router'; import { api } from '/@/renderer/api'; import { PageHeader } from '/@/renderer/components/page-header/page-header'; import { isLegacyAuth, isServerLock, } from '/@/renderer/features/action-required/utils/window-properties'; import JellyfinIcon from '/@/renderer/features/servers/assets/jellyfin.png'; import NavidromeIcon from '/@/renderer/features/servers/assets/navidrome.png'; import SubsonicIcon from '/@/renderer/features/servers/assets/opensubsonic.png'; import { IgnoreCorsSslSwitches } from '/@/renderer/features/servers/components/ignore-cors-ssl-switches'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary'; import { AppRoute } from '/@/renderer/router/routes'; import { getServerById, useAuthStoreActions, useCurrentServer, useServerList, } from '/@/renderer/store'; import { Button } from '/@/shared/components/button/button'; import { Center } from '/@/shared/components/center/center'; import { Code } from '/@/shared/components/code/code'; import { Paper } from '/@/shared/components/paper/paper'; import { PasswordInput } from '/@/shared/components/password-input/password-input'; import { Stack } from '/@/shared/components/stack/stack'; import { TextInput } from '/@/shared/components/text-input/text-input'; import { TextTitle } from '/@/shared/components/text-title/text-title'; import { Text } from '/@/shared/components/text/text'; import { toast } from '/@/shared/components/toast/toast'; import { useForm } from '/@/shared/hooks/use-form'; import { AuthenticationResponse, ServerListItemWithCredential } from '/@/shared/types/domain-types'; import { ServerType, toServerType } from '/@/shared/types/types'; const localSettings = isElectron() ? window.api.localSettings : null; const SERVER_ICONS: Record = { [ServerType.JELLYFIN]: JellyfinIcon, [ServerType.NAVIDROME]: NavidromeIcon, [ServerType.SUBSONIC]: SubsonicIcon, }; const SERVER_NAMES: Record = { [ServerType.JELLYFIN]: 'Jellyfin', [ServerType.NAVIDROME]: 'Navidrome', [ServerType.SUBSONIC]: 'OpenSubsonic', }; const normalizeUrl = (url: string) => url.replace(/\/$/, ''); const LoginRoute = () => { const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const { addServer, setCurrentServer, updateServer } = useAuthStoreActions(); const currentServer = useCurrentServer(); const serverList = useServerList(); // Check if server lock is configured const serverLock = isServerLock(); const serverType = window.SERVER_TYPE ? toServerType(window.SERVER_TYPE) : null; const serverName = window.SERVER_NAME || ''; const serverUrl = window.SERVER_URL || ''; const remoteUrl = window.REMOTE_URL || ''; const legacyAuth = serverLock && isLegacyAuth(); const config = [ { isValid: true, key: 'SERVER_LOCK', value: serverLock, }, { isValid: serverType !== null, key: 'SERVER_TYPE', value: serverType, }, { isValid: true, key: 'SERVER_NAME', value: serverName, }, { isValid: serverUrl !== '', key: 'SERVER_URL', value: serverUrl, }, { isValid: true, key: 'REMOTE_URL', value: remoteUrl, }, ]; const form = useForm({ initialValues: { password: '', username: '', }, }); // If server lock is not enabled, or we already have a server, redirect to home if (currentServer) { return ; } // If any of the config values are invalid, show error if (config.some((c) => !c.isValid)) { return (
{t('error.genericError')} {t('error.serverNotSelectedError')} {JSON.stringify(config, null, 2)}
); } const handleSubmit = form.onSubmit(async (values) => { const authFunction = api.controller.authenticate; if (!authFunction) { return toast.error({ message: t('error.invalidServer'), }); } try { setIsLoading(true); const data: AuthenticationResponse | undefined = await authFunction( serverUrl, { legacy: legacyAuth, password: values.password, username: values.username, }, serverType as ServerType, ); if (!data) { return toast.error({ message: t('error.authenticationFailed'), }); } const normalizedUrl = normalizeUrl(serverUrl); const normalizedRemoteURL = normalizeUrl(remoteUrl); const existingServer = serverLock && Object.values(serverList).find((s) => normalizeUrl(s.url) === normalizedUrl); const serverItem: ServerListItemWithCredential = { credential: data.credential, id: nanoid(), isAdmin: data.isAdmin, name: serverName, remoteUrl: normalizedRemoteURL, type: serverType as ServerType, url: normalizedUrl, userId: data.userId, username: data.username, }; if (existingServer) { const updates: Partial = { credential: data.credential, isAdmin: data.isAdmin, userId: data.userId, username: data.username, }; if (data.ndCredential !== undefined) { updates.ndCredential = data.ndCredential; } updateServer(existingServer.id, updates); const updated = getServerById(existingServer.id); if (updated) setCurrentServer(updated); } else { if (data.ndCredential !== undefined) { serverItem.ndCredential = data.ndCredential; } addServer(serverItem); setCurrentServer(serverItem); } toast.success({ message: t('form.addServer.success'), }); if (localSettings && values.password) { const saved = await localSettings.passwordSet(values.password, serverItem.id); if (!saved) { toast.error({ message: t('form.addServer.error', { context: 'savePassword', }), }); } } } catch (err: any) { setIsLoading(false); return toast.error({ message: err?.message }); } return setIsLoading(false); }); const isSubmitDisabled = !form.values.username || !form.values.password; const serverIcon = SERVER_ICONS[serverType as ServerType]; const serverDisplayName = SERVER_NAMES[serverType as ServerType]; return (
{serverDisplayName} {serverName} {serverName && ( {serverDisplayName} )}
); }; const LoginRouteWithBoundary = () => { return ( ); }; export default LoginRouteWithBoundary;