diff --git a/shop/app/api/admin/orders/[id]/download/route.ts b/shop/app/api/admin/orders/[id]/download/route.ts index 4f4b163..3ad8c32 100644 --- a/shop/app/api/admin/orders/[id]/download/route.ts +++ b/shop/app/api/admin/orders/[id]/download/route.ts @@ -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(); diff --git a/shop/app/api/orders/[id]/download/route.ts b/shop/app/api/orders/[id]/download/route.ts new file mode 100644 index 0000000..d73d856 --- /dev/null +++ b/shop/app/api/orders/[id]/download/route.ts @@ -0,0 +1,133 @@ +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"; +import React from "react"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const inline = request.nextUrl.searchParams.get("inline") === "true"; + + // 1. Auth check — any authenticated user + 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, company_id, user_id") + .eq("id", id) + .single(); + + if (error || !order) { + 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; + + // 4. Try to download from storage if pdf_url is set + if (order.pdf_url) { + try { + const { data: fileData, error: downloadError } = await adminDb + .storage + .from("invoices") + .download(storageFileName); + + if (!downloadError && fileData) { + fileBuffer = await fileData.arrayBuffer(); + } + } catch (storageErr) { + console.warn("Could not retrieve file from storage, will regenerate:", storageErr); + } + } + + // 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', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); + + const buffer = await renderToBuffer( + React.createElement(InvoicePDF, { + orderNumber: order.order_number || `AE-${id.slice(0, 8)}`, + dateStr: formattedDate, + customer: order.customer_data, + orderData: order.order_data, + totalPrice: Number(order.total_price), + }) + ); + + // Upload/Upsert to storage bucket + const { error: uploadError } = await adminDb.storage + .from("invoices") + .upload(storageFileName, Buffer.from(buffer), { + contentType: "application/pdf", + upsert: true, + }); + + if (uploadError) { + console.error("Failed to upload regenerated PDF:", uploadError); + } else { + await adminDb.from("orders").update({ pdf_url: storageFileName }).eq("id", id); + } + + fileBuffer = buffer; + } catch (genErr: any) { + console.error("PDF generation failed inside API:", genErr); + return new NextResponse("Error generating PDF: " + genErr.message, { status: 500 }); + } + } + + // 6. Return as PDF response + const downloadFileName = `Anfragebestaetigung_${order.order_number || id.slice(0, 8)}.pdf`; + + const headers = new Headers(); + if (inline) { + headers.append("Content-Disposition", "inline"); + } else { + headers.append("Content-Disposition", `attachment; filename="${downloadFileName}"`); + } + headers.append("Content-Type", "application/pdf"); + + return new NextResponse(fileBuffer as any, { + status: 200, + headers, + }); + } catch (err: any) { + console.error("Download endpoint failed:", err); + return new NextResponse(err.message || "Internal Server Error", { status: 500 }); + } +} diff --git a/shop/app/my-orders/page.tsx b/shop/app/my-orders/page.tsx index 6875751..ffa7154 100644 --- a/shop/app/my-orders/page.tsx +++ b/shop/app/my-orders/page.tsx @@ -6,7 +6,6 @@ import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import type { Order } from '@/lib/types' -import { resolveSupabaseUrl } from '@/lib/utils' const statusLabel: Record = { pending: 'Eingegangen', @@ -154,12 +153,12 @@ export default async function MyOrdersPage() { {o.pdf_url && ( <> diff --git a/shop/app/order/success/page.tsx b/shop/app/order/success/page.tsx index 9356544..51f2293 100644 --- a/shop/app/order/success/page.tsx +++ b/shop/app/order/success/page.tsx @@ -7,7 +7,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { Badge } from '@/components/ui/badge' import type { Order } from '@/lib/types' -import { resolveSupabaseUrl } from '@/lib/utils' export default async function OrderSuccessPage({ searchParams, @@ -223,12 +222,12 @@ export default async function OrderSuccessPage({ {o.pdf_url && (
diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index c0a7e0c..39e1e56 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -227,9 +227,8 @@ export async function submitOrder(params: { if (uploadError) { console.error('PDF Upload Error:', uploadError) } else { - const { data: publicUrlData } = supabase.storage.from('invoices').getPublicUrl(fileName) - await supabase.from('orders').update({ pdf_url: publicUrlData.publicUrl }).eq('id', order.id) - order.pdf_url = publicUrlData.publicUrl + await supabase.from('orders').update({ pdf_url: fileName }).eq('id', order.id) + order.pdf_url = fileName } // 7. E-Mail mit PDF-Anhang an den Besteller senden @@ -655,9 +654,8 @@ export async function updateOrder( if (uploadError) { console.error('PDF Upload Error:', uploadError) } else { - const { data: publicUrlData } = supabase.storage.from('invoices').getPublicUrl(fileName) - await admin.from('orders').update({ pdf_url: publicUrlData.publicUrl }).eq('id', order.id) - order.pdf_url = publicUrlData.publicUrl + await admin.from('orders').update({ pdf_url: fileName }).eq('id', order.id) + order.pdf_url = fileName } // 4. E-Mail mit PDF-Anhang an den Besteller senden (Info über Änderung)