added init page
This commit is contained in:
@@ -5,7 +5,7 @@ import './app.css';
|
||||
import AppTheme from './components/theme';
|
||||
import { ApiProvider } from './providers/ApiProvider';
|
||||
import { LayoutProvider } from './providers/LayoutProvider';
|
||||
// import { LayoutContainer } from './routes/layout';
|
||||
import { Tooltip } from 'radix-ui';
|
||||
|
||||
export const links: Route.LinksFunction = () => [];
|
||||
|
||||
@@ -31,9 +31,11 @@ export default function App() {
|
||||
return (
|
||||
<AppTheme>
|
||||
<ApiProvider>
|
||||
<Tooltip.Provider delayDuration={250}>
|
||||
<LayoutProvider>
|
||||
<Outlet />
|
||||
</LayoutProvider>
|
||||
</Tooltip.Provider>
|
||||
</ApiProvider>
|
||||
</AppTheme>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { type RouteConfig, index, layout, route } from '@react-router/dev/routes
|
||||
|
||||
export default [
|
||||
route('login', 'routes/auth/login.tsx'),
|
||||
route('init', 'routes/init.tsx'),
|
||||
layout('routes/layout.tsx', [index('routes/home.tsx')]),
|
||||
// route('init', 'routes/init.tsx'),
|
||||
// catch-all 404 route
|
||||
route('*', 'routes/404.tsx'),
|
||||
] satisfies RouteConfig;
|
||||
|
||||
170
apps/frontend/app/routes/init.tsx
Normal file
170
apps/frontend/app/routes/init.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user