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
71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
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 ?</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 : après correction, déconnectez-vous et reconnectez-vous.
|
||
</p>
|
||
</section>
|
||
);
|
||
}
|