188 lines
No EOL
6.9 KiB
TypeScript
188 lines
No EOL
6.9 KiB
TypeScript
// app/api/documents/route.ts
|
||
export const dynamic = "force-dynamic";
|
||
import { NextResponse } from "next/server";
|
||
import { cookies } from "next/headers";
|
||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||
import { getS3SignedUrl } from "@/lib/aws-s3";
|
||
|
||
function json(status: number, body: any) {
|
||
return NextResponse.json(body, { status });
|
||
}
|
||
|
||
export async function GET(req: Request) {
|
||
try {
|
||
const c = cookies();
|
||
const sb = createRouteHandlerClient({ cookies });
|
||
|
||
// 1) Récupérer la catégorie depuis les query params
|
||
const { searchParams } = new URL(req.url);
|
||
const category = searchParams.get("category");
|
||
const metadataOnly = searchParams.get("metadata_only") === "true";
|
||
const period = searchParams.get("period");
|
||
|
||
if (!category) {
|
||
return json(400, { error: "missing_category_parameter" });
|
||
}
|
||
|
||
console.log('📄 Documents API - Category:', category, 'Metadata only:', metadataOnly, 'Period:', period);
|
||
|
||
// 2) <20> SÉCURITÉ : Authentification obligatoire
|
||
const { data: { user }, error: userError } = await sb.auth.getUser();
|
||
|
||
console.log('📄 Documents API - User:', user?.id, 'Error:', userError);
|
||
|
||
if (userError || !user) {
|
||
console.error('❌ [SÉCURITÉ] Utilisateur non authentifié');
|
||
return json(401, { error: "unauthorized", details: "Authentification requise" });
|
||
}
|
||
|
||
// 3) 🔒 SÉCURITÉ : Vérifier si l'utilisateur est staff
|
||
const { data: staffUser } = await sb
|
||
.from("staff_users")
|
||
.select("is_staff")
|
||
.eq("user_id", user.id)
|
||
.maybeSingle();
|
||
|
||
const isStaff = !!staffUser?.is_staff;
|
||
console.log('📄 Documents API - Is staff?', isStaff);
|
||
|
||
let orgId: string;
|
||
const requestedOrgId = c.get("active_org_id")?.value || "";
|
||
|
||
if (isStaff) {
|
||
// 🔒 STAFF : Peut accéder à n'importe quelle organisation
|
||
if (!requestedOrgId) {
|
||
console.error('❌ [SÉCURITÉ] Staff sans organisation sélectionnée');
|
||
return json(400, {
|
||
error: "no_organization_selected",
|
||
details: "Staff user must select an organization first"
|
||
});
|
||
}
|
||
|
||
// Vérifier que l'organisation existe
|
||
const { data: org, error: orgError } = await sb
|
||
.from("organizations")
|
||
.select("id")
|
||
.eq("id", requestedOrgId)
|
||
.maybeSingle();
|
||
|
||
if (orgError || !org) {
|
||
console.error('❌ [SÉCURITÉ] Staff a demandé une organisation inexistante:', requestedOrgId);
|
||
return json(404, {
|
||
error: "organization_not_found",
|
||
details: "L'organisation demandée n'existe pas"
|
||
});
|
||
}
|
||
|
||
orgId = requestedOrgId;
|
||
console.log('✅ [SÉCURITÉ] Staff accède à l\'organisation:', orgId);
|
||
} else {
|
||
// 🔒 CLIENT : Forcé à utiliser son organisation uniquement
|
||
const { data: member, error: memberError } = await sb
|
||
.from("organization_members")
|
||
.select("org_id")
|
||
.eq("user_id", user.id)
|
||
.eq("revoked", false)
|
||
.maybeSingle();
|
||
|
||
console.log('📄 Documents API - Member:', member, 'Error:', memberError);
|
||
|
||
if (!member?.org_id) {
|
||
console.error('❌ [SÉCURITÉ] Client sans organisation trouvée');
|
||
return json(403, {
|
||
error: "no_organization",
|
||
details: "Aucune organisation trouvée pour cet utilisateur"
|
||
});
|
||
}
|
||
|
||
// 🔒 SÉCURITÉ CRITIQUE : Vérifier que le cookie correspond à l'organisation de l'utilisateur
|
||
if (requestedOrgId && requestedOrgId !== member.org_id) {
|
||
console.error('❌ [SÉCURITÉ CRITIQUE] Client a tenté d\'accéder à une autre organisation !');
|
||
console.error(' - Cookie active_org_id:', requestedOrgId);
|
||
console.error(' - Organisation utilisateur:', member.org_id);
|
||
console.error(' - User ID:', user.id);
|
||
console.error(' - User email:', user.email);
|
||
|
||
return json(403, {
|
||
error: "unauthorized_organization",
|
||
message: "Vous ne pouvez accéder qu'aux documents de votre organisation"
|
||
});
|
||
}
|
||
|
||
// Forcer l'utilisation de l'organisation de l'utilisateur
|
||
orgId = member.org_id;
|
||
console.log('✅ [SÉCURITÉ] Client forcé à son organisation:', orgId);
|
||
}
|
||
|
||
// 4) Récupérer les documents depuis Supabase avec RLS
|
||
console.log('📄 Documents API - Fetching from Supabase with org_id:', orgId, 'category:', category);
|
||
|
||
let query = sb
|
||
.from("documents")
|
||
.select("*")
|
||
.eq("org_id", orgId)
|
||
.eq("category", category);
|
||
|
||
// Filtrer par période si spécifié
|
||
if (period) {
|
||
query = query.eq("period_label", period);
|
||
}
|
||
|
||
const { data: documents, error } = await query.order("date_added", { ascending: false });
|
||
|
||
if (error) {
|
||
console.error('📄 Documents API - Supabase Error:', error);
|
||
return json(500, { error: "supabase_error", details: error.message });
|
||
}
|
||
|
||
console.log('📄 Documents API - Found documents:', documents?.length || 0);
|
||
|
||
// 5) Transformer les documents au format attendé par le frontend
|
||
// Si metadata_only=true, ne pas générer les URLs pré-signées S3
|
||
// Exclure les fichiers .DS_Store et autres fichiers système
|
||
const formattedDocuments = await Promise.all(
|
||
(documents || [])
|
||
.filter(doc => {
|
||
// Exclure les fichiers .DS_Store et autres fichiers système
|
||
const filename = doc.filename || '';
|
||
return !filename.startsWith('.') && filename !== '.DS_Store';
|
||
})
|
||
.map(async (doc) => {
|
||
let presignedUrl: string | null = null;
|
||
|
||
// Générer l'URL S3 présignée seulement si demandé (pas en mode metadata_only)
|
||
if (!metadataOnly && doc.storage_path) {
|
||
try {
|
||
// 🔒 SÉCURITÉ : URLs expirées après 15 minutes (au lieu de 1 heure)
|
||
presignedUrl = await getS3SignedUrl(doc.storage_path, 900); // 900s = 15 minutes
|
||
console.log('✅ Generated presigned URL for:', doc.filename);
|
||
} catch (error) {
|
||
console.error('❌ Error generating presigned URL for:', doc.filename, error);
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: doc.id,
|
||
title: doc.filename || doc.type_label || 'Document',
|
||
url: presignedUrl, // null si metadata_only=true
|
||
updatedAt: doc.date_added,
|
||
sizeBytes: doc.size_bytes || 0,
|
||
period_label: doc.period_label,
|
||
meta: {
|
||
category: doc.category,
|
||
type_label: doc.type_label,
|
||
storage_path: doc.storage_path, // Garder le path original pour référence
|
||
}
|
||
};
|
||
})
|
||
);
|
||
|
||
console.log('📄 Documents API - Returning formatted documents:', formattedDocuments.length);
|
||
|
||
return json(200, formattedDocuments);
|
||
|
||
} catch (err: any) {
|
||
console.error('📄 Documents API - Unexpected error:', err);
|
||
return json(500, { error: "server_error", message: err.message });
|
||
}
|
||
} |