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>
42 lines
1.5 KiB
TypeScript
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>
|
|
);
|
|
}
|