Compare commits
3 Commits
d122c957eb
...
c7a4b2a9c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7a4b2a9c9 | ||
|
|
5b03de6b7e | ||
|
|
100ad254cb |
@@ -59,7 +59,8 @@ graph TD
|
|||||||
---
|
---
|
||||||
|
|
||||||
### F. PDF-Rechnungsgenerierung & Mail-Versand (Post-Processing)
|
### F. PDF-Rechnungsgenerierung & Mail-Versand (Post-Processing)
|
||||||
- **Snapshot-Exklusivität**: Die Rechnungsgenerierung erfolgt ausschließlich auf Basis der in `orders.order_data` und `orders.customer_data` eingefrorenen Snapshots.
|
- **Entkopplung & Asynchronität**: Die Generierung der PDFs und der E-Mail-Versand sind vollständig aus dem synchronen Checkout-Flow entkoppelt. Der Checkout liefert dem Frontend sofort nach Speicherung in der DB eine Erfolgsmeldung zurück. Die Generierung und der Versand werden asynchron im Hintergrund (via unblockiertem Promise-Worker) ausgeführt, um SMTP-Latenzen abzufangen.
|
||||||
|
- **Snapshot-Exklusivität**: Die PDF-Generierung erfolgt ausschließlich auf Basis der in `orders.order_data` und `orders.customer_data` eingefrorenen Snapshots.
|
||||||
- **Layout-Unterscheidung**:
|
- **Layout-Unterscheidung**:
|
||||||
- **Typ `purchase` (Kauf)**:
|
- **Typ `purchase` (Kauf)**:
|
||||||
- Generiert eine klassische **B2B-Angebot**.
|
- Generiert eine klassische **B2B-Angebot**.
|
||||||
|
|||||||
@@ -13,3 +13,7 @@ RLS ist auf Datenbankebene in Postgres implementiert und erzwingt Datenisolierun
|
|||||||
- Admins haben uneingeschränkten Zugriff und können Bestellungen nachträglich anderen Companies zuweisen.
|
- Admins haben uneingeschränkten Zugriff und können Bestellungen nachträglich anderen Companies zuweisen.
|
||||||
- **Lizenzen (`licenses`)**:
|
- **Lizenzen (`licenses`)**:
|
||||||
- Partner sehen nur Lizenzen von Endkunden, die ihrer Company zugeordnet sind.
|
- Partner sehen nur Lizenzen von Endkunden, die ihrer Company zugeordnet sind.
|
||||||
|
|
||||||
|
- **Systembenutzer (`users`) & Rollen-Schutz**:
|
||||||
|
- Authentifizierte Nutzer dürfen nur ihr eigenes Profil lesen.
|
||||||
|
- Rollenschutz-Trigger (`check_user_role_escalation`): Jegliche Änderung der Spalte `role` oder Zuweisung der Rolle `'admin'` ist auf API-Ebene für normale Nutzer gesperrt. Updates/Inserts der Rolle werden ausschließlich von der `service_role` (Backend Admin-Client) akzeptiert, um Privilegien-Eskalation zu verhindern.
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { createClient } from '@/lib/supabase/server';
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { buildCustomerSnapshot, buildOrderSnapshot } from '@/lib/license-transform';
|
import { buildCustomerSnapshot, buildOrderSnapshot } from '@/lib/license-transform';
|
||||||
import { getProducts, getCategories } from '@/lib/actions/products';
|
import { getProducts, getCategories } from '@/lib/actions/products';
|
||||||
|
import { sendMail } from '@/utils/mail';
|
||||||
|
import { renderToBuffer } from '@react-pdf/renderer';
|
||||||
|
import React from 'react';
|
||||||
|
import { InvoicePDF } from '@/components/invoice-pdf';
|
||||||
|
|
||||||
function generateOrderNumber(prefix: 'BE' | 'AE' = 'AE'): string {
|
function generateOrderNumber(prefix: 'BE' | 'AE' = 'AE'): string {
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
@@ -16,6 +20,55 @@ function hashOrderPayload(endCustomerId: string, items: any[]): string {
|
|||||||
return createHash('sha256').update(str).digest('hex');
|
return createHash('sha256').update(str).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Asynchroner Hintergrund-Worker für Post-Processing (PDF-Erstellung und E-Mail)
|
||||||
|
async function triggerPostProcessing(orderId: string, supabase: any, customerSnapshot: any, orderSnapshot: any, type: string, email: string) {
|
||||||
|
try {
|
||||||
|
const { data: order, error } = await supabase
|
||||||
|
.from('orders')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', orderId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !order) {
|
||||||
|
console.error(`Post-Processing failed: Order ${orderId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF generieren
|
||||||
|
const buffer = await renderToBuffer(
|
||||||
|
React.createElement(InvoicePDF, {
|
||||||
|
order,
|
||||||
|
customer: customerSnapshot,
|
||||||
|
orderSnapshot,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileName = `ab_${order.id}.pdf`;
|
||||||
|
const { error: uploadError } = await supabase
|
||||||
|
.storage
|
||||||
|
.from('invoices')
|
||||||
|
.upload(fileName, buffer, { contentType: 'application/pdf', upsert: true });
|
||||||
|
|
||||||
|
if (!uploadError) {
|
||||||
|
await supabase.from('orders').update({ pdf_url: fileName }).eq('id', order.id);
|
||||||
|
} else {
|
||||||
|
console.error(`PDF Upload failed for order ${orderId}:`, uploadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail versenden
|
||||||
|
if (email) {
|
||||||
|
await sendMail({
|
||||||
|
to: email,
|
||||||
|
subject: `Bestätigung Ihrer CASPOS-Anfrage ${order.order_number}`,
|
||||||
|
text: `Vielen Dank für Ihre Anfrage ${order.order_number}.`,
|
||||||
|
html: `<p>Vielen Dank für Ihre Anfrage <strong>${order.order_number}</strong>.</p>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error in post-processing for order ${orderId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkoutAction(params: {
|
export async function checkoutAction(params: {
|
||||||
items: any[];
|
items: any[];
|
||||||
endCustomerId: string;
|
endCustomerId: string;
|
||||||
@@ -132,6 +185,13 @@ export async function checkoutAction(params: {
|
|||||||
|
|
||||||
if (orderError) throw orderError;
|
if (orderError) throw orderError;
|
||||||
orderIds.push(order.id);
|
orderIds.push(order.id);
|
||||||
|
|
||||||
|
// Entkoppeltes Post-Processing (asynchroner Aufruf)
|
||||||
|
if (user.email) {
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
triggerPostProcessing(order.id, supabase, customerSnapshot, orderSnapshot, type, user.email!);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await createOrderGroup(purchaseItems, 'purchase', 'invoice');
|
await createOrderGroup(purchaseItems, 'purchase', 'invoice');
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- Migration: Secure User Roles from Self-Escalation
|
||||||
|
-- Purpose: Prevent users from updating their own roles to 'admin' using RLS / Triggers.
|
||||||
|
|
||||||
|
-- Ensure RLS is enabled on users
|
||||||
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policy to allow users to view their own records
|
||||||
|
CREATE POLICY select_own_user ON public.users
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (auth.uid() = id);
|
||||||
|
|
||||||
|
-- Policy to allow admins to view all users
|
||||||
|
CREATE POLICY select_all_users_for_admin ON public.users
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
(SELECT role FROM public.users WHERE id = auth.uid()) = 'admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Trigger to prevent any role updates to 'admin' from unauthorized users
|
||||||
|
CREATE OR REPLACE FUNCTION check_user_role_escalation()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Only allow changes to the role column if executed by the service_role
|
||||||
|
IF (TG_OP = 'UPDATE' AND OLD.role IS DISTINCT FROM NEW.role) OR (TG_OP = 'INSERT') THEN
|
||||||
|
IF current_setting('role', true) <> 'service_role' THEN
|
||||||
|
-- Partners cannot upgrade themselves or others to admin
|
||||||
|
IF NEW.role = 'admin' THEN
|
||||||
|
RAISE EXCEPTION 'Unberechtigtes Rollen-Upgrade verweigert.';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS enforce_role_protection ON public.users;
|
||||||
|
CREATE TRIGGER enforce_role_protection
|
||||||
|
BEFORE INSERT OR UPDATE ON public.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION check_user_role_escalation();
|
||||||
Reference in New Issue
Block a user