169 lines
No EOL
5.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
} |