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
+
+ (
+ <>
+ 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.
+
+ ),
+ }}
+ />
+
+ >
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}