171 lines
5.9 KiB
TypeScript
171 lines
5.9 KiB
TypeScript
import { Box, Container, Flex, Heading, Text } from '@radix-ui/themes';
|
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
import { useNavigate } from 'react-router';
|
|
import { Slide, toast, ToastContainer } from 'react-toastify';
|
|
import * as v from 'valibot';
|
|
import { useResponseErrorHandler } from '../hooks/ResponseHelper';
|
|
import { useApi } from '../providers/ApiProvider';
|
|
import { formHook } from '../providers/FormProvider';
|
|
import { TooltipContentContainer } from '../components/info';
|
|
import { SearchParamKeys } from '../lib/constants';
|
|
|
|
const initFormSchema = v.object({
|
|
username: v.pipe(v.string(), v.trim(), v.minLength(1, 'Username is required')),
|
|
password: v.pipe(v.string(), v.minLength(1, 'Password is required')),
|
|
setup_secret: v.pipe(v.string(), v.minLength(1, 'Setup secret is required')),
|
|
});
|
|
|
|
export default function InitRoute() {
|
|
const navigate = useNavigate();
|
|
const { tanstackApiClient } = useApi();
|
|
const { defaultResponseErrorHandler } = useResponseErrorHandler();
|
|
|
|
const { mutateAsync: initAdmin, isPending } = useMutation({
|
|
...tanstackApiClient.mutation('post', '/api/auth/init_admin').mutationOptions,
|
|
onSuccess: async () => {
|
|
const searchParams = new URLSearchParams();
|
|
searchParams.set(SearchParamKeys.Message, 'Initialization successful. Please log in.');
|
|
navigate(`/login?${searchParams.toString()}`);
|
|
},
|
|
onError: (error) => {
|
|
if (defaultResponseErrorHandler(error)) return;
|
|
console.error('Init failed:', error);
|
|
},
|
|
});
|
|
|
|
const { queryOptions: healthInfoQuery } = tanstackApiClient.get('/api/health/info');
|
|
useQuery({
|
|
...healthInfoQuery,
|
|
queryFn: async (...args) => {
|
|
try {
|
|
const data = await healthInfoQuery.queryFn!(...args);
|
|
if (data.is_initialized) {
|
|
navigate('/');
|
|
return data;
|
|
}
|
|
return data;
|
|
} catch (error) {
|
|
if (defaultResponseErrorHandler(error)) return {} as never;
|
|
throw error;
|
|
}
|
|
},
|
|
});
|
|
|
|
const form = formHook.useAppForm({
|
|
defaultValues: { username: '', password: '', setup_secret: '' },
|
|
validators: { onBlur: initFormSchema, onSubmit: initFormSchema },
|
|
onSubmit: async ({ value }) => {
|
|
toast.dismiss();
|
|
return await initAdmin({ body: { username: value.username, password: value.password, setup_secret: value.setup_secret } });
|
|
},
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<Flex align="center" justify="center" style={{ minHeight: 'calc(100vh - 64px)' }}>
|
|
<Container size="3" p="0">
|
|
<Box
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
maxWidth: 480,
|
|
margin: '40px auto',
|
|
backgroundColor: 'white',
|
|
padding: 24,
|
|
borderRadius: 8,
|
|
boxShadow: '0 6px 18px rgba(15,23,42,0.06)',
|
|
}}
|
|
>
|
|
<Heading size="6" style={{ marginBottom: 12, alignSelf: 'center' }}>
|
|
Initialize YANPM
|
|
</Heading>
|
|
|
|
<Heading size="3" style={{ marginBottom: 24, color: 'var(--gray-11)', alignSelf: 'center' }}>
|
|
Create the initial admin user
|
|
</Heading>
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.handleSubmit();
|
|
}}
|
|
>
|
|
<form.AppField
|
|
name="username"
|
|
children={(field) => (
|
|
<>
|
|
<field.TextField
|
|
label="Username"
|
|
value={field.state.value}
|
|
autoComplete="username"
|
|
spellCheck={false}
|
|
required
|
|
onChange={(e) => field.handleChange(e.target.value)}
|
|
/>
|
|
<field.TextFieldErrorMessage {...field.state.meta} />
|
|
</>
|
|
)}
|
|
/>
|
|
|
|
<form.AppField
|
|
name="password"
|
|
children={(field) => (
|
|
<>
|
|
<field.TextField
|
|
label="Password"
|
|
value={field.state.value}
|
|
type="password"
|
|
required
|
|
autoComplete="new-password"
|
|
onChange={(e) => field.handleChange(e.target.value)}
|
|
showPasswordToggle
|
|
/>
|
|
<field.TextFieldErrorMessage {...field.state.meta} />
|
|
</>
|
|
)}
|
|
/>
|
|
|
|
<form.AppField
|
|
name="setup_secret"
|
|
children={(field) => (
|
|
<>
|
|
<field.TextField
|
|
label="Setup Secret"
|
|
value={field.state.value}
|
|
required
|
|
onChange={(e) => field.handleChange(e.target.value)}
|
|
infoIconProps={{
|
|
children: (
|
|
<TooltipContentContainer>
|
|
<Text>This secret is provided when the API server is first started. Refer to your server logs to find it.</Text>
|
|
</TooltipContentContainer>
|
|
),
|
|
}}
|
|
/>
|
|
<field.TextFieldErrorMessage {...field.state.meta} />
|
|
</>
|
|
)}
|
|
/>
|
|
|
|
<div style={{ marginTop: 18, display: 'flex', gap: 8, justifySelf: 'center' }}>
|
|
<form.SubmitButton loading={isPending} label={{ default: 'Initialize' }} />
|
|
</div>
|
|
</form>
|
|
</Box>
|
|
</Container>
|
|
</Flex>
|
|
|
|
<ToastContainer
|
|
position="top-center"
|
|
autoClose={false}
|
|
newestOnTop={false}
|
|
closeOnClick
|
|
rtl={false}
|
|
pauseOnFocusLoss
|
|
draggable={false}
|
|
theme="colored"
|
|
transition={Slide}
|
|
/>
|
|
</>
|
|
);
|
|
}
|