espace-paie-odentas/app/api/contrats/[id]/payslip-urls/route.ts

169 lines
No EOL
5.6 KiB
TypeScript

// app/api/contrats/[id]/payslip-urls/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
import { createClient } from "@supabase/supabase-js";
import { getS3SignedUrlIfExists } from "@/lib/aws-s3";
/** Résout l'organisation active 100% server-side */
async function resolveOrganization(supabase: any, session: any) {
const userId = session?.user?.id;
if (!userId) throw new Error("Session invalide");
// Vérifier si c'est un utilisateur staff via la table staff_users
let isStaff = false;
try {
const { data: staffRow } = await supabase.from('staff_users').select('is_staff').eq('user_id', userId).maybeSingle();
isStaff = !!staffRow?.is_staff;
} catch (e) {
// Fallback sur metadata de session si la requête échoue
const userMeta = session?.user?.user_metadata || {};
const appMeta = session?.user?.app_metadata || {};
isStaff = userMeta.is_staff === true || userMeta.role === 'staff' || (Array.isArray(appMeta?.roles) && appMeta.roles.includes('staff'));
}
if (isStaff) {
// Staff : accès global, retourne un objet avec isStaff = true
return { id: null, name: "Staff Access", isStaff: true } as const;
}
// Utilisateur client : récupérer son org via organization_members
const { data: member, error: mErr } = await supabase
.from("organization_members")
.select("org_id")
.eq("user_id", userId)
.single();
if (mErr || !member?.org_id) {
throw new Error("Aucune organisation associée à l'utilisateur");
}
return { id: member.org_id, name: "Client Org", isStaff: false } as const;
}
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const sb = createSbServer();
// Vérification de l'authentification
const { data: { user }, error: authError } = await sb.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ error: "Non autorisé" },
{ status: 401 }
);
}
// ✅ SÉCURITÉ : Résoudre l'organisation de l'utilisateur
const { data: { session } } = await sb.auth.getSession();
if (!session) {
return NextResponse.json(
{ error: "Session invalide" },
{ status: 401 }
);
}
const org = await resolveOrganization(sb, session);
// ✅ SÉCURITÉ : Vérifier que le contrat existe ET appartient à l'organisation (pour les clients)
let contractQuery = sb.from("cddu_contracts").select("id").eq("id", params.id);
// Si l'utilisateur est un client (non-staff), filtrer par org_id
if (!org.isStaff && org.id) {
contractQuery = contractQuery.eq("org_id", org.id);
}
const { data: contract, error: contractError } = await contractQuery.single();
if (contractError || !contract) {
return NextResponse.json(
{ error: "Contrat introuvable ou accès refusé" },
{ status: 404 }
);
}
// ✅ SÉCURITÉ : Récupération des fiches de paie avec filtrage par organisation
// Pour les staff : utiliser le service-role pour bypass RLS
// Pour les clients : RLS va automatiquement filtrer par organization_id
let payslipsQuery;
if (org.isStaff) {
// Staff : utiliser le service-role pour accès global
const admin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || "",
process.env.SUPABASE_SERVICE_ROLE_KEY || ""
);
payslipsQuery = admin
.from("payslips")
.select("*")
.eq("contract_id", params.id)
.order("pay_number", { ascending: true });
} else {
// Client : RLS filtre automatiquement par organization_id
// Mais on ajoute quand même une vérification explicite par sécurité
payslipsQuery = sb
.from("payslips")
.select("*")
.eq("contract_id", params.id)
.order("pay_number", { ascending: true });
// Filtrage explicite par organization_id si disponible
if (org.id) {
payslipsQuery = payslipsQuery.eq("organization_id", org.id);
}
}
const { data: payslips, error: payslipsError } = await payslipsQuery;
if (payslipsError) {
console.error("Erreur récupération payslips:", payslipsError);
return NextResponse.json(
{ error: "Erreur lors de la récupération des fiches de paie" },
{ status: 500 }
);
}
// Générer les URLs signées pour chaque fiche de paie qui a un storage_path
const payslipUrls = [];
for (const payslip of payslips || []) {
let signedUrl = null;
if (payslip.storage_path) {
try {
signedUrl = await getS3SignedUrlIfExists(payslip.storage_path, 3600);
} catch (error) {
console.error(`Erreur URL signée pour payslip ${payslip.id}:`, error);
}
}
payslipUrls.push({
id: payslip.id,
pay_number: payslip.pay_number,
period_start: payslip.period_start,
period_end: payslip.period_end,
pay_date: payslip.pay_date,
gross_amount: payslip.gross_amount,
net_amount: payslip.net_amount,
net_after_withholding: payslip.net_after_withholding,
processed: payslip.processed,
aem_status: payslip.aem_status,
signedUrl,
hasDocument: !!signedUrl
});
}
return NextResponse.json({
payslips: payslipUrls
});
} catch (error) {
console.error("Erreur lors de la génération des URLs des fiches de paie:", error);
return NextResponse.json(
{ error: "Erreur interne du serveur" },
{ status: 500 }
);
}
}