Files
ita-ito-horaires/middleware.ts
T
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

48 lines
1.7 KiB
TypeScript

import { NextResponse } from 'next/server';
import { decideAccess } from '@/lib/auth/access';
import { auth } from '@/lib/auth';
/**
* The single choke point for administrative access.
*
* Every /admin page and every /api/admin route passes through here, so no
* individual page can forget to check. Pages get a redirect to the sign-in
* screen; API routes get a status code, because a fetch that receives an HTML
* login page is a confusing way to learn you are signed out.
*
* Write access is enforced here for /api/admin only, and deliberately NOT for
* pages. A React server action POSTs to the URL of the page it lives on, so a
* method check over /admin would refuse every form on the site to a read-only
* account — including the sign-out button, which is not a write by any useful
* definition. Page-level write protection belongs inside the actions
* themselves, via lib/auth/actions.ts, where the intent is actually known.
*
* The device API (/api/setup, /api/display, /api/log) is deliberately outside
* this matcher: the panel cannot sign in, and carries its own bearer token.
*/
export default auth((request) => {
const { pathname } = request.nextUrl;
const decision = decideAccess({
pathname,
method: request.method,
role: request.auth?.user?.role,
});
if (decision.kind === 'redirect') {
const target = new URL('/login', request.nextUrl.origin);
target.searchParams.set('from', pathname);
return NextResponse.redirect(target);
}
if (decision.kind === 'deny') {
return NextResponse.json({ error: decision.error }, { status: decision.status });
}
return NextResponse.next();
});
export const config = {
matcher: ['/admin/:path*', '/api/admin/:path*'],
};