From 86756c41da9cec5e5fd0ec4de1f7120f47f3bea2 Mon Sep 17 00:00:00 2001 From: Valentin ROBIN Date: Sun, 23 Aug 2026 22:12:31 +0200 Subject: [PATCH] =?UTF-8?q?Corrige=20les=20comptes=20invit=C3=A9s=20qui=20?= =?UTF-8?q?restaient=20bloqu=C3=A9s=20sans=20mot=20de=20passe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le pseudo temporaire fixe "Nouveau membre" entrait en collision (contrainte unique) dès qu'une 2e personne était invitée avant que la 1re ait fini son inscription ; il inclut maintenant un suffixe d'uuid. Le token d'invitation pouvait aussi s'établir sur n'importe quelle page (pas seulement /signup), laissant certains comptes connectés sans jamais voir le formulaire de mot de passe — /signup affiche désormais un état de chargement au lieu d'un écran vide, et le proxy renvoie systématiquement vers /signup tant que le compte n'est pas finalisé. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 +- src/app/signup/page.tsx | 11 +++++++++-- src/lib/supabase/middleware.ts | 21 +++++++++++++++++++++ supabase/schema.sql | 10 +++++++++- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 76376dc..d605cd8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,7 +125,7 @@ Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accè ## 4. Authentification -**Sur invitation uniquement**, plus d'auto-inscription publique : "Allow new users to sign up" est désactivé côté Supabase. Un Archonte invite via Authentication → Users → Invite user ; le lien reçu établit une session directement sur `/signup`, qui sert alors à finaliser le compte (mot de passe, pseudo, photo) — sans session valide, la page affiche un message « invitation requise ». Le profil est créé dès l'invitation par le trigger `handle_new_user` (pseudo temporaire `"Nouveau membre"` jusqu'à finalisation). Nécessite un SMTP custom côté Supabase (le service intégré est trop limité en volume, voir README). Le pseudo est un simple nom d'affichage stocké dans `profiles`, jamais l'email n'est montré aux autres Citoyens — seuls les Archontes le voient, sur `/admin` (via `admin_list_members()`, voir §3). +**Sur invitation uniquement**, plus d'auto-inscription publique : "Allow new users to sign up" est désactivé côté Supabase. Un Archonte invite via Authentication → Users → Invite user ; le lien reçu établit une session directement sur `/signup`, qui sert alors à finaliser le compte (mot de passe, pseudo, photo) — sans session valide, la page affiche un message « invitation requise ». Le profil est créé dès l'invitation par le trigger `handle_new_user` (pseudo temporaire `"Nouveau membre " + 8 premiers caractères de l'uuid` jusqu'à finalisation — le suffixe garantit l'unicité quand plusieurs personnes sont invitées avant d'avoir fini leur inscription ; `/signup` détecte l'état « pas encore finalisé » par préfixe, jamais par égalité stricte). Nécessite un SMTP custom côté Supabase (le service intégré est trop limité en volume, voir README). Le pseudo est un simple nom d'affichage stocké dans `profiles`, jamais l'email n'est montré aux autres Citoyens — seuls les Archontes le voient, sur `/admin` (via `admin_list_members()`, voir §3). --- diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 8af6dde..da68ce2 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -67,7 +67,7 @@ export default function SignupPage() { .eq("id", user.id) .single(); - if (profile && profile.pseudo !== "Nouveau membre") { + if (profile && !profile.pseudo.startsWith("Nouveau membre")) { window.location.href = "/leaderboard"; return; } @@ -147,7 +147,14 @@ export default function SignupPage() { } if (status === "checking") { - return
; + return ( +
+
+ +

Connexion à ton invitation…

+
+
+ ); } if (status === "no-invite") { diff --git a/src/lib/supabase/middleware.ts b/src/lib/supabase/middleware.ts index 7680d1c..8571b33 100644 --- a/src/lib/supabase/middleware.ts +++ b/src/lib/supabase/middleware.ts @@ -47,6 +47,7 @@ export async function updateSession(request: NextRequest) { const { pathname } = request.nextUrl; const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path)); const isAuthPath = AUTH_PATHS.some((path) => pathname.startsWith(path)); + const isSignup = pathname.startsWith("/signup"); if (!user && isProtected) { const url = request.nextUrl.clone(); @@ -54,6 +55,26 @@ export async function updateSession(request: NextRequest) { return NextResponse.redirect(url); } + // Le token d'un lien d'invitation Supabase s'établit dès qu'il touche le + // navigateur (detectSessionInUrl du SDK), sur N'IMPORTE QUELLE page, pas + // seulement /signup. Si le compte n'a pas encore fini son inscription + // (pseudo toujours au placeholder du trigger handle_new_user), on le + // renvoie systématiquement vers /signup pour ne jamais le laisser bloqué + // connecté mais sans mot de passe choisi. + if (user && !isSignup) { + const { data: profile } = await supabase + .from("profiles") + .select("pseudo") + .eq("id", user.id) + .single(); + + if (profile?.pseudo.startsWith("Nouveau membre")) { + const url = request.nextUrl.clone(); + url.pathname = "/signup"; + return NextResponse.redirect(url); + } + } + if (user && isAuthPath) { const url = request.nextUrl.clone(); url.pathname = "/leaderboard"; diff --git a/supabase/schema.sql b/supabase/schema.sql index abb5020..6cdf157 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -147,6 +147,11 @@ create trigger profiles_before_update -- Création automatique du profil à l'inscription (auth.users → profiles), -- indépendamment de l'état de la session (fonctionne même si l'email -- n'est pas encore confirmé, contrairement à un insert fait depuis le client). +-- Le pseudo temporaire inclut les 8 premiers caractères de l'uuid : un +-- littéral fixe "Nouveau membre" pour tout le monde entrait en collision +-- avec profiles_pseudo_lower_key (unique) dès qu'une 2e personne était +-- invitée avant que la 1re ait fini son inscription — ce qui n'était +-- jamais arrivé avant d'inviter plusieurs personnes d'affilée. create or replace function public.handle_new_user() returns trigger language plpgsql @@ -154,7 +159,10 @@ security definer set search_path = public as $$ begin insert into public.profiles (id, pseudo) - values (new.id, coalesce(new.raw_user_meta_data ->> 'pseudo', 'Nouveau membre')); + values ( + new.id, + coalesce(new.raw_user_meta_data ->> 'pseudo', 'Nouveau membre ' || substr(new.id::text, 1, 8)) + ); return new; end; $$;