cameras/src/app/page.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

42 lines
1.5 KiB
TypeScript

import { requirePageAccess } from "@/lib/access";
import { listCameras } from "@/lib/camera-registry";
import CameraCard from "./camera-card";
import CameraScanner from "./camera-scanner";
import RefreshRateSelect from "./refresh-rate-select";
import { refreshMsSchema } from "./refresh-rate";
export default async function Home({ searchParams }: PageProps<"/">) {
await requirePageAccess();
const [cameras, { refresh }] = await Promise.all([listCameras(), searchParams]);
const intervalMs = refreshMsSchema.parse(refresh);
return (
<main className="mx-auto w-full max-w-4xl flex-1 px-4 py-12 font-sans">
<h1 className="text-3xl font-semibold tracking-tight">ONVIF Cameras</h1>
<p className="mt-2 text-zinc-600 dark:text-zinc-400">
Discover ONVIF cameras on the local network using WS-Discovery.
</p>
<section className="mt-8">
<div className="flex flex-wrap items-center gap-4">
<CameraScanner />
<RefreshRateSelect value={intervalMs} />
</div>
<div className="mt-6">
<p className="text-sm text-zinc-600 dark:text-zinc-400">
{cameras.length === 0
? "No cameras known yet. Scan the network to find them."
: `${cameras.length} camera${cameras.length === 1 ? "" : "s"}`}
</p>
<ul className="mt-3 grid gap-3 sm:grid-cols-2">
{cameras.map((cam) => (
<CameraCard key={cam.id} cam={cam} intervalMs={intervalMs} />
))}
</ul>
</div>
</section>
</main>
);
}