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;
$$;