Files
ita-ito-horaires/app/login/page.tsx
T
vliaudatandClaude Opus 5 efd91c3e8e fix: survive an unconfigured identity provider
Auth.js refuses to build a provider with no issuer, and that refusal
takes down the whole auth layer — including reading a session that
already exists. A missing or misspelled AUTH_AUTHENTIK_ISSUER would
therefore lock everyone out of an otherwise healthy application, and
explain itself only as a stack trace in the logs.

The provider is now registered only when its three settings are present.
Sessions stay readable either way, and the sign-in page says which
variables are missing instead of offering a button that fails.

Found by the first CI run, which has no .env to inherit from: every
signed-in test failed at once, looking exactly like a broken cookie.
The local suite had been passing on variables Playwright was quietly
inheriting from the development environment — so the E2E server is now
given explicit placeholders rather than whatever happens to be around.

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

83 lines
2.8 KiB
TypeScript
Raw 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.
import Image from 'next/image';
import { redirect } from 'next/navigation';
import { auth, isAuthentikConfigured, 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}
{!isAuthentikConfigured ? (
<p
role="alert"
className="mt-8 rounded-[var(--radius-md)] border border-[var(--danger)] bg-[var(--danger-tint)] px-4 py-3 text-sm text-[var(--danger)]"
>
La connexion n’est pas configurée&nbsp;: <span className="font-mono">AUTH_AUTHENTIK_ID</span>,{' '}
<span className="font-mono">AUTH_AUTHENTIK_SECRET</span> et{' '}
<span className="font-mono">AUTH_AUTHENTIK_ISSUER</span> doivent être renseignés.
</p>
) : (
<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>
);
}