Completes securing the web front end (vrek gol-wqf95dq, iss-r5vrjx7). - /login: Server Action with a generic error, same-site-only redirect back to ?next=, and throttling of failed logins (10 per address and 100 overall per 15 min). The header shows "Signed in as" with Sign out (iss-nj9wmwp). - First run with no admin: instrumentation prints a one-time setup code, shared with the app through globalThis. /setup requires it, and 5 wrong codes rotate it. "Skip for now" runs unsecured for the browser session behind a red warning banner on every page (iss-9nxdndr). - src/proxy.ts: optimistic redirects to /login or /setup, 401 for the API, and the 12 h sliding session refresh. requirePageAccess() and apiAccessDenied() re-check in the page and all 6 route handlers (iss-76d5wrb). - An expired session now shows "Your session has ended" instead of the camera-login form. - README documents in-app setup, skipping and signing in. Verified with unit tests (383, 99.9% line coverage), end to end against `next start`, and manually in a browser by the user. Refreshes the vrek export. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState } from "react";
|
|
import { setupAdmin, type FormState } from "../auth-actions";
|
|
|
|
const inputClass =
|
|
"rounded border border-zinc-300 bg-transparent px-2 py-1.5 dark:border-zinc-700";
|
|
|
|
export default function SetupForm() {
|
|
const [state, action, pending] = useActionState<FormState, FormData>(setupAdmin, {});
|
|
|
|
return (
|
|
<form action={action} className="mt-6 flex flex-col gap-3 text-sm">
|
|
<label className="flex flex-col gap-1">
|
|
Setup code (from the server console)
|
|
<input
|
|
name="code"
|
|
autoComplete="off"
|
|
placeholder="XXXX-XXXX"
|
|
required
|
|
className={`${inputClass} font-mono uppercase`}
|
|
/>
|
|
</label>
|
|
<label className="flex flex-col gap-1">
|
|
Admin username
|
|
<input name="username" defaultValue="admin" autoComplete="username" required className={inputClass} />
|
|
</label>
|
|
<label className="flex flex-col gap-1">
|
|
Password (at least 12 characters)
|
|
<input
|
|
name="password"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
minLength={12}
|
|
required
|
|
className={inputClass}
|
|
/>
|
|
</label>
|
|
<label className="flex flex-col gap-1">
|
|
Repeat password
|
|
<input
|
|
name="confirm"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
minLength={12}
|
|
required
|
|
className={inputClass}
|
|
/>
|
|
</label>
|
|
{state.error && (
|
|
<p role="alert" className="text-red-600">
|
|
{state.error}
|
|
</p>
|
|
)}
|
|
<button
|
|
type="submit"
|
|
disabled={pending}
|
|
className="rounded-md bg-black px-4 py-2 font-medium text-white disabled:opacity-50 dark:bg-white dark:text-black"
|
|
>
|
|
{pending ? "Securing…" : "Create admin and sign in"}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|