cameras/src/app/setup/setup-form.tsx
Michael Mainguy 5d181f3b3b Enforce admin login: login, first-run setup, proxy and access checks
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>
2026-09-19 11:58:20 -05:00

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>
);
}