Files
ita-ito-horaires/app/admin/page.tsx
T
vliaudatandClaude Opus 5 f6ff81bf16 feat: explain why an account is read-only
A viewer account has exactly two causes, fixed in completely different
places: the identity provider sent no groups at all, or it sent groups
that do not include the one granting write access. From the outside the
two look identical, so the dashboard now says which it is and what to
do about it, and the sign-in logs the same thing server-side.

The groups are carried in the session for that purpose, capped so the
cookie cannot grow with someone's group membership. Group names are not
secrets, and a support conversation that starts with the actual claim is
a thirty-second fix rather than a guessing game.

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

71 lines
2.7 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 { auth } from '@/lib/auth';
export const metadata = { title: 'Tableau de bord — ITA ITO' };
export default async function AdminHome() {
const session = await auth();
const user = session?.user;
const isAdmin = user?.role === 'admin';
return (
<main className="mx-auto max-w-5xl px-4 py-10">
<h1 className="text-2xl">Tableau de bord</h1>
<p className="mt-2 text-[var(--ink-muted)]">
Connecté en tant que {user?.email} ({isAdmin ? 'administrateur' : 'lecture seule'}).
</p>
{!isAdmin ? <ReadOnlyExplanation groups={user?.groups ?? []} expected={user?.adminGroup ?? ''} /> : null}
</main>
);
}
/**
* A read-only account has exactly two causes, fixed in completely different
* places: either the identity provider sent no groups at all, or it sent
* groups that do not include the one that grants write access. Saying which
* turns a support conversation into a thirty-second fix.
*/
function ReadOnlyExplanation({ groups, expected }: { groups: string[]; expected: string }) {
const claimMissing = groups.length === 0;
return (
<section className="mt-8 max-w-2xl rounded-[var(--radius-md)] border border-[var(--line-strong)] bg-[var(--surface)] p-5">
<h2 className="text-base">Pourquoi ce compte est-il en lecture seule&nbsp;?</h2>
<dl className="mt-4 space-y-3 text-sm">
<div>
<dt className="text-[var(--ink-muted)]">Groupe donnant l’accès en écriture</dt>
<dd className="mt-0.5 font-mono">{expected || '(non configuré)'}</dd>
</div>
<div>
<dt className="text-[var(--ink-muted)]">Groupes reçus d’Authentik</dt>
<dd className="mt-0.5 font-mono">
{claimMissing ? 'aucun — le claim « groups » est absent' : groups.join(', ')}
</dd>
</div>
</dl>
<p className="mt-4 text-sm text-[var(--ink-muted)]">
{claimMissing ? (
<>
Authentik n’envoie aucun groupe. Dans le provider OAuth2/OpenID, vérifiez que le scope{' '}
<span className="font-mono">profile</span> est bien sélectionné et que{' '}
<em>Include claims in id_token</em> est activé.
</>
) : (
<>
Authentik envoie bien des groupes, mais pas celui attendu. Ajoutez ce compte au groupe{' '}
<span className="font-mono">{expected}</span>, ou corrigez{' '}
<span className="font-mono">AUTHENTIK_ADMIN_GROUP</span> pour qu’il corresponde à l’un
des groupes ci-dessus.
</>
)}
</p>
<p className="mt-3 text-sm text-[var(--ink-muted)]">
Le rôle est fixé à la connexion&nbsp;: après correction, déconnectez-vous et reconnectez-vous.
</p>
</section>
);
}