Wire authentication into the application

Adds the sign-in screen, sign-out, and a server-side guard on every
application route. The guard lives in the layout rather than the proxy
because the proxy cannot query the database to check whether a session
was revoked — and revocation is the reason sessions are stored there.

Sign-in returns one message for an unknown account and for a wrong
password, and verifies a dummy hash when the account does not exist, so
neither the wording nor the timing enumerates staff addresses. An
end-to-end test compares the two messages rather than trusting the
code to keep them aligned.

The shell now shows the signed-in person and their role from the
database instead of hardcoded initials.

Playwright signs in once in a setup project and shares the cookie;
argon2 is deliberately slow, and logging in per test would also drive
the shared failed-attempt counter toward a lockout. The seed resets
that counter so repeated local runs cannot lock the demo account.

Two test locators had to be scoped to the form: Next's route announcer
carries role="alert" and an empty string, which silently satisfied the
assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
This commit is contained in:
Claude committed 2026-08-07 22:45:24 +00:00
1 parent 11763142d5
commit 2c0e9e8dd4
12 files changed
+361 -19

No files matched your search

+21 -4
View File
@@ -8,6 +8,7 @@ import { ThemeToggle } from '@/components/shell/ThemeToggle';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { cx } from '@/lib/cx';
import { signOutAction } from '@/server/auth/actions';
import {
isActive,
NAVIGATION,
@@ -19,9 +20,19 @@ import {
export interface AppShellProps {
children: ReactNode;
initials: string;
fullName: string;
roleName: string;
accountName: string;
}
export function AppShell({ children }: AppShellProps) {
export function AppShell({
children,
initials,
fullName,
roleName,
accountName,
}: AppShellProps) {
const pathname = usePathname();
const section = sectionForPath(pathname);
@@ -67,11 +78,17 @@ export function AppShell({ children }: AppShellProps) {
</Button>
<ThemeToggle />
<span
aria-hidden
className="flex size-7 items-center justify-center rounded-full bg-accent-soft text-xs font-semibold text-accent-soft-ink"
title={`${fullName} · ${roleName}`}
>
MR
<span aria-hidden>{initials}</span>
<span className="sr-only">{`${fullName}, ${roleName}`}</span>
</span>
<form action={signOutAction}>
<Button size="sm" type="submit">
Déconnexion
</Button>
</form>
</header>
<div className="flex min-h-0 flex-1">
@@ -86,7 +103,7 @@ export function AppShell({ children }: AppShellProps) {
<p className="border-t border-line-1 px-2.5 pt-3 text-micro leading-relaxed text-ink-3">
Instance auto-hébergée
<br />
Maison Rivage · 34 établissements
{accountName}
</p>
</aside>