- 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éé'
213 lines
No EOL
7.6 KiB
TypeScript
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 });
|
|
}
|
|
} |