← Retour aux projets

Projet personnel
Admin Dashboard — Next.js 16 + Supabase
Une route /admin sécurisée par HTTP Basic Auth, client Supabase service_role, et zéro dépendance supplémentaire.
Pourquoi j'ai construit ça
Tout portfolio data-driven a besoin, à un moment, d'un moyen de surveiller ses propres données sans ouvrir le dashboard Supabase à chaque fois. Je voulais voir mes messages de contact, le nombre de projets et l'état de mes certifications en un coup d'œil — mais construire un système d'authentification complet m'a semblé totalement surdimensionné pour un outil personnel.
Le défi : comment protéger une route interne dans Next.js 16 sans aucune nouvelle dépendance, tout en pouvant lire des tables verrouillées par Row Level Security pour les utilisateurs anonymes ?
Contraintes techniques à résoudre
- Next.js 16 remplace middleware.ts par proxy.ts — le même fichier gère le routing i18n pour next-intl. L'auth admin devait être injectée sans casser les préfixes de locale.
- L'Edge runtime n'a pas Buffer : l'approche Node.js classique (Buffer.from(b64, 'base64').toString()) échoue silencieusement. J'ai dû utiliser atob() à la place.
- La table messages est bloquée en SELECT pour le rôle anon par la politique RLS. La lire nécessite un client service_role — qui ne doit jamais quitter le serveur.
- Le layout admin ne doit PAS inclure <html>/<body> — app/layout.tsx les fournit déjà. Les dupliquer provoque une erreur de hydration React.
- Le chemin /admin doit être exclu de la logique de préfixage de locale de next-intl, sinon le proxy tente de rediriger /admin vers /en/admin.
Comment je l'ai construit
- 1Étape 1 — Client Supabase adminCréation de lib/supabase/admin.ts utilisant la clé SUPABASE_SERVICE_ROLE_KEY (sans préfixe NEXT_PUBLIC_ — côté serveur uniquement). Ce client contourne toutes les politiques RLS et peut faire des SELECT sur la table messages que le client anon public ne peut pas lire.
- 2Étape 2 — HTTP Basic Auth dans proxy.tsAjout d'une garde adminAuth() en début de la fonction proxy(). Si le pathname commence par /admin, le header Authorization est vérifié avant de passer au gestionnaire next-intl. Utilisation d'atob() pour un décodage base64 compatible Edge. ADMIN_USERNAME et ADMIN_PASSWORD sont définis dans .env.local (côté serveur uniquement).
- 3Étape 3 — Layout admin isoléCréation de app/admin/layout.tsx retournant un wrapper <div> (pas <html>/<body>) avec un thème Slate sombre, un header minimal, et les métadonnées robots: index: false. Le layout est entièrement hors de l'arbre de routing [locale].
- 4Étape 4 — Dashboard Server Componentapp/admin/page.tsx récupère les quatre tables en parallèle via Promise.all(). Stat cards, table complète des projets (status / track / featured / date), 20 derniers messages de contact avec liens mailto cliquables, table des certifications et compteurs de témoignages. Zéro JavaScript client.
Noyau : HTTP Basic Auth compatible Edge (proxy.ts)
function adminAuth(request: NextRequest): NextResponse | null {
const expectedUser = process.env.ADMIN_USERNAME;
const expectedPass = process.env.ADMIN_PASSWORD;
if (!expectedUser || !expectedPass) {
return new NextResponse("Admin non configuré.", { status: 503 });
}
const authHeader = request.headers.get("authorization");
if (authHeader?.startsWith("Basic ")) {
try {
const decoded = atob(authHeader.slice(6)); // Edge-safe — sans Buffer
const colonIdx = decoded.indexOf(":");
if (colonIdx !== -1) {
const user = decoded.slice(0, colonIdx);
const pass = decoded.slice(colonIdx + 1);
if (user === expectedUser && pass === expectedPass) {
return null; // ✅ autorisé
}
}
} catch {
// base64 malformé — rejeter
}
}
return new NextResponse("Authentification requise.", {
status: 401,
headers: { "WWW-Authenticate": 'Basic realm="Admin", charset="UTF-8"' },
});
}Ce que ce projet démontre
0
Packages npm ajoutés
HTTP Basic Auth via proxy.ts + variables d'env uniquement
4
Tables surveillées
projets · certifications · messages · témoignages
0 Ko
JavaScript client
100% Server Components — aucun overhead d'hydration
~25
Lignes de code d'auth
Dans proxy.ts — pas de page de connexion, pas de session, pas de JWT
Apprentissages clés
- L'Edge runtime est plus restrictif que Node.js : toujours vérifier quelles API sont disponibles (Buffer, crypto, fs…) avant de les utiliser.
- next-intl et une logique de proxy personnalisée peuvent coexister proprement si on garde le chemin admin avant d'appeler le gestionnaire intl.
- Les Server Components sont le bon défaut pour les dashboards internes — pas d'état, pas d'effets, pas de bundles envoyés au navigateur.
- RLS est votre filet de sécurité, pas un substitut aux secrets côté serveur : même avec service_role, la clé ne quitte jamais le serveur.