fix: vérifier les comptes clients créés par le staff

This commit is contained in:
odentas 2025-12-09 14:01:52 +01:00
parent d1957400bc
commit 2a08a69efd
4 changed files with 112 additions and 125 deletions

View file

@ -113,57 +113,49 @@ export async function POST(req: Request) {
{ auth: { autoRefreshToken: false, persistSession: false } }
);
// Générer un lien d'invitation personnalisé (sans envoi automatique)
console.log("🔗 [API] Génération du lien d'invitation personnalisé...");
console.log("🔗 [API] Création utilisateur vérifié...");
const origin = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
console.log("🌍 [API] Configuration:", {
origin,
redirectTo: `${origin}/activate`,
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
email
});
const { data: linkData, error: linkError } = await admin.auth.admin.generateLink({
type: "invite",
email,
options: {
redirectTo: `${origin}/activate`,
data: {
first_name: firstName || null,
role: requestedRole,
org_id: org.id,
org_name: org.name,
structure_api: org.structure_api ?? null,
},
const signinUrl = `${origin}/signin`;
const metadata = {
first_name: firstName || null,
role: requestedRole,
org_id: org.id,
org_name: org.name,
structure_api: org.structure_api ?? null,
};
// Vérifier si utilisateur déjà existant
const { data: existingUser } = await admin.auth.admin.getUserByEmail(email);
let newUserId: string | undefined = existingUser?.user?.id;
if (newUserId) {
await admin.auth.admin.updateUserById(newUserId, {
email_confirm: true,
user_metadata: { ...(existingUser.user?.user_metadata || {}), ...metadata },
});
} else {
const { data: createData, error: createError } = await admin.auth.admin.createUser({
email,
email_confirm: true,
user_metadata: metadata,
});
if (createError || !createData?.user?.id) {
console.log("❌ [API] Erreur création utilisateur:", createError);
return NextResponse.json({ error: "create_failed", message: createError?.message }, { status: 400 });
}
});
if (linkError) {
console.log("❌ [API] Erreur lors de la génération du lien:", linkError);
return NextResponse.json({ error: "link_generation_failed", message: linkError.message }, { status: 400 });
}
const actionLink = linkData?.properties?.action_link;
const newUserId = linkData?.user?.id;
console.log("✅ [API] Lien généré avec succès:", {
userId: newUserId,
email: linkData?.user?.email,
linkExists: !!actionLink,
linkStart: actionLink ? actionLink.substring(0, 50) + "..." : null
});
if (!newUserId || !actionLink) {
console.log("❌ [API] Données manquantes dans la réponse generateLink");
return NextResponse.json({ error: "invite_failed" }, { status: 400 });
newUserId = createData.user.id;
}
console.log("✅ [API] Utilisateur prêt:", { userId: newUserId, email });
// Attacher l'adhésion à l'organisation
console.log("👥 [API] Création de l'adhésion à l'organisation...");
const { error: memberErr } = await admin
.from("organization_members")
.insert({ org_id: org.id, user_id: newUserId, role: requestedRole, revoked: false });
.upsert({ org_id: org.id, user_id: newUserId, role: requestedRole, revoked: false }, { onConflict: "org_id,user_id" });
if (memberErr) {
console.log("❌ [API] Erreur lors de la création de l'adhésion:", memberErr);
@ -173,7 +165,7 @@ export async function POST(req: Request) {
console.log("✅ [API] Adhésion créée avec succès");
// Envoyer l'email via le système universel
console.log("📧 [API] Envoi de l'email d'activation via le système universel...");
console.log("📧 [API] Envoi de l'email de confirmation via le système universel...");
// Récupérer le rôle du créateur dans l'organisation (si disponible)
let inviterStatus: string | undefined = undefined;
try {
@ -188,13 +180,10 @@ export async function POST(req: Request) {
const inviterName = (user.user_metadata as any)?.first_name || user.email || 'Administrateur';
// Forcer le domaine production dans le lien d'activation pour les tests depuis localhost
const productionLink = actionLink.replace(/http:\/\/localhost:\d+/, 'https://paie.odentas.fr');
await sendInvitationWithActivationEmail(email, {
firstName,
organizationName: org.name,
activationUrl: productionLink,
activationUrl: signinUrl,
role: requestedRole,
inviterName,
inviterStatus,

View file

@ -423,42 +423,42 @@ export async function POST(req: Request) {
let user_id: string | undefined = existing?.id;
if (!user_id) {
console.log("👤 Utilisateur non trouvé, création d'une invitation avec envoi d'email...");
// Générer un lien d'invitation et envoyer l'email
console.log("👤 Utilisateur non trouvé, création directe du compte vérifié...");
const origin = process.env.NEXT_PUBLIC_BASE_URL || "https://paie.odentas.fr";
const { data: linkData, error: linkError } = await admin.auth.admin.generateLink({
type: "invite",
const signinUrl = `${origin}/signin`;
const { data: orgData } = await admin
.from('organizations')
.select('name')
.eq('id', org_id)
.maybeSingle();
const orgName = (orgData as any)?.name || '';
const metadata = {
first_name: first_name || null,
role,
org_id,
org_name: orgName,
};
const { data: createData, error: createError } = await admin.auth.admin.createUser({
email,
options: {
redirectTo: `${origin}/activate`,
data: {
first_name: first_name || null,
role,
org_id,
org_name: org_id, // Sera enrichi après
},
},
email_confirm: true,
user_metadata: metadata,
});
if (linkError) {
console.log("❌ Erreur génération lien:", linkError.message);
return json(400, { error: linkError.message });
if (createError || !createData?.user?.id) {
console.log("❌ Erreur création utilisateur:", createError?.message);
return json(400, { error: createError?.message || "create_failed" });
}
const actionLink = linkData?.properties?.action_link;
const newUserId = linkData?.user?.id;
if (!newUserId || !actionLink) {
console.log("❌ Erreur: lien ou user_id manquant");
return json(400, { error: "invite_failed" });
}
const newUserId = createData.user.id;
// Attacher à l'organisation
const { error: memberError } = await admin
.from("organization_members")
.insert({ org_id, user_id: newUserId, role, revoked: false });
.upsert({ org_id, user_id: newUserId, role, revoked: false }, { onConflict: "org_id,user_id" });
if (memberError) {
console.log("❌ Erreur création membership:", memberError.message);
@ -470,27 +470,20 @@ export async function POST(req: Request) {
.from("user_auth_prefs")
.upsert({ user_id: newUserId, allow_magic_link: true, allow_password: false });
// Récupérer le nom de l'organisation
const { data: orgData } = await admin
.from('organizations')
.select('name')
.eq('id', org_id)
.maybeSingle();
// Envoyer l'email d'activation
// Envoyer l'email de confirmation
try {
await sendInvitationWithActivationEmail(email, {
firstName: first_name || undefined,
organizationName: (orgData as any)?.name || '',
activationUrl: actionLink,
organizationName: orgName,
activationUrl: signinUrl,
role,
});
} catch (emailErr) {
console.warn('⚠️ Envoi email invitation échoué (non-bloquant):', (emailErr as any)?.message || emailErr);
console.warn('⚠️ Envoi email confirmation échoué (non-bloquant):', (emailErr as any)?.message || emailErr);
}
console.log("✅ Invitation créée et email envoyé avec succès");
return json(200, { ok: true, message: "Invitation envoyée" });
console.log("✅ Compte créé et email envoyé avec succès");
return json(200, { ok: true, message: "Compte créé" });
} else {
console.log("👤 Utilisateur existant trouvé, ajout/mise à jour membership...");
// 2) Upsert membership si user déjà existant

View file

@ -45,36 +45,44 @@ export async function POST(req: Request) {
);
const origin = process.env.NEXT_PUBLIC_BASE_URL || "https://paie.odentas.fr";
const { data: linkData, error: linkError } = await admin.auth.admin.generateLink({
type: "invite",
email,
options: {
redirectTo: `${origin}/activate`,
data: {
first_name: firstName || null,
role,
org_id: org.id,
org_name: org.name,
structure_api: org.structure_api ?? null,
},
const signinUrl = `${origin}/signin`;
// Metadata partagée côté auth
const metadata = {
first_name: firstName || null,
role,
org_id: org.id,
org_name: org.name,
structure_api: org.structure_api ?? null,
};
// Vérifier si l'utilisateur existe déjà
const { data: existingUser } = await admin.auth.admin.getUserByEmail(email);
let newUserId: string | undefined = existingUser?.user?.id;
if (newUserId) {
// Mettre à jour les métadonnées et marquer comme vérifié
await admin.auth.admin.updateUserById(newUserId, {
email_confirm: true,
user_metadata: { ...(existingUser.user?.user_metadata || {}), ...metadata },
});
} else {
const { data: createData, error: createError } = await admin.auth.admin.createUser({
email,
email_confirm: true,
user_metadata: metadata,
});
if (createError || !createData?.user?.id) {
return NextResponse.json({ error: createError?.message || "create_failed" }, { status: 400 });
}
});
if (linkError) {
return NextResponse.json({ error: "link_generation_failed", message: linkError.message }, { status: 400 });
}
const actionLink = linkData?.properties?.action_link;
const newUserId = linkData?.user?.id;
if (!newUserId || !actionLink) {
return NextResponse.json({ error: "invite_failed" }, { status: 400 });
newUserId = createData.user.id;
}
const { error: memberErr } = await admin
.from("organization_members")
.insert({ org_id: orgId, user_id: newUserId, role });
.upsert({ org_id: orgId, user_id: newUserId, role }, { onConflict: "org_id,user_id" });
if (memberErr) {
return NextResponse.json({ error: memberErr.message }, { status: 400 });
}
@ -86,14 +94,11 @@ export async function POST(req: Request) {
try {
const inviterName = (user.user_metadata as any)?.first_name || user.email || 'Administrateur';
const inviterStatus = staff?.is_staff ? 'Staff' : 'Utilisateur';
// Forcer le domaine production dans le lien d'activation pour les tests depuis localhost
const productionLink = actionLink.replace(/http:\/\/localhost:\d+/, 'https://paie.odentas.fr');
await sendInvitationWithActivationEmail(email, {
firstName: firstName || undefined,
organizationName: org.name,
activationUrl: productionLink,
activationUrl: signinUrl,
role,
inviterName,
inviterStatus,

View file

@ -401,14 +401,14 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
},
'account-activation': {
subject: 'Activez votre compte {{organizationName}}',
title: 'Activez votre compte',
subject: 'Votre accès est prêt {{organizationName}}',
title: 'Votre compte est prêt',
greeting: '{{#if firstName}}Bonjour {{firstName}},{{/if}}',
mainMessage: 'Bienvenue ! Vous avez été invité à rejoindre {{organizationName}} sur l\'Espace Paie Odentas.<br><br><span style="color: #64748B; font-size: 14px;">Après validation, vous serez redirigé vers votre compte Odentas. Vous pourrez, si vous le souhaitez, créer un mot de passe depuis la page Sécurité. Si vous ne créez pas de mot de passe, la connexion se fera avec l\'envoi d\'un code par e-mail.</span>',
mainMessage: 'Bienvenue ! Votre accès à {{organizationName}} sur l\'Espace Paie Odentas est prêt. Vous pouvez vous connecter dès maintenant pour accéder à vos dossiers.<br><br><span style="color: #64748B; font-size: 14px;">Depuis la page de connexion, vous pouvez vous authentifier par code e-mail ou créer un mot de passe dans la section Sécurité.</span>',
closingMessage: 'Toute l\'équipe Odentas reste à votre disposition si vous avez des questions. <a href="mailto:paie@odentas.fr" style="color:#0B5FFF; text-decoration:none;">Contactez-nous</a> ou répondez à cet e-mail.<br><br><span style="color: #64748B; font-size: 13px;">Par mesure de sécurité, nous vous invitons à ne pas transférer cet e-mail.</span>',
ctaText: 'Activer mon compte',
ctaText: 'Se connecter',
footerText: 'Vous recevez cet e-mail car un accès vous a été créé sur l\'Espace Paie Odentas, par un administrateur de l\'organisation {{organizationName}}, qui est cliente de Odentas. Si vous estimez avoir reçu cet e-mail par erreur, contactez-nous en répondant à cet e-mail.',
preheaderText: 'Activation de votre compte · {{organizationName}} · Activez maintenant',
preheaderText: 'Connexion à votre compte · {{organizationName}} · Accès prêt',
colors: {
headerColor: STANDARD_COLORS.HEADER,
titleColor: '#0F172A',