remove server admin from config and add onboarding ui

This commit is contained in:
miloschwartz
2025-06-19 22:11:05 -04:00
parent f300838f8e
commit d03f45279c
15 changed files with 345 additions and 190 deletions

View File

@@ -0,0 +1,17 @@
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { InitialSetupCompleteResponse } from "@server/routers/auth";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
export default async function Layout(props: { children: React.ReactNode }) {
const setupRes = await internal.get<
AxiosResponse<InitialSetupCompleteResponse>
>(`/auth/initial-setup-complete`, await authCookieHeader());
const complete = setupRes.data.data.complete;
if (complete) {
redirect("/");
}
return <div>{props.children}</div>;
}

View File

@@ -0,0 +1,174 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { passwordSchema } from "@server/auth/passwordSchema";
const formSchema = z
.object({
email: z.string().email({ message: "Invalid email address" }),
password: passwordSchema,
confirmPassword: z.string()
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords do not match"
});
export default function InitialSetupPage() {
const router = useRouter();
const api = createApiClient(useEnvContext());
const t = useTranslations();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [checking, setChecking] = useState(true);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
confirmPassword: ""
}
});
async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true);
setError(null);
try {
const res = await api.put("/auth/set-server-admin", {
email: values.email,
password: values.password
});
if (res && res.status === 200) {
router.replace("/");
return;
}
} catch (e) {
setError(formatAxiosError(e, t("setupErrorCreateAdmin")));
}
setLoading(false);
}
return (
<Card className="w-full max-w-md mx-auto mt-12">
<CardHeader>
<div className="flex flex-row items-center justify-center">
<Image
src="/logo/pangolin_orange.svg"
alt={t("pangolinLogoAlt")}
width={100}
height={100}
/>
</div>
<div className="text-center space-y-1">
<h1 className="text-2xl font-bold mt-1">
{t("initialSetupTitle")}
</h1>
<CardDescription>
{t("initialSetupDescription")}
</CardDescription>
</div>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("email")}</FormLabel>
<FormControl>
<Input
{...field}
autoComplete="username"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t("password")}</FormLabel>
<FormControl>
<Input
type="password"
{...field}
autoComplete="new-password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("confirmPassword")}
</FormLabel>
<FormControl>
<Input
type="password"
{...field}
autoComplete="new-password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
type="submit"
className="w-full"
disabled={loading}
>
{t("createAdminAccount")}
</Button>
</form>
</Form>
</CardContent>
</Card>
);
}

View File

@@ -11,6 +11,7 @@ import { pullEnv } from "@app/lib/pullEnv";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { Layout } from "@app/components/Layout";
import { rootNavItems } from "./navigation";
import { InitialSetupCompleteResponse } from "@server/routers/auth";
export const dynamic = "force-dynamic";
@@ -27,6 +28,15 @@ export default async function Page(props: {
const getUser = cache(verifySession);
const user = await getUser({ skipCheckVerifyEmail: true });
const setupRes = await internal.get<
AxiosResponse<InitialSetupCompleteResponse>
>(`/auth/initial-setup-complete`, await authCookieHeader());
const complete = setupRes.data.data.complete;
if (!complete) {
console.log("compelte", complete);
redirect("/auth/initial-setup");
}
if (!user) {
if (params.redirect) {
const safe = cleanRedirect(params.redirect);

View File

@@ -14,6 +14,8 @@ const alertVariants = cva(
"border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
success:
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
info:
"border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500",
},
},
defaultVariants: {