security: replace public PDF URLs with authenticated API route, private bucket
All checks were successful
Staging Build / build (push) Successful in 3m33s

This commit is contained in:
DanielS
2026-07-04 17:43:33 +02:00
parent 28f2285a0e
commit 80abbfe566
5 changed files with 176 additions and 21 deletions

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import { createAdminClient } from "@/lib/supabase/admin";
import { InvoicePDF } from "@/components/invoice-pdf";
import { renderToBuffer } from "@react-pdf/renderer";
@@ -10,13 +11,31 @@ export async function GET(
) {
try {
const { id } = await params;
const adminDb = createAdminClient();
const inline = request.nextUrl.searchParams.get("inline") === "true";
// 1. Get the order from database
// 1. Auth check
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { data: dbUser } = await supabase
.from("users")
.select("role, company_id")
.eq("id", user.id)
.single();
if (!dbUser) {
return new NextResponse("Unauthorized", { status: 401 });
}
const adminDb = createAdminClient();
// 2. Get the order from database
const { data: order, error } = await adminDb
.from("orders")
.select("customer_data, order_data, total_price, created_at, pdf_url, order_number")
.select("customer_data, order_data, total_price, created_at, pdf_url, order_number, company_id, user_id")
.eq("id", id)
.single();
@@ -24,10 +43,19 @@ export async function GET(
return new NextResponse("Order not found", { status: 404 });
}
// 3. Authorization: admin, owner, or company member
const isAdmin = dbUser.role === "admin";
const isOwner = order.user_id === user.id;
const isCompanyMember = dbUser.company_id && order.company_id === dbUser.company_id;
if (!isAdmin && !isOwner && !isCompanyMember) {
return new NextResponse("Forbidden", { status: 403 });
}
const storageFileName = `ab_${id}.pdf`;
let fileBuffer: Uint8Array | ArrayBuffer | null = null;
// 2. Try to download from storage if pdf_url is set
// 4. Try to download from storage if pdf_url is set
if (order.pdf_url) {
try {
const { data: fileData, error: downloadError } = await adminDb
@@ -43,7 +71,7 @@ export async function GET(
}
}
// 3. If file not found or pdf_url not set, generate dynamically
// 5. If file not found or pdf_url not set, generate dynamically
if (!fileBuffer) {
try {
const formattedDate = new Date(order.created_at).toLocaleDateString('de-DE', {
@@ -73,9 +101,7 @@ export async function GET(
if (uploadError) {
console.error("Failed to upload regenerated PDF:", uploadError);
} else {
// Update db with pdf_url
const path = `invoices/${storageFileName}`;
await adminDb.from("orders").update({ pdf_url: path }).eq("id", id);
await adminDb.from("orders").update({ pdf_url: storageFileName }).eq("id", id);
}
fileBuffer = buffer;
@@ -85,7 +111,7 @@ export async function GET(
}
}
// 4. Return as PDF response
// 6. Return as PDF response
const downloadFileName = `Anfragebestaetigung_${order.order_number || id.slice(0, 8)}.pdf`;
const headers = new Headers();