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
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
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 : <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>
|
||
);
|
||
}
|