import { NextResponse } from "next/server"; import { revalidatePath } from "next/cache"; import { AppwriteException, ID, Permission, Role } from "node-appwrite"; import { InputFile } from "node-appwrite/file"; import { logAudit } from "@/lib/appwrite/audit"; import { BUCKETS, DATABASE_ID, TABLES, type Job, type JobFileKind, } from "@/lib/appwrite/schema"; import { createAdminClient } from "@/lib/appwrite/server"; import { requireRole, requireTenant } from "@/lib/appwrite/tenant-guard"; const MAX_FILE_BYTES = 30 * 1024 * 1024; function classifyFile(mimeType: string | undefined, name: string): JobFileKind { const lower = (mimeType || name).toLowerCase(); if (/\.(stl|obj|ply|3mf|dcm)$/i.test(name)) return "scan"; if (lower.startsWith("image/") || /\.(png|jpe?g|webp|tiff?|heic|heif|bmp)$/i.test(name)) { return "image"; } return "document"; } function filePermissions(clinicTenantId: string, labTenantId: string): string[] { return [ Permission.read(Role.team(clinicTenantId)), Permission.read(Role.team(labTenantId)), Permission.delete(Role.team(clinicTenantId, "owner")), Permission.delete(Role.team(clinicTenantId, "admin")), Permission.delete(Role.team(labTenantId, "owner")), Permission.delete(Role.team(labTenantId, "admin")), ]; } async function loadJobForTenant(jobId: string, tenantId: string): Promise { try { const { tablesDB } = createAdminClient(); const row = await tablesDB.getRow(DATABASE_ID, TABLES.jobs, jobId); const job = row as unknown as Job; if (job.clinicTenantId !== tenantId && job.labTenantId !== tenantId) return null; return job; } catch { return null; } } function errorMessage(e: unknown, fallback: string): string { if (e instanceof AppwriteException) return e.message || fallback; if (process.env.NODE_ENV !== "production" && e instanceof Error) { return `${fallback} (${e.message})`; } return fallback; } export async function POST( request: Request, ctx: { params: Promise<{ jobId: string }> }, ) { const { jobId } = await ctx.params; if (!jobId) { return NextResponse.json({ ok: false, error: "İş bulunamadı." }, { status: 400 }); } let tenantCtx; try { tenantCtx = await requireTenant(); requireRole(tenantCtx, ["owner", "admin", "member"]); } catch { return NextResponse.json({ ok: false, error: "Yetkiniz yok." }, { status: 401 }); } const job = await loadJobForTenant(jobId, tenantCtx.tenantId); if (!job) { return NextResponse.json({ ok: false, error: "İş bulunamadı." }, { status: 404 }); } let formData: FormData; try { formData = await request.formData(); } catch (e) { console.error("[POST /api/jobs/files] formData parse", e); return NextResponse.json( { ok: false, error: "İstek okunamadı." }, { status: 400 }, ); } const files = formData .getAll("files") .filter((v): v is File => v instanceof File && v.size > 0); if (files.length === 0) { return NextResponse.json({ ok: false, error: "Dosya seçin." }, { status: 400 }); } for (const f of files) { if (f.size > MAX_FILE_BYTES) { return NextResponse.json( { ok: false, error: `${f.name} 30MB sınırını aşıyor.` }, { status: 400 }, ); } } const { storage, tablesDB } = createAdminClient(); const uploadedFileIds: string[] = []; const createdRowIds: string[] = []; try { for (const f of files) { const fileId = ID.unique(); const buffer = Buffer.from(await f.arrayBuffer()); const inputFile = InputFile.fromBuffer(buffer, f.name); await storage.createFile({ bucketId: BUCKETS.jobFiles, fileId, file: inputFile, permissions: filePermissions(job.clinicTenantId, job.labTenantId), }); uploadedFileIds.push(fileId); const kind = classifyFile(f.type, f.name); const row = await tablesDB.createRow( DATABASE_ID, TABLES.jobFiles, ID.unique(), { jobId: job.$id, clinicTenantId: job.clinicTenantId, labTenantId: job.labTenantId, uploadedBy: tenantCtx.user.id, kind, fileId, name: f.name.slice(0, 255), size: f.size, mimeType: f.type ? f.type.slice(0, 100) : undefined, }, filePermissions(job.clinicTenantId, job.labTenantId), ); createdRowIds.push(row.$id); } await logAudit({ tenantId: tenantCtx.tenantId, userId: tenantCtx.user.id, action: "create", entityType: "job_files", entityId: jobId, changes: { count: files.length }, }); revalidatePath(`/jobs/${jobId}`); return NextResponse.json({ ok: true, uploaded: files.length }); } catch (e) { console.error("[POST /api/jobs/files]", e); for (const id of createdRowIds) { try { await tablesDB.deleteRow(DATABASE_ID, TABLES.jobFiles, id); } catch { /* ignore */ } } for (const id of uploadedFileIds) { try { await storage.deleteFile({ bucketId: BUCKETS.jobFiles, fileId: id }); } catch { /* ignore */ } } return NextResponse.json( { ok: false, error: errorMessage(e, "Dosya yüklenemedi.") }, { status: 500 }, ); } }