diff --git a/apps/frontend/app/root.tsx b/apps/frontend/app/root.tsx index a4d1e9f..57f86ec 100644 --- a/apps/frontend/app/root.tsx +++ b/apps/frontend/app/root.tsx @@ -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 ( - - - + + + + + ); diff --git a/apps/frontend/app/routes.ts b/apps/frontend/app/routes.ts index 3527a78..baff85a 100644 --- a/apps/frontend/app/routes.ts +++ b/apps/frontend/app/routes.ts @@ -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; diff --git a/apps/frontend/app/routes/init.tsx b/apps/frontend/app/routes/init.tsx new file mode 100644 index 0000000..9ef7067 --- /dev/null +++ b/apps/frontend/app/routes/init.tsx @@ -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 ( + <> + + + + + Initialize YANPM + + + + Create the initial admin user + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + ( + <> + field.handleChange(e.target.value)} + /> + + + )} + /> + + ( + <> + field.handleChange(e.target.value)} + showPasswordToggle + /> + + + )} + /> + + ( + <> + field.handleChange(e.target.value)} + infoIconProps={{ + children: ( + + This secret is provided when the API server is first started. Refer to your server logs to find it. + + ), + }} + /> + + + )} + /> + +
+ +
+ +
+
+
+ + + + ); +}