espace-paie-odentas/app/api/staff/users/invite/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

115 lines
3.9 KiB
TypeScript

import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { createClient } from "@supabase/supabase-js";
import { sendInvitationWithActivationEmail } from "@/lib/emailMigrationHelpers";
const ROLES = ["SUPER_ADMIN","ADMIN","AGENT","COMPTA"] as const;
type Role = typeof ROLES[number];
export async function POST(req: Request) {
try {
const sb = createRouteHandlerClient({ cookies });
const { data: { user } } = await sb.auth.getUser();
if (!user) return new NextResponse("Unauthorized", { status: 401 });
const { data: staff } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
if (!staff?.is_staff) return new NextResponse("Forbidden", { status: 403 });
const body = await req.json();
const email = String(body.email || "").trim().toLowerCase();
const firstName = String(body.firstName || "").trim();
const orgId = String(body.orgId || "").trim();
const role = String(body.role || "").trim().toUpperCase() as Role;
if (!email || !orgId || !ROLES.includes(role)) {
return NextResponse.json({ error: "invalid_input" }, { status: 400 });
}
const { data: org } = await sb
.from("organizations")
.select("id,name,structure_api")
.eq("id", orgId)
.maybeSingle();
if (!org) return NextResponse.json({ error: "org_not_found" }, { status: 404 });
const admin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { autoRefreshToken: false, persistSession: false } }
);
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,
},
}
});
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 });
}
const { error: memberErr } = await admin
.from("organization_members")
.insert({ org_id: orgId, user_id: newUserId, role });
if (memberErr) {
return NextResponse.json({ error: memberErr.message }, { status: 400 });
}
await admin
.from("user_auth_prefs")
.upsert({ user_id: newUserId, allow_magic_link: true, allow_password: false });
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,
role,
inviterName,
inviterStatus,
});
} catch (emailErr) {
console.warn('⚠️ Envoi email invitation échoué (non-bloquant):', (emailErr as any)?.message || emailErr);
}
return NextResponse.json({
ok: true,
user_id: newUserId,
email,
org: { id: org.id, name: org.name },
role,
});
} catch (e: any) {
return new NextResponse(e?.message || "Internal Server Error", { status: 500 });
}
}