mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add login page for locked server
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { openModal } from '@mantine/modals';
|
||||
import isElectron from 'is-electron';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Navigate } from 'react-router';
|
||||
|
||||
@@ -16,6 +17,8 @@ import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
const ActionRequiredRoute = () => {
|
||||
const { t } = useTranslation();
|
||||
const currentServer = useCurrentServerWithCredential();
|
||||
@@ -60,7 +63,7 @@ const ActionRequiredRoute = () => {
|
||||
<Stack mt="2rem">
|
||||
{canReturnHome && <Navigate to={AppRoute.HOME} />}
|
||||
{/* This should be displayed if a credential is required */}
|
||||
{isCredentialRequired && (
|
||||
{isCredentialRequired && !localSettings?.env.SERVER_LOCK && (
|
||||
<Group justify="center" wrap="nowrap">
|
||||
<Button
|
||||
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 isElectron from 'is-electron';
|
||||
import { Dispatch, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
@@ -9,6 +10,8 @@ import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useAuthStoreActions, useServerList } from '/@/renderer/store';
|
||||
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
interface ServerCommandsProps {
|
||||
handleClose: () => void;
|
||||
setPages: (pages: CommandPalettePages[]) => void;
|
||||
@@ -42,6 +45,8 @@ export const ServerCommands = ({ handleClose, setPages, setQuery }: ServerComman
|
||||
[handleClose, navigate, setCurrentServer, setPages, setQuery],
|
||||
);
|
||||
|
||||
const serverLock = localSettings?.env.SERVER_LOCK || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Command.Group
|
||||
@@ -54,11 +59,13 @@ export const ServerCommands = ({ handleClose, setPages, setQuery }: ServerComman
|
||||
>{`${serverList[key].name}...`}</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
<Command.Group heading={t('common.manage', { postProcess: 'sentenceCase' })}>
|
||||
<Command.Item onSelect={handleManageServersModal}>
|
||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}...
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
{!serverLock && (
|
||||
<Command.Group heading={t('common.manage', { postProcess: 'sentenceCase' })}>
|
||||
<Command.Item onSelect={handleManageServersModal}>
|
||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}...
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
<Command.Separator />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -301,7 +301,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<Group grow>
|
||||
<Group grow justify="flex-end">
|
||||
{onCancel && (
|
||||
<ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { openModal } from '@mantine/modals';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
export const ServerSelectorItems = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -87,6 +90,8 @@ export const ServerSelectorItems = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const serverLock = localSettings?.env.SERVER_LOCK || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Label>
|
||||
@@ -120,12 +125,14 @@ export const ServerSelectorItems = () => {
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
})}
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="edit" />}
|
||||
onClick={handleManageServersModal}
|
||||
>
|
||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{!serverLock && (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="edit" />}
|
||||
onClick={handleManageServersModal}
|
||||
>
|
||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
{musicFolders && musicFolders.items.length > 0 && (
|
||||
<>
|
||||
<DropdownMenu.Divider />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NuqsAdapter } from '@offlegacy/nuqs-hash-router';
|
||||
import isElectron from 'is-electron';
|
||||
import { useMemo } from 'react';
|
||||
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 { AuthState } from '/@/shared/types/types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
export const AppOutlet = () => {
|
||||
const currentServer = useCurrentServer();
|
||||
const authState = useServerAuthenticated();
|
||||
|
||||
const serverLock = localSettings?.env.SERVER_LOCK || false;
|
||||
|
||||
const isActionsRequired = useMemo(() => {
|
||||
const isServerRequired = !currentServer;
|
||||
|
||||
@@ -30,6 +35,10 @@ export const AppOutlet = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (serverLock && !currentServer) {
|
||||
return <Navigate replace to={AppRoute.LOGIN} />;
|
||||
}
|
||||
|
||||
if (isActionsRequired || authState === AuthState.INVALID) {
|
||||
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ const InvalidRoute = lazy(
|
||||
() => 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 ArtistListRoute = lazy(() => import('/@/renderer/features/artists/routes/artist-list-route'));
|
||||
@@ -209,6 +211,7 @@ export const AppRouter = () => {
|
||||
element={<ActionRequiredRoute />}
|
||||
path={AppRoute.ACTION_REQUIRED}
|
||||
/>
|
||||
<Route element={<LoginRoute />} path={AppRoute.LOGIN} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum AppRoute {
|
||||
ACTION_REQUIRED = '/action-required',
|
||||
EXPLORE = '/explore',
|
||||
LOGIN = '/login',
|
||||
FAKE_LIBRARY_ALBUM_DETAILS = '/library/albums/dummy/:albumId',
|
||||
HOME = '/',
|
||||
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
||||
|
||||
Reference in New Issue
Block a user