feat: fatura PDF, hizmet/yazılım atama dosya ekleri
- /print/invoices/[id] sayfası: A4 fatura yazdırma/PDF (AutoPrint + PrintActionBar) - Fatura detayı header'ına PDF butonu eklendi (Yazdır yerine) - Appwrite Storage: entity-attachments bucket (20MB, şifreli) - Appwrite Tables: attachments collection (tenantId, entityType, entityId, fileId, name, size, mimeType) - attachment-actions.ts: fetchAttachmentsAction, uploadAttachmentAction, deleteAttachmentAction - AttachmentsPanel bileşeni: dosya yükleme/listeleme/silme, edit modunda görünür - Hizmet ve yazılım atama form sheet'lerine AttachmentsPanel entegrasyonu - /api/files/[attachmentId]: güvenli proxy indirme (tenant doğrulama + admin key ile Appwrite'a istek)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { BUCKETS, DATABASE_ID, TABLES, type Attachment } from "@/lib/appwrite/schema";
|
||||
import { createAdminClient } from "@/lib/appwrite/server";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ attachmentId: string }> },
|
||||
) {
|
||||
const { attachmentId } = await params;
|
||||
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
let attachment: Attachment;
|
||||
try {
|
||||
attachment = (await tablesDB.getRow(
|
||||
DATABASE_ID,
|
||||
TABLES.attachments,
|
||||
attachmentId,
|
||||
)) as unknown as Attachment;
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (attachment.tenantId !== ctx.tenantId) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const endpoint = (process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT ?? "").replace(/\/$/, "");
|
||||
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID ?? "";
|
||||
const apiKey = process.env.APPWRITE_API_KEY ?? "";
|
||||
|
||||
const fileUrl = `${endpoint}/storage/buckets/${BUCKETS.entityAttachments}/files/${attachment.fileId}/download`;
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(fileUrl, {
|
||||
headers: {
|
||||
"X-Appwrite-Project": projectId,
|
||||
"X-Appwrite-Key": apiKey,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Storage unavailable" }, { status: 502 });
|
||||
}
|
||||
|
||||
if (!upstream.ok) {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const contentType =
|
||||
upstream.headers.get("Content-Type") || "application/octet-stream";
|
||||
const encodedName = encodeURIComponent(attachment.name);
|
||||
|
||||
return new NextResponse(upstream.body, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Content-Disposition": `attachment; filename*=UTF-8''${encodedName}`,
|
||||
"Cache-Control": "private, no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user