Files
vliaudatandClaude Opus 5 5c0b8119a4 fix: let a read-only account sign out
A React server action POSTs to the URL of the page it lives on, so the
middleware's method check over /admin refused every form on the site to
a viewer — including the sign-out button, which surfaced as "an
unexpected response was received from the server".

Gating pages by HTTP method was the wrong instrument: at the edge there
is no way to tell a form that changes the shop's hours from one that
ends a session. The method check now applies to /api/admin only, and
page-level writes are authorised inside the actions themselves, where
the intent is actually known. lib/auth/actions.ts carries that check and
returns an error rather than throwing, since "you do not have
permission" is a normal outcome and not a crash.

The edge decision moves into a pure function with a regression test for
this exact case. These rules are short, but they are the only thing in
front of the administration and one of them has now been got wrong once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 18:51:40 +02:00

49 lines
1.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Authorisation for React server actions.
*
* A server action POSTs to the URL of the page it sits on, so the edge
* middleware cannot tell a form that changes the shop's hours from a form that
* signs someone out. It therefore only authenticates page requests, and every
* action that writes states its own requirement here.
*/
import { auth } from './index';
import type { Role } from './roles';
import { canWrite } from './roles';
export type Caller = { email: string; role: Role };
export type ActionResult<T = void> = { ok: true; value: T } | { ok: false; error: string };
export async function currentCaller(): Promise<Caller | null> {
const session = await auth();
if (!session?.user) {
return null;
}
return {
// The subject is the e-mail address, which is what the audit trail records.
email: session.user.email ?? 'inconnu',
role: session.user.role,
};
}
/**
* Resolves the caller, or an error to show the user.
*
* Returns rather than throws: an action that throws renders the Next error
* overlay, and "you do not have permission" is a normal outcome, not a crash.
*/
export async function requireAdmin(): Promise<ActionResult<Caller>> {
const caller = await currentCaller();
if (!caller) {
return { ok: false, error: 'Session expirée. Reconnectez-vous.' };
}
if (!canWrite(caller.role)) {
return {
ok: false,
error: `Ce compte est en lecture seule. Demandez à être ajouté au groupe d’administration.`,
};
}
return { ok: true, value: caller };
}