Sign-in goes through Authentik over OIDC with PKCE. Verified against the live provider: the discovery issuer matches the configured one exactly, and the authorize redirect carries code_challenge_method=S256. Sessions are JWTs with no database adapter, which keeps the module usable from edge middleware and makes a sign-in cost no query. The trade-off is stated in the code: the role travels in the token, so removing someone from the admin group takes effect at the next sign-in or when the eight-hour session expires, not instantly. Immediate revocation would mean asking Authentik on every request, which is what api_llm_loxi does and what this application deliberately does not — it drives a shop window, not a fleet. Group matching is trimmed and case-insensitive. Authentik group names are case-sensitive, but a capitalisation mismatch between the group and the environment variable locks the shop owner out silently, and that is the worse of the two failures. An empty variable never promotes anyone. Authorisation is enforced twice. The middleware covers every /admin page and /api/admin route at the edge; a guard inside the handlers repeats the check, because a matcher is a string, strings get edited, and a route falling outside one should not be the same thing as a route with no access control. The rule itself lives in its own framework-free module so it can be tested directly. Unknown HTTP verbs count as writes: new methods arrive locked. Pages get a redirect to the sign-in screen, API routes get a status code — a fetch that receives an HTML login page is a confusing way to learn you are signed out. The device API stays outside the matcher, as the panel cannot sign in and carries its own bearer token; this is covered by a check that /api/display still answers 401 rather than redirecting. The audit diff compares values by their JSON form, so slot arrays and dates compare by value rather than identity, and a save that changed nothing writes no entry. Recording never throws: losing the trail is bad, refusing the user's change because the trail could not be written is worse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
import Image from 'next/image';
|
|
import { redirect } from 'next/navigation';
|
|
|
|
import { auth, signIn } from '@/lib/auth';
|
|
|
|
export const metadata = { title: 'Connexion — ITA ITO' };
|
|
|
|
const ERRORS: Record<string, string> = {
|
|
AccessDenied: "Ce compte n'a pas accès à cette application.",
|
|
Configuration: "La configuration de la connexion est incomplète. Prévenez l'administrateur.",
|
|
Verification: 'Le lien de connexion a expiré. Réessayez.',
|
|
};
|
|
|
|
export default async function LoginPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ error?: string; from?: string }>;
|
|
}) {
|
|
const { error, from } = await searchParams;
|
|
|
|
// Someone who is already signed in has no business on this page.
|
|
if (await auth()) {
|
|
redirect(from && from.startsWith('/admin') ? from : '/admin');
|
|
}
|
|
|
|
const providerName = process.env.AUTHENTIK_DISPLAY_NAME?.trim() || 'Loxi';
|
|
|
|
return (
|
|
<main className="flex min-h-dvh items-center justify-center px-6 py-16">
|
|
<div className="w-full max-w-sm text-center">
|
|
<Image
|
|
src="/brand/logo.png"
|
|
alt="ITA ITO"
|
|
width={406}
|
|
height={194}
|
|
priority
|
|
className="mx-auto h-auto w-44"
|
|
/>
|
|
|
|
<h1 className="mt-10 text-xl">Horaires de la boutique</h1>
|
|
<p className="mt-2 text-sm text-[var(--ink-muted)]">Réservé à l’équipe ITA ITO.</p>
|
|
|
|
{error ? (
|
|
<p
|
|
role="alert"
|
|
className="mt-6 rounded-[var(--radius-md)] border border-[var(--danger)] bg-[var(--danger-tint)] px-4 py-3 text-sm text-[var(--danger)]"
|
|
>
|
|
{ERRORS[error] ?? 'La connexion a échoué. Réessayez.'}
|
|
</p>
|
|
) : null}
|
|
|
|
<form
|
|
className="mt-8"
|
|
action={async () => {
|
|
'use server';
|
|
await signIn('authentik', {
|
|
redirectTo: from && from.startsWith('/admin') ? from : '/admin',
|
|
});
|
|
}}
|
|
>
|
|
<button
|
|
type="submit"
|
|
className="w-full rounded-[var(--radius-md)] bg-[var(--accent)] px-5 py-3 text-base font-medium text-[var(--on-accent)] transition-colors hover:bg-[var(--accent-hover)]"
|
|
>
|
|
Se connecter avec {providerName}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|