- Réorganiser la logique de sécurité pour distinguer staff/client - Staff: accepter employer_id fourni et vérifier existence de l'org - Client: utiliser l'org associée et bloquer toute tentative cross-org - Améliorer les logs pour identifier le type d'utilisateur - Corriger le retour no_organization inapproprié pour les staff
675 lines
25 KiB
TypeScript
675 lines
25 KiB
TypeScript
// (POST handler is defined later in this file)
|
|
export const dynamic = "force-dynamic";
|
|
import { NextResponse, NextRequest } from "next/server";
|
|
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
|
|
import { resolveActiveOrg } from "@/lib/resolveActiveOrg";
|
|
import { sendUniversalEmailV2 } from "@/lib/emailTemplateService";
|
|
import { generateAutoDeclarationToken } from "@/lib/autoDeclarationTokenService";
|
|
|
|
type SalarieRow = {
|
|
matricule: string;
|
|
nom: string;
|
|
email?: string | null;
|
|
transat_connecte?: boolean;
|
|
dernier_emploi?: string | null;
|
|
code_salarie?: string | null;
|
|
prenom?: string | null;
|
|
civilite?: string | null;
|
|
tel?: string | null;
|
|
org_id?: string | null;
|
|
org_name?: string | null;
|
|
};
|
|
|
|
type SalariesResponse = {
|
|
items: SalarieRow[];
|
|
page: number;
|
|
limit: number;
|
|
total?: number | null;
|
|
totalPages?: number;
|
|
hasMore: boolean;
|
|
};
|
|
|
|
async function resolveActiveOrgId(sb: any, _isStaff: boolean) {
|
|
return await resolveActiveOrg(sb);
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
// 🎭 Mode démo : retourner des données fictives
|
|
const isDemoMode = process.env.DEMO_MODE === 'true';
|
|
|
|
if (isDemoMode) {
|
|
console.log('🎭 [API /salaries] Mode démo - retour de données fictives');
|
|
|
|
const { searchParams } = new URL(req.url);
|
|
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1);
|
|
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "10", 10) || 10));
|
|
const q = (searchParams.get("search") || searchParams.get("q") || "").trim();
|
|
|
|
// Données fictives de salariés
|
|
const DEMO_SALARIES: SalarieRow[] = [
|
|
{
|
|
matricule: "demo-sal-001",
|
|
nom: "MARTIN Alice",
|
|
email: "alice.martin@demo.fr",
|
|
transat_connecte: true,
|
|
dernier_emploi: "Comédien",
|
|
code_salarie: "demo-sal-001",
|
|
prenom: "Alice",
|
|
civilite: "Mme",
|
|
tel: "06 12 34 56 78",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-002",
|
|
nom: "DUBOIS Pierre",
|
|
email: "pierre.dubois@demo.fr",
|
|
transat_connecte: false,
|
|
dernier_emploi: "Metteur en scène",
|
|
code_salarie: "demo-sal-002",
|
|
prenom: "Pierre",
|
|
civilite: "M.",
|
|
tel: "06 23 45 67 89",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-003",
|
|
nom: "LEROY Sophie",
|
|
email: "sophie.leroy@demo.fr",
|
|
transat_connecte: true,
|
|
dernier_emploi: "Danseur",
|
|
code_salarie: "demo-sal-003",
|
|
prenom: "Sophie",
|
|
civilite: "Mme",
|
|
tel: "06 34 56 78 90",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-004",
|
|
nom: "BERNARD Marc",
|
|
email: "marc.bernard@demo.fr",
|
|
transat_connecte: false,
|
|
dernier_emploi: "Technicien son",
|
|
code_salarie: "demo-sal-004",
|
|
prenom: "Marc",
|
|
civilite: "M.",
|
|
tel: "06 45 67 89 01",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-005",
|
|
nom: "GARCIA Elena",
|
|
email: "elena.garcia@demo.fr",
|
|
transat_connecte: true,
|
|
dernier_emploi: "Costumière",
|
|
code_salarie: "demo-sal-005",
|
|
prenom: "Elena",
|
|
civilite: "Mme",
|
|
tel: "06 56 78 90 12",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
];
|
|
|
|
// Filtrer par recherche si nécessaire
|
|
let filteredSalaries = DEMO_SALARIES;
|
|
if (q) {
|
|
const searchTerm = q.toLowerCase();
|
|
filteredSalaries = DEMO_SALARIES.filter(salarie =>
|
|
salarie.nom.toLowerCase().includes(searchTerm) ||
|
|
salarie.prenom?.toLowerCase().includes(searchTerm) ||
|
|
salarie.matricule.toLowerCase().includes(searchTerm) ||
|
|
salarie.email?.toLowerCase().includes(searchTerm) ||
|
|
salarie.dernier_emploi?.toLowerCase().includes(searchTerm)
|
|
);
|
|
}
|
|
|
|
// Pagination
|
|
const offset = (page - 1) * limit;
|
|
const paginatedSalaries = filteredSalaries.slice(offset, offset + limit);
|
|
const total = filteredSalaries.length;
|
|
const hasMore = offset + paginatedSalaries.length < total;
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
const payload: SalariesResponse = {
|
|
items: paginatedSalaries,
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages,
|
|
hasMore,
|
|
};
|
|
|
|
return NextResponse.json(payload);
|
|
}
|
|
|
|
const sb = createSbServer();
|
|
|
|
const {
|
|
data: { user },
|
|
error: authError,
|
|
} = await sb.auth.getUser();
|
|
if (authError || !user) {
|
|
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
}
|
|
|
|
// Determine if current user is staff
|
|
let isStaff = false;
|
|
try {
|
|
const { data } = await sb
|
|
.from("staff_users")
|
|
.select("is_staff")
|
|
.eq("user_id", user.id)
|
|
.maybeSingle();
|
|
isStaff = !!data?.is_staff;
|
|
} catch {}
|
|
|
|
const orgId = await resolveActiveOrgId(sb, isStaff);
|
|
|
|
const { searchParams } = new URL(req.url);
|
|
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1);
|
|
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "25", 10) || 25));
|
|
// Accept either `search` or `q` (some clients send `q`)
|
|
const q = (searchParams.get("search") || searchParams.get("q") || "").trim();
|
|
|
|
console.log("👥 API salaries search:", { q, page, limit, user_id: user.id });
|
|
|
|
const offset = (page - 1) * limit;
|
|
const to = offset + limit - 1;
|
|
|
|
// Build query
|
|
let query = sb
|
|
.from("salaries")
|
|
.select(
|
|
`id, code_salarie, num_salarie, salarie, nom, prenom, civilite, adresse_mail, compte_transat, derniere_profession, employer_id, infos_caisses_organismes, tel`,
|
|
{ count: "exact" }
|
|
);
|
|
|
|
// If an organization id was resolved, scope results to that employer.
|
|
if (orgId) {
|
|
query = query.eq("employer_id", orgId);
|
|
}
|
|
|
|
if (q) {
|
|
const like = `%${q}%`;
|
|
// Use OR with ilike across relevant columns
|
|
query = query.or(
|
|
[
|
|
`nom.ilike.${like}`,
|
|
`prenom.ilike.${like}`,
|
|
`salarie.ilike.${like}`,
|
|
`adresse_mail.ilike.${like}`,
|
|
`code_salarie.ilike.${like}`,
|
|
`num_salarie.ilike.${like}`,
|
|
].join(",")
|
|
);
|
|
}
|
|
|
|
// Order by last name then first name for stability
|
|
query = query.order("nom", { ascending: true }).order("prenom", { ascending: true, nullsFirst: true }).range(offset, to);
|
|
|
|
const { data, error, count } = await query;
|
|
console.log("👥 API salaries result:", {
|
|
data: data?.length,
|
|
error: error?.message,
|
|
count
|
|
});
|
|
if (error) {
|
|
console.error("💥 [API /salaries] Supabase error:", error.message);
|
|
return NextResponse.json({ error: "supabase_error", detail: error.message }, { status: 500 });
|
|
}
|
|
|
|
const items: SalarieRow[] = (data || []).map((r: any) => {
|
|
const fullName = r.salarie || [r.nom, r.prenom].filter(Boolean).join(" ").trim() || r.nom || "";
|
|
const comp = (r.compte_transat || "").toString().toLowerCase();
|
|
const transat = comp.includes("connect") && !comp.includes("non");
|
|
return {
|
|
matricule: r.code_salarie || (r.num_salarie ? String(r.num_salarie) : r.id),
|
|
nom: fullName,
|
|
email: r.adresse_mail ?? null,
|
|
transat_connecte: transat,
|
|
dernier_emploi: r.derniere_profession ?? null,
|
|
code_salarie: r.code_salarie ?? null,
|
|
prenom: r.prenom ?? null,
|
|
civilite: r.civilite ?? null,
|
|
tel: r.tel ?? null,
|
|
org_id: r.employer_id || null,
|
|
org_name: r.infos_caisses_organismes || null,
|
|
} as SalarieRow;
|
|
});
|
|
|
|
const total = count ?? null;
|
|
const hasMore = typeof total === "number" ? offset + items.length < total : items.length === limit;
|
|
const totalPages = typeof total === "number" ? Math.ceil(total / limit) : 0;
|
|
|
|
const payload: SalariesResponse = {
|
|
items,
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages,
|
|
hasMore,
|
|
};
|
|
|
|
return NextResponse.json(payload);
|
|
} catch (e: any) {
|
|
console.error("💥 [API /salaries] Unexpected error:", e?.message);
|
|
return NextResponse.json({ error: "server_error", message: e?.message || "unknown" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// Rate limiting map: userId -> { count, windowStart }
|
|
const rateLimitMap = new Map<string, { count: number; windowStart: number }>();
|
|
const RATE_LIMIT_MAX = 50; // 50 salariés par heure
|
|
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 heure en millisecondes
|
|
|
|
// POST: create a new salary record (used by the nouveau salarié form)
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
let body: any = {};
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
|
|
}
|
|
|
|
if (!body || !body.nom || !body.prenom || !body.email_salarie) {
|
|
return NextResponse.json({ ok: false, error: 'missing_required_fields' }, { status: 400 });
|
|
}
|
|
|
|
// 🔒 SÉCURITÉ : Validation du format email salarié
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(body.email_salarie)) {
|
|
console.error('❌ [SÉCURITÉ] Format d\'email salarié invalide:', body.email_salarie);
|
|
return NextResponse.json(
|
|
{
|
|
ok: false,
|
|
error: 'invalid_email',
|
|
message: 'Le format de l\'email du salarié est invalide'
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const supabase = createSbServiceRole();
|
|
|
|
// 🔒 SÉCURITÉ : Authentification et récupération de l'utilisateur
|
|
const sbAuth = createSbServer();
|
|
const { data: { user }, error: authErr } = await sbAuth.auth.getUser();
|
|
|
|
if (authErr || !user) {
|
|
console.error('❌ [SÉCURITÉ] Utilisateur non authentifié');
|
|
return NextResponse.json(
|
|
{ ok: false, error: 'unauthorized', message: 'Authentification requise' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
// 🔒 SÉCURITÉ : Vérifier si l'utilisateur est staff
|
|
let isStaff = false;
|
|
try {
|
|
const { data: staffRow } = await sbAuth.from('staff_users').select('is_staff').eq('user_id', user.id).maybeSingle();
|
|
isStaff = !!staffRow?.is_staff;
|
|
} catch {
|
|
const userMeta = user?.user_metadata || {};
|
|
const appMeta = user?.app_metadata || {};
|
|
isStaff = Boolean((userMeta.is_staff === true || userMeta.role === 'staff') || (Array.isArray(appMeta.roles) && appMeta.roles.includes('staff')));
|
|
}
|
|
|
|
console.log('🔍 [SÉCURITÉ] Vérification utilisateur:', {
|
|
email: user.email,
|
|
isStaff,
|
|
employer_id_fourni: body.employer_id
|
|
});
|
|
|
|
// 🔒 SÉCURITÉ : Rate limiting (50 créations par heure par utilisateur)
|
|
const now = Date.now();
|
|
const userRateLimit = rateLimitMap.get(user.id);
|
|
|
|
if (userRateLimit) {
|
|
// Vérifier si on est dans la même fenêtre temporelle
|
|
if (now - userRateLimit.windowStart < RATE_LIMIT_WINDOW) {
|
|
if (userRateLimit.count >= RATE_LIMIT_MAX) {
|
|
const remainingTime = Math.ceil((RATE_LIMIT_WINDOW - (now - userRateLimit.windowStart)) / 60000);
|
|
console.warn('⚠️ [RATE LIMIT] Limite atteinte pour utilisateur:', user.id);
|
|
return NextResponse.json(
|
|
{
|
|
ok: false,
|
|
error: 'rate_limit_exceeded',
|
|
message: `Limite de ${RATE_LIMIT_MAX} créations par heure atteinte. Réessayez dans ${remainingTime} minutes.`
|
|
},
|
|
{ status: 429 }
|
|
);
|
|
}
|
|
// Incrémenter le compteur
|
|
userRateLimit.count++;
|
|
} else {
|
|
// Nouvelle fenêtre temporelle
|
|
rateLimitMap.set(user.id, { count: 1, windowStart: now });
|
|
}
|
|
} else {
|
|
// Première création pour cet utilisateur
|
|
rateLimitMap.set(user.id, { count: 1, windowStart: now });
|
|
}
|
|
|
|
// Nettoyer les entrées expirées du rate limit map (éviter la fuite mémoire)
|
|
for (const [userId, data] of rateLimitMap.entries()) {
|
|
if (now - data.windowStart > RATE_LIMIT_WINDOW) {
|
|
rateLimitMap.delete(userId);
|
|
}
|
|
}
|
|
|
|
// 🔒 SÉCURITÉ : Logique différente pour staff et clients
|
|
let orgId: string | null = null;
|
|
|
|
if (isStaff) {
|
|
// Pour les utilisateurs staff : accepter l'employer_id fourni
|
|
orgId = body.employer_id || null;
|
|
|
|
if (!orgId) {
|
|
console.error('❌ [STAFF] employer_id manquant dans la requête');
|
|
return NextResponse.json(
|
|
{
|
|
ok: false,
|
|
error: 'no_organization',
|
|
message: 'Vous devez sélectionner une organisation'
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Vérifier que l'organisation existe
|
|
const { data: orgExists, error: orgError } = await sbAuth
|
|
.from('organizations')
|
|
.select('id')
|
|
.eq('id', orgId)
|
|
.maybeSingle();
|
|
|
|
if (orgError || !orgExists) {
|
|
console.error('❌ [STAFF] Organisation introuvable:', orgId);
|
|
return NextResponse.json(
|
|
{
|
|
ok: false,
|
|
error: 'invalid_organization',
|
|
message: 'Organisation introuvable'
|
|
},
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
console.log('✅ [STAFF] Organisation validée:', orgId);
|
|
} else {
|
|
// Pour les utilisateurs clients : utiliser leur organisation associée
|
|
const userOrgId = await resolveActiveOrg(sbAuth);
|
|
|
|
if (!userOrgId) {
|
|
console.error('❌ [CLIENT] Aucune organisation trouvée pour l\'utilisateur:', user.id);
|
|
return NextResponse.json(
|
|
{
|
|
ok: false,
|
|
error: 'no_organization',
|
|
message: 'Aucune organisation associée à votre compte'
|
|
},
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// 🔒 SÉCURITÉ CRITIQUE : Vérifier que l'employer_id fourni (si présent) correspond à l'organisation de l'utilisateur
|
|
if (body.employer_id && body.employer_id !== userOrgId) {
|
|
console.error('❌ [SÉCURITÉ CRITIQUE] Tentative de création salarié dans une autre organisation!');
|
|
console.error(' - employer_id fourni:', body.employer_id);
|
|
console.error(' - organisation utilisateur:', userOrgId);
|
|
console.error(' - utilisateur:', user.email);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
ok: false,
|
|
error: 'unauthorized_organization',
|
|
message: 'Vous ne pouvez pas créer un salarié dans cette organisation'
|
|
},
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// Utiliser l'organisation de l'utilisateur authentifié
|
|
orgId = userOrgId;
|
|
|
|
console.log('✅ [CLIENT] Organisation validée:', orgId);
|
|
}
|
|
|
|
console.log('✅ [SÉCURITÉ] Vérifications réussies');
|
|
console.log(' - Utilisateur:', user.email);
|
|
console.log(' - Type:', isStaff ? 'STAFF' : 'CLIENT');
|
|
console.log(' - Organisation:', orgId);
|
|
console.log(' - Email salarié validé:', body.email_salarie);
|
|
console.log(' - Rate limit:', `${rateLimitMap.get(user.id)?.count || 0}/${RATE_LIMIT_MAX}`)
|
|
|
|
// Compute next matricule/code_salarie for this org if possible
|
|
let computedCode: string | null = null;
|
|
let computedNum: number | null = null;
|
|
if (orgId) {
|
|
try {
|
|
// Récupérer le code employeur depuis organization_details
|
|
const { data: orgDetailsData, error: orgDetailsError } = await supabase
|
|
.from('organization_details')
|
|
.select('code_employeur')
|
|
.eq('org_id', orgId)
|
|
.single();
|
|
|
|
console.log('🔍 [MATRICULE] Requête organization_details:', {
|
|
orgId,
|
|
orgDetailsData,
|
|
orgDetailsError,
|
|
hasCodeEmployeur: !!orgDetailsData?.code_employeur
|
|
});
|
|
|
|
const codeEmployeur = orgDetailsData?.code_employeur || '';
|
|
console.log('🔍 [MATRICULE] code_employeur final:', codeEmployeur);
|
|
|
|
const { data: rows, error: qerr } = await supabase
|
|
.from('salaries')
|
|
.select('code_salarie')
|
|
.eq('employer_id', orgId)
|
|
.not('code_salarie', 'is', null)
|
|
.limit(1000);
|
|
|
|
if (!qerr && Array.isArray(rows)) {
|
|
console.log('🔍 [MATRICULE] Nombre de salariés existants:', rows.length);
|
|
let maxNum = -Infinity;
|
|
let detectedPrefix = '';
|
|
let maxPad = 0;
|
|
|
|
for (const r of rows) {
|
|
const s = (r?.code_salarie || '').toString();
|
|
const m = s.match(/(\d+)$/);
|
|
if (m) {
|
|
const numStr = m[1];
|
|
const num = parseInt(numStr, 10);
|
|
if (!Number.isNaN(num) && num > maxNum) {
|
|
maxNum = num;
|
|
detectedPrefix = s.slice(0, s.length - numStr.length);
|
|
maxPad = numStr.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (maxNum !== -Infinity) {
|
|
// On a trouvé des matricules existants
|
|
const nextNum = maxNum + 1;
|
|
const padded = String(nextNum).padStart(Math.max(maxPad, 3), '0');
|
|
|
|
// Utiliser le code employeur comme préfixe, même si les anciens n'en avaient pas
|
|
const prefix = codeEmployeur || detectedPrefix;
|
|
computedCode = `${prefix}${padded}`;
|
|
computedNum = nextNum;
|
|
console.log('✅ [MATRICULE] Incrémentation:', computedCode, '(prefix utilisé:', prefix, 'prefix détecté:', detectedPrefix, ')');
|
|
} else {
|
|
// No existing numeric matricules found: use code_employeur as prefix
|
|
computedNum = 1;
|
|
computedCode = codeEmployeur ? `${codeEmployeur}001` : `001`;
|
|
console.log('✅ [MATRICULE] Premier salarié, code généré:', computedCode, '(codeEmployeur:', codeEmployeur, ')');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('❌ [MATRICULE] Erreur génération:', e);
|
|
// ignore computation errors and continue without code
|
|
}
|
|
}
|
|
|
|
const fullName = [body.nom, body.prenom].filter(Boolean).join(" ").trim();
|
|
|
|
// Only include columns that exist in the `salaries` table to avoid PGRST204
|
|
const payload: any = {
|
|
salarie: fullName || null,
|
|
nom: body.nom || null,
|
|
prenom: body.prenom || null,
|
|
nom_de_naissance: body.nomnaissance_salarie || body.nom_naissance || null,
|
|
// pseudonyme fallback to 'n/a' when empty
|
|
pseudonyme: (body.pseudonyme || body.pseudo || "").toString().trim() || "n/a",
|
|
civilite: body.civilite_salarie || body.civilite || null,
|
|
// map address text into the `adresse` column (table has `adresse`)
|
|
adresse: body.adresse_salarie || body.adresse || null,
|
|
// keep primary contact fields that exist in the table
|
|
adresse_mail: body.email_salarie || body.email || null,
|
|
tel: body.tel_salarie || body.telephone || null,
|
|
date_naissance: body.dob_salarie || body.dateNaissance || null,
|
|
lieu_de_naissance: body.cob_salarie || body.lieuNaissance || null,
|
|
nir: body.ss_salarie || body.nir || null,
|
|
conges_spectacles: body.cs_salarie || body.conges_spectacles || null,
|
|
iban: body.iban_salarie || body.iban || null,
|
|
bic: body.bic_salarie || body.bic || null,
|
|
// enforce requested defaults
|
|
compte_transat: "En cours",
|
|
topaze: "NC",
|
|
justificatifs_personnels: body.justificatifs_personnels || "En attente de réception",
|
|
rf_au_sens_fiscal: true,
|
|
intermittent_mineur_16: false,
|
|
abattement_2024: body.abattement_2024 || "Sans objet",
|
|
notif_nouveau_salarie: "Fait",
|
|
notif_employeur: "Fait",
|
|
infos_caisses_organismes: body.structure || null,
|
|
employer_id: orgId || body.employer_id || null,
|
|
// computed matricule fields
|
|
code_salarie: computedCode,
|
|
num_salarie: computedNum,
|
|
};
|
|
|
|
const { data, error } = await supabase.from('salaries').insert([payload]).select().single();
|
|
if (error) {
|
|
console.error('❌ [API /salaries POST] supabase error', error);
|
|
return NextResponse.json({ ok: false, error: 'db_error', detail: error }, { status: 500 });
|
|
}
|
|
|
|
// Envoyer les e-mails de notification après la création réussie
|
|
try {
|
|
const sbAuth = createSbServer();
|
|
const { data: { user } } = await sbAuth.auth.getUser();
|
|
|
|
// Récupérer les infos de l'organisation depuis les deux tables
|
|
const orgData = orgId ? await supabase.from('organizations').select('name').eq('id', orgId).single() : { data: null };
|
|
const orgDetailsResult = orgId ? await supabase.from('organization_details').select('code_employeur, prenom_contact, email_notifs, email_notifs_cc').eq('org_id', orgId).single() : { data: null, error: null };
|
|
|
|
console.log('🔍 [EMAIL] Données récupérées:', {
|
|
orgId,
|
|
orgData: orgData?.data,
|
|
orgDetailsData: 'data' in orgDetailsResult ? orgDetailsResult.data : null,
|
|
orgDetailsError: 'error' in orgDetailsResult ? orgDetailsResult.error : null
|
|
});
|
|
|
|
const orgDetails = {
|
|
data: orgData?.data ? {
|
|
name: orgData.data.name,
|
|
code_employeur: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.code_employeur || null : null,
|
|
prenom_contact: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.prenom_contact || null : null,
|
|
email_notifs: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.email_notifs || null : null,
|
|
email_notifs_cc: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.email_notifs_cc || null : null
|
|
} : null
|
|
};
|
|
|
|
console.log('🔍 [EMAIL] orgDetails final:', orgDetails);
|
|
|
|
// 1. Email de notification à l'employeur (corrigé: utilise email_notifs au lieu de user.email)
|
|
if (orgDetails?.data?.email_notifs) {
|
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
|
|
|
// 🔒 SÉCURITÉ : Nettoyer et valider l'email principal
|
|
const cleanAndValidateEmail = (email: string | null, fallback: string = 'paie@odentas.fr'): string => {
|
|
if (!email) return fallback;
|
|
|
|
const trimmed = email.trim().toLowerCase();
|
|
|
|
// Valider le format email
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(trimmed)) {
|
|
console.warn('⚠️ [SÉCURITÉ] Email invalide détecté, utilisation du fallback:', trimmed);
|
|
return fallback;
|
|
}
|
|
|
|
return trimmed;
|
|
};
|
|
|
|
const emailNotifs = cleanAndValidateEmail(orgDetails.data.email_notifs);
|
|
const emailNotifsCC = orgDetails.data.email_notifs_cc
|
|
? cleanAndValidateEmail(orgDetails.data.email_notifs_cc, '')
|
|
: null;
|
|
|
|
console.log('📧 [EMAIL] Envoi à email_notifs:', emailNotifs);
|
|
console.log('📧 [EMAIL] CC à email_notifs_cc:', emailNotifsCC || 'Aucun');
|
|
|
|
if (emailNotifs) {
|
|
await sendUniversalEmailV2({
|
|
type: 'employee-created',
|
|
toEmail: emailNotifs,
|
|
ccEmail: emailNotifsCC || undefined,
|
|
data: {
|
|
userName: orgDetails.data.prenom_contact || user?.user_metadata?.first_name || user?.user_metadata?.display_name?.split(' ')[0] || 'Cher client',
|
|
companyName: orgDetails.data.name,
|
|
employerCode: orgDetails.data.code_employeur || 'N/A',
|
|
handlerName: 'Renaud BREVIERE-ABRAHAM',
|
|
employeeName: data.salarie,
|
|
email: data.adresse_mail,
|
|
matricule: data.code_salarie,
|
|
ctaUrl: `${baseUrl}/salaries/${data.id}`,
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
console.warn('⚠️ [EMAIL] email_notifs non défini pour l\'organisation', orgId);
|
|
}
|
|
|
|
// 2. Générer token et envoyer invitation au salarié (nouveau)
|
|
if (data.adresse_mail) {
|
|
try {
|
|
const result = await generateAutoDeclarationToken({
|
|
salarie_id: data.id,
|
|
send_email: true
|
|
});
|
|
|
|
if (result.success) {
|
|
console.log('✅ [API /salaries POST] Token généré et invitation envoyée au salarié');
|
|
} else {
|
|
console.error('❌ [API /salaries POST] Erreur génération token:', result.error);
|
|
}
|
|
} catch (tokenError) {
|
|
console.error('❌ [API /salaries POST] Erreur lors de la génération du token:', tokenError);
|
|
}
|
|
} else {
|
|
console.warn('⚠️ [API /salaries POST] Pas d\'email pour le salarié, invitation non envoyée');
|
|
}
|
|
|
|
} catch (emailError) {
|
|
console.error('📧 [API /salaries POST] Failed to send email notifications:', emailError);
|
|
// Ne pas bloquer la réponse en cas d'échec des e-mails
|
|
}
|
|
|
|
return NextResponse.json({ ok: true, data }, { status: 201 });
|
|
} catch (e: any) {
|
|
console.error('💥 [API /salaries POST] unexpected', e?.message || e);
|
|
return NextResponse.json({ ok: false, error: 'server_exception', detail: e?.message || String(e) }, { status: 500 });
|
|
}
|
|
}
|
|
|