Corrige les comptes invités qui restaient bloqués sans mot de passe
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 <noreply@anthropic.com>
This commit is contained in:
@@ -125,7 +125,7 @@ Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accè
|
|||||||
|
|
||||||
## 4. Authentification
|
## 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default function SignupPage() {
|
|||||||
.eq("id", user.id)
|
.eq("id", user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (profile && profile.pseudo !== "Nouveau membre") {
|
if (profile && !profile.pseudo.startsWith("Nouveau membre")) {
|
||||||
window.location.href = "/leaderboard";
|
window.location.href = "/leaderboard";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,14 @@ export default function SignupPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === "checking") {
|
if (status === "checking") {
|
||||||
return <div className="flex flex-1 items-center justify-center px-4 py-12" />;
|
return (
|
||||||
|
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||||
|
<div className="marble-surface w-full max-w-sm rounded-2xl border border-gold/40 p-6 text-center shadow-xl sm:p-8">
|
||||||
|
<AgoraLogo className="mx-auto mb-3 h-16 w-16 animate-pulse" />
|
||||||
|
<p className="text-sm text-text-marble/80">Connexion à ton invitation…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "no-invite") {
|
if (status === "no-invite") {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export async function updateSession(request: NextRequest) {
|
|||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path));
|
const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path));
|
||||||
const isAuthPath = AUTH_PATHS.some((path) => pathname.startsWith(path));
|
const isAuthPath = AUTH_PATHS.some((path) => pathname.startsWith(path));
|
||||||
|
const isSignup = pathname.startsWith("/signup");
|
||||||
|
|
||||||
if (!user && isProtected) {
|
if (!user && isProtected) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
@@ -54,6 +55,26 @@ export async function updateSession(request: NextRequest) {
|
|||||||
return NextResponse.redirect(url);
|
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) {
|
if (user && isAuthPath) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
url.pathname = "/leaderboard";
|
url.pathname = "/leaderboard";
|
||||||
|
|||||||
+9
-1
@@ -147,6 +147,11 @@ create trigger profiles_before_update
|
|||||||
-- Création automatique du profil à l'inscription (auth.users → profiles),
|
-- Création automatique du profil à l'inscription (auth.users → profiles),
|
||||||
-- indépendamment de l'état de la session (fonctionne même si l'email
|
-- 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).
|
-- 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()
|
create or replace function public.handle_new_user()
|
||||||
returns trigger
|
returns trigger
|
||||||
language plpgsql
|
language plpgsql
|
||||||
@@ -154,7 +159,10 @@ security definer set search_path = public
|
|||||||
as $$
|
as $$
|
||||||
begin
|
begin
|
||||||
insert into public.profiles (id, pseudo)
|
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;
|
return new;
|
||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|||||||
Reference in New Issue
Block a user