mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-19 18:04:22 +02:00
add login page for locked server
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { openModal } from '@mantine/modals';
|
import { openModal } from '@mantine/modals';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Navigate } from 'react-router';
|
import { Navigate } from 'react-router';
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ import { Group } from '/@/shared/components/group/group';
|
|||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
const ActionRequiredRoute = () => {
|
const ActionRequiredRoute = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentServer = useCurrentServerWithCredential();
|
const currentServer = useCurrentServerWithCredential();
|
||||||
@@ -60,7 +63,7 @@ const ActionRequiredRoute = () => {
|
|||||||
<Stack mt="2rem">
|
<Stack mt="2rem">
|
||||||
{canReturnHome && <Navigate to={AppRoute.HOME} />}
|
{canReturnHome && <Navigate to={AppRoute.HOME} />}
|
||||||
{/* This should be displayed if a credential is required */}
|
{/* This should be displayed if a credential is required */}
|
||||||
{isCredentialRequired && (
|
{isCredentialRequired && !localSettings?.env.SERVER_LOCK && (
|
||||||
<Group justify="center" wrap="nowrap">
|
<Group justify="center" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
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 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 { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { Center } from '/@/shared/components/center/center';
|
||||||
|
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 { Text } from '/@/shared/components/text/text';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
|
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, string> = {
|
||||||
|
[ServerType.JELLYFIN]: JellyfinIcon,
|
||||||
|
[ServerType.NAVIDROME]: NavidromeIcon,
|
||||||
|
[ServerType.SUBSONIC]: SubsonicIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SERVER_NAMES: Record<ServerType, string> = {
|
||||||
|
[ServerType.JELLYFIN]: 'Jellyfin',
|
||||||
|
[ServerType.NAVIDROME]: 'Navidrome',
|
||||||
|
[ServerType.SUBSONIC]: 'OpenSubsonic',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoginRoute = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { addServer, setCurrentServer } = useAuthStoreActions();
|
||||||
|
const currentServer = useCurrentServer();
|
||||||
|
|
||||||
|
// Check if server lock is configured
|
||||||
|
const serverLock = localSettings?.env.SERVER_LOCK || false;
|
||||||
|
const serverType = localSettings?.env.SERVER_TYPE
|
||||||
|
? toServerType(localSettings.env.SERVER_TYPE)
|
||||||
|
: null;
|
||||||
|
const serverName = localSettings?.env.SERVER_NAME || '';
|
||||||
|
const serverUrl = localSettings?.env.SERVER_URL || '';
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
password: '',
|
||||||
|
username: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!serverLock || !serverType || currentServer) {
|
||||||
|
return <Navigate replace to={AppRoute.HOME} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = form.onSubmit(async (values) => {
|
||||||
|
const authFunction = api.controller.authenticate;
|
||||||
|
|
||||||
|
if (!authFunction) {
|
||||||
|
return toast.error({
|
||||||
|
message: t('error.invalidServer', { postProcess: 'sentenceCase' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const data: AuthenticationResponse | undefined = await authFunction(
|
||||||
|
serverUrl,
|
||||||
|
{
|
||||||
|
legacy: false,
|
||||||
|
password: values.password,
|
||||||
|
username: values.username,
|
||||||
|
},
|
||||||
|
serverType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return toast.error({
|
||||||
|
message: t('error.authenticationFailed', { postProcess: 'sentenceCase' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverItem: ServerListItemWithCredential = {
|
||||||
|
credential: data.credential,
|
||||||
|
id: nanoid(),
|
||||||
|
name: serverName,
|
||||||
|
type: serverType,
|
||||||
|
url: serverUrl.replace(/\/$/, ''),
|
||||||
|
userId: data.userId,
|
||||||
|
username: data.username,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.ndCredential !== undefined) {
|
||||||
|
serverItem.ndCredential = data.ndCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
addServer(serverItem);
|
||||||
|
setCurrentServer(serverItem);
|
||||||
|
|
||||||
|
toast.success({
|
||||||
|
message: t('form.addServer.success', { postProcess: 'sentenceCase' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (localSettings && values.password) {
|
||||||
|
const saved = await localSettings.passwordSet(values.password, serverItem.id);
|
||||||
|
if (!saved) {
|
||||||
|
toast.error({
|
||||||
|
message: t('form.addServer.error', {
|
||||||
|
context: 'savePassword',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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];
|
||||||
|
const serverDisplayName = SERVER_NAMES[serverType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedPage>
|
||||||
|
<PageHeader />
|
||||||
|
<Center style={{ height: '100%', width: '100vw' }}>
|
||||||
|
<Paper p="xl" style={{ maxWidth: '400px', width: '100%' }}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<img
|
||||||
|
alt={serverDisplayName}
|
||||||
|
height="80"
|
||||||
|
src={serverIcon}
|
||||||
|
width="80"
|
||||||
|
/>
|
||||||
|
<Text fw={600} size="xl">
|
||||||
|
{serverDisplayName}
|
||||||
|
</Text>
|
||||||
|
{serverName && (
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
{serverName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
data-autofocus
|
||||||
|
label={t('form.addServer.input', {
|
||||||
|
context: 'username',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
})}
|
||||||
|
required
|
||||||
|
variant="filled"
|
||||||
|
{...form.getInputProps('username')}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label={t('form.addServer.input', {
|
||||||
|
context: 'password',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
})}
|
||||||
|
required
|
||||||
|
variant="filled"
|
||||||
|
{...form.getInputProps('password')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
fullWidth
|
||||||
|
loading={isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{t('common.login', {
|
||||||
|
defaultValue: 'Login',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Center>
|
||||||
|
</AnimatedPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginRoute;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { openModal } from '@mantine/modals';
|
import { openModal } from '@mantine/modals';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
import { Dispatch, useCallback } from 'react';
|
import { Dispatch, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
@@ -9,6 +10,8 @@ import { AppRoute } from '/@/renderer/router/routes';
|
|||||||
import { useAuthStoreActions, useServerList } from '/@/renderer/store';
|
import { useAuthStoreActions, useServerList } from '/@/renderer/store';
|
||||||
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
interface ServerCommandsProps {
|
interface ServerCommandsProps {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
setPages: (pages: CommandPalettePages[]) => void;
|
setPages: (pages: CommandPalettePages[]) => void;
|
||||||
@@ -42,6 +45,8 @@ export const ServerCommands = ({ handleClose, setPages, setQuery }: ServerComman
|
|||||||
[handleClose, navigate, setCurrentServer, setPages, setQuery],
|
[handleClose, navigate, setCurrentServer, setPages, setQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const serverLock = localSettings?.env.SERVER_LOCK || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Command.Group
|
<Command.Group
|
||||||
@@ -54,11 +59,13 @@ export const ServerCommands = ({ handleClose, setPages, setQuery }: ServerComman
|
|||||||
>{`${serverList[key].name}...`}</Command.Item>
|
>{`${serverList[key].name}...`}</Command.Item>
|
||||||
))}
|
))}
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
<Command.Group heading={t('common.manage', { postProcess: 'sentenceCase' })}>
|
{!serverLock && (
|
||||||
<Command.Item onSelect={handleManageServersModal}>
|
<Command.Group heading={t('common.manage', { postProcess: 'sentenceCase' })}>
|
||||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}...
|
<Command.Item onSelect={handleManageServersModal}>
|
||||||
</Command.Item>
|
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}...
|
||||||
</Command.Group>
|
</Command.Item>
|
||||||
|
</Command.Group>
|
||||||
|
)}
|
||||||
<Command.Separator />
|
<Command.Separator />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Group grow>
|
<Group grow justify="flex-end">
|
||||||
{onCancel && (
|
{onCancel && (
|
||||||
<ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton>
|
<ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { openModal } from '@mantine/modals';
|
import { openModal } from '@mantine/modals';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ import { Icon } from '/@/shared/components/icon/icon';
|
|||||||
import { ServerListItemWithCredential, ServerType } from '/@/shared/types/domain-types';
|
import { ServerListItemWithCredential, ServerType } from '/@/shared/types/domain-types';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
export const ServerSelectorItems = () => {
|
export const ServerSelectorItems = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -87,6 +90,8 @@ export const ServerSelectorItems = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const serverLock = localSettings?.env.SERVER_LOCK || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Label>
|
<DropdownMenu.Label>
|
||||||
@@ -120,12 +125,14 @@ export const ServerSelectorItems = () => {
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<DropdownMenu.Item
|
{!serverLock && (
|
||||||
leftSection={<Icon icon="edit" />}
|
<DropdownMenu.Item
|
||||||
onClick={handleManageServersModal}
|
leftSection={<Icon icon="edit" />}
|
||||||
>
|
onClick={handleManageServersModal}
|
||||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}
|
>
|
||||||
</DropdownMenu.Item>
|
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)}
|
||||||
{musicFolders && musicFolders.items.length > 0 && (
|
{musicFolders && musicFolders.items.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NuqsAdapter } from '@offlegacy/nuqs-hash-router';
|
import { NuqsAdapter } from '@offlegacy/nuqs-hash-router';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Navigate, Outlet } from 'react-router';
|
import { Navigate, Outlet } from 'react-router';
|
||||||
|
|
||||||
@@ -9,10 +10,14 @@ import { Center } from '/@/shared/components/center/center';
|
|||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { AuthState } from '/@/shared/types/types';
|
import { AuthState } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
export const AppOutlet = () => {
|
export const AppOutlet = () => {
|
||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
const authState = useServerAuthenticated();
|
const authState = useServerAuthenticated();
|
||||||
|
|
||||||
|
const serverLock = localSettings?.env.SERVER_LOCK || false;
|
||||||
|
|
||||||
const isActionsRequired = useMemo(() => {
|
const isActionsRequired = useMemo(() => {
|
||||||
const isServerRequired = !currentServer;
|
const isServerRequired = !currentServer;
|
||||||
|
|
||||||
@@ -30,6 +35,10 @@ export const AppOutlet = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverLock && !currentServer) {
|
||||||
|
return <Navigate replace to={AppRoute.LOGIN} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isActionsRequired || authState === AuthState.INVALID) {
|
if (isActionsRequired || authState === AuthState.INVALID) {
|
||||||
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
|
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const InvalidRoute = lazy(
|
|||||||
() => import('/@/renderer/features/action-required/routes/invalid-route'),
|
() => import('/@/renderer/features/action-required/routes/invalid-route'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const LoginRoute = lazy(() => import('/@/renderer/features/login/routes/login-route'));
|
||||||
|
|
||||||
const HomeRoute = lazy(() => import('/@/renderer/features/home/routes/home-route'));
|
const HomeRoute = lazy(() => import('/@/renderer/features/home/routes/home-route'));
|
||||||
|
|
||||||
const ArtistListRoute = lazy(() => import('/@/renderer/features/artists/routes/artist-list-route'));
|
const ArtistListRoute = lazy(() => import('/@/renderer/features/artists/routes/artist-list-route'));
|
||||||
@@ -209,6 +211,7 @@ export const AppRouter = () => {
|
|||||||
element={<ActionRequiredRoute />}
|
element={<ActionRequiredRoute />}
|
||||||
path={AppRoute.ACTION_REQUIRED}
|
path={AppRoute.ACTION_REQUIRED}
|
||||||
/>
|
/>
|
||||||
|
<Route element={<LoginRoute />} path={AppRoute.LOGIN} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum AppRoute {
|
export enum AppRoute {
|
||||||
ACTION_REQUIRED = '/action-required',
|
ACTION_REQUIRED = '/action-required',
|
||||||
EXPLORE = '/explore',
|
EXPLORE = '/explore',
|
||||||
|
LOGIN = '/login',
|
||||||
FAKE_LIBRARY_ALBUM_DETAILS = '/library/albums/dummy/:albumId',
|
FAKE_LIBRARY_ALBUM_DETAILS = '/library/albums/dummy/:albumId',
|
||||||
HOME = '/',
|
HOME = '/',
|
||||||
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
||||||
|
|||||||
Reference in New Issue
Block a user