espace-paie-odentas/app/api/access/nouveau/route.ts
odentas e9cb6e7e0e feat: Système unifié d'invitation avec emails d'activation
- Créé sendInvitationWithActivationEmail() pour unifier les invitations
- Modifié /api/staff/users/invite pour utiliser generateLink + email
- Modifié /api/access/nouveau pour envoyer email d'activation
- Modifié /api/access POST pour remplacer pending_invites par système direct
- Template account-activation mis à jour :
  * Titre 'Activez votre compte'
  * Encart avec infos : invitant (statut), organisation, niveau d'accès
  * Message de contact formaté comme autres emails
  * Renommage 'Odentas Paie' → 'Espace Paie Odentas'
- Fix page /activate : délai 100ms pour hash fragment + redirection 1s
- Liens d'activation forcés vers paie.odentas.fr (tests depuis localhost)
- Messages UI cohérents : 'Invitation envoyée' au lieu de 'Compte créé'
2025-11-14 17:41:46 +01:00

213 lines
No EOL
7.6 KiB
TypeScript

import { NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";
import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { sendInvitationWithActivationEmail } from "@/lib/emailMigrationHelpers";
// Types de rôles autorisés (alignés avec public.role)
const ROLES = ["SUPER_ADMIN","ADMIN","AGENT","COMPTA"] as const;
type Role = typeof ROLES[number];
export async function POST(req: Request) {
console.log("🚀 [API] /api/access/nouveau - Début de la requête");
try {
const body = await req.json();
console.log("📝 [API] Body reçu:", {
email: body.email,
firstName: body.firstName,
orgId: body.orgId,
role: body.role
});
const email = String(body.email || "").trim().toLowerCase();
const firstName = String(body.firstName || "").trim();
const requestedOrgId = String(body.orgId || "").trim();
const requestedRole = String(body.role || "").trim().toUpperCase() as Role;
if (!email || !requestedRole || !ROLES.includes(requestedRole)) {
console.log("❌ [API] Validation échouée - données invalides");
return NextResponse.json({ error: "invalid_input" }, { status: 400 });
}
// Client Supabase pour l'authentification
const sb = createRouteHandlerClient({ cookies });
const { data: { user } } = await sb.auth.getUser();
if (!user) {
console.log("❌ [API] Utilisateur non authentifié");
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}
console.log("✅ [API] Utilisateur authentifié:", user.email);
// Vérifier les permissions (simplifié pour le debug)
let targetOrgId = requestedOrgId;
// Si pas d'orgId fourni, essayer de récupérer via organization_members
if (!targetOrgId) {
// Récupérer l'organisation via requête directe
const { data: memberData } = await sb
.from("organization_members")
.select(`
organization:organizations(id, name, structure_api)
`)
.eq("user_id", user.id)
.eq("revoked", false)
.maybeSingle();
if (memberData?.organization) {
const org = memberData.organization as any;
targetOrgId = org.id;
}
}
// If still not found, and user is staff, try staff_settings.active_org_id
if (!targetOrgId) {
try {
const { data: staffRow } = await sb.from('staff_users').select('is_staff').eq('user_id', user.id).maybeSingle();
const isStaff = !!staffRow?.is_staff;
if (isStaff) {
const { data: ss } = await sb.from('staff_settings').select('active_org_id').eq('user_id', user.id).maybeSingle();
if (ss?.active_org_id) targetOrgId = ss.active_org_id;
}
} catch {}
}
if (!targetOrgId) {
console.log("❌ [API] Aucune organisation trouvée");
return NextResponse.json({ error: "org_missing" }, { status: 400 });
}
// Vérifier que l'organisation existe
const { data: org } = await sb
.from("organizations")
.select("id,name,structure_api")
.eq("id", targetOrgId)
.maybeSingle();
if (!org) {
console.log("❌ [API] Organisation non trouvée");
return NextResponse.json({ error: "org_not_found" }, { status: 404 });
}
console.log("🏛️ [API] Organisation trouvée:", org.name);
// Client admin Supabase (service role)
const admin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ 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é...");
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,
},
}
});
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 });
}
// 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 });
if (memberErr) {
console.log("❌ [API] Erreur lors de la création de l'adhésion:", memberErr);
return NextResponse.json({ error: "attach_failed", message: memberErr.message }, { status: 400 });
}
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...");
// Récupérer le rôle du créateur dans l'organisation (si disponible)
let inviterStatus: string | undefined = undefined;
try {
const { data: inviterMember } = await sb
.from('organization_members')
.select('role')
.eq('org_id', org.id)
.eq('user_id', user.id)
.maybeSingle();
inviterStatus = (inviterMember as any)?.role || undefined;
} catch {}
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,
role: requestedRole,
inviterName,
inviterStatus,
});
// Configurer les préférences d'authentification
await admin
.from("user_auth_prefs")
.upsert({ user_id: newUserId, allow_magic_link: true, allow_password: false });
const responsePayload = {
ok: true,
user_id: newUserId,
email,
org: { id: org.id, name: org.name, structure_api: org.structure_api },
role: requestedRole,
};
console.log("🎉 [API] Succès - Réponse:", responsePayload);
return NextResponse.json(responsePayload);
} catch (e: any) {
console.error("💥 [API] Erreur critique:", {
message: e?.message,
stack: e?.stack?.split('\n')[0]
});
return NextResponse.json({ error: "server_error", message: e?.message || "Internal Server Error" }, { status: 500 });
}
}