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

+25 -1
View File
@@ -138,9 +138,19 @@ export async function revokeAllSessions(
return result.count;
}
export interface SessionUser {
firstName: string;
lastName: string;
email: string;
initials: string;
}
export interface SessionContext {
actor: Actor;
sessionId: string;
user: SessionUser;
accountName: string;
roleName: string;
}
/**
@@ -162,6 +172,7 @@ export async function resolveSession(
memberships: {
where: { status: 'ACTIVE', archivedAt: null },
include: {
account: { select: { name: true } },
role: { include: { permissions: { include: { permission: true } } } },
scopes: true,
},
@@ -199,7 +210,20 @@ export async function resolveSession(
scope,
};
return { actor, sessionId: session.id };
const { firstName, lastName, email } = session.user;
return {
actor,
sessionId: session.id,
user: {
firstName,
lastName,
email,
initials: `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(),
},
accountName: membership.account.name,
roleName: membership.role.name,
};
}
/** Session courante depuis le cookie, ou `null`. */