/** * Pfad → Resource → Owner Mapping für `/api/files/download`. * * Jeder Upload-Subdirectory ist mit genau einem Prisma-Model + Path-Field * verknüpft. Wir suchen den Record, der diesen Path referenziert, und * leiten daraus den zuständigen Customer/Contract ab. canAccessCustomer / * canAccessContract entscheidet danach über Zugriff. * * Pfade werden 1:1 mit dem in der DB gespeicherten Wert verglichen * (z.B. `/uploads/bank-cards/12345.pdf`). Damit ist Path-Traversal * automatisch ausgeschlossen – ein konstruierter Pfad findet keinen Record. */ import prisma from '../lib/prisma.js'; export type FileOwner = | { kind: 'customer'; customerId: number } | { kind: 'contract'; contractId: number } | { kind: 'admin' } | { kind: 'gdpr-admin' }; export async function findUploadOwner(uploadPath: string): Promise { // Format-Check: muss mit /uploads// beginnen, kein Traversal. if (!uploadPath.startsWith('/uploads/')) return null; if (uploadPath.includes('..') || uploadPath.includes('\0')) return null; const parts = uploadPath.split('/'); // ['', 'uploads', '', ''] if (parts.length < 4) return null; const subDir = parts[2]; switch (subDir) { case 'bank-cards': { const r = await prisma.bankCard.findFirst({ where: { documentPath: uploadPath }, select: { customerId: true }, }); return r ? { kind: 'customer', customerId: r.customerId } : null; } case 'documents': { const r = await prisma.identityDocument.findFirst({ where: { documentPath: uploadPath }, select: { customerId: true }, }); return r ? { kind: 'customer', customerId: r.customerId } : null; } case 'business-registrations': { const r = await prisma.customer.findFirst({ where: { businessRegistrationPath: uploadPath }, select: { id: true }, }); return r ? { kind: 'customer', customerId: r.id } : null; } case 'commercial-registers': { const r = await prisma.customer.findFirst({ where: { commercialRegisterPath: uploadPath }, select: { id: true }, }); return r ? { kind: 'customer', customerId: r.id } : null; } case 'privacy-policies': { const r = await prisma.customer.findFirst({ where: { privacyPolicyPath: uploadPath }, select: { id: true }, }); return r ? { kind: 'customer', customerId: r.id } : null; } case 'authorizations': { const r = await prisma.representativeAuthorization.findFirst({ where: { documentPath: uploadPath }, select: { customerId: true }, }); return r ? { kind: 'customer', customerId: r.customerId } : null; } case 'contract-documents': { const r = await prisma.contractDocument.findFirst({ where: { documentPath: uploadPath }, select: { contractId: true }, }); return r ? { kind: 'contract', contractId: r.contractId } : null; } case 'invoices': { const r = await prisma.invoice.findFirst({ where: { documentPath: uploadPath }, select: { contractId: true }, }); return r?.contractId ? { kind: 'contract', contractId: r.contractId } : null; } case 'cancellation-letters': case 'cancellation-confirmations': case 'cancellation-letters-options': case 'cancellation-confirmations-options': { const fieldMap: Record = { 'cancellation-letters': 'cancellationLetterPath', 'cancellation-confirmations': 'cancellationConfirmationPath', 'cancellation-letters-options': 'cancellationLetterOptionsPath', 'cancellation-confirmations-options': 'cancellationConfirmationOptionsPath', }; const field = fieldMap[subDir]; const r = await prisma.contract.findFirst({ where: { [field]: uploadPath }, select: { id: true }, }); return r ? { kind: 'contract', contractId: r.id } : null; } case 'pdf-templates': { // Admin-only Resource: Vorlagen gehören keinem Customer. const r = await prisma.pdfTemplate.findFirst({ where: { templatePath: uploadPath }, select: { id: true }, }); return r ? { kind: 'admin' } : null; } default: return null; } }