security: fix 6 critical RLS vulns, SHA-256 hash, DSGVO [GELÖSCHT], completed status
Some checks failed
Staging Build / build (push) Has been cancelled
Some checks failed
Staging Build / build (push) Has been cancelled
This commit is contained in:
11
.agents/skills/second-brain/skill.md
Normal file
11
.agents/skills/second-brain/skill.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: second-brain
|
||||||
|
description: Verwaltet und aktualisiert die projektspezifische Wissensdatenbank (Second Brain Wiki) im Ordner `.brain/`.
|
||||||
|
---
|
||||||
|
Du bist der Wissens-Architekt des Projekts. Deine Aufgabe ist es, eine strukturierte Markdown-Wissensdatenbank im Ordner `.brain/` im Projekt-Root zu führen.
|
||||||
|
|
||||||
|
**Deine Kernaufgaben bei Aktivierung:**
|
||||||
|
1. Falls der Ordner `.brain/` oder die `index.md` nicht existieren, lege sie sofort strukturiert an.
|
||||||
|
2. Wenn du Code-Änderungen vornimmst, neue Features baust oder Fehler behebst, aktualisiere die betroffene Dokumentation im `.brain/`-Ordner autonom.
|
||||||
|
3. Dokumentiere hierbei: Technische Entscheidungen (Architecture Decision Records - ADRs), geänderte Tabellen-Strukturen, Edge-Cases in der Business-Logik und API-Schnittstellen.
|
||||||
|
4. Halte alle Einträge extrem präzise, modular und frei von Redundanzen.
|
||||||
@@ -10,12 +10,14 @@ import type { Order } from '@/lib/types'
|
|||||||
const statusLabel: Record<string, string> = {
|
const statusLabel: Record<string, string> = {
|
||||||
pending: 'Eingegangen',
|
pending: 'Eingegangen',
|
||||||
active: 'In Bearbeitung',
|
active: 'In Bearbeitung',
|
||||||
|
completed: 'Abgeschlossen',
|
||||||
cancelled: 'Storniert',
|
cancelled: 'Storniert',
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusClass: Record<string, string> = {
|
const statusClass: Record<string, string> = {
|
||||||
pending: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
pending: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
||||||
active: 'bg-green-500/20 text-green-400 border-green-500/30',
|
active: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||||
|
completed: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||||
cancelled: 'bg-red-500/20 text-red-400 border-red-500/30',
|
cancelled: 'bg-red-500/20 text-red-400 border-red-500/30',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,17 +167,17 @@ export async function anonymizeEndCustomer(id: string): Promise<void> {
|
|||||||
.from('end_customers')
|
.from('end_customers')
|
||||||
.update({
|
.update({
|
||||||
company_name: '[GELÖSCHT]',
|
company_name: '[GELÖSCHT]',
|
||||||
vat_id: null,
|
vat_id: '[GELÖSCHT]',
|
||||||
first_name: '[GELÖSCHT]',
|
first_name: '[GELÖSCHT]',
|
||||||
last_name: '[GELÖSCHT]',
|
last_name: '[GELÖSCHT]',
|
||||||
street: '[GELÖSCHT]',
|
street: '[GELÖSCHT]',
|
||||||
zip: null,
|
zip: '[GELÖSCHT]',
|
||||||
city: null,
|
city: '[GELÖSCHT]',
|
||||||
email: null,
|
email: '[GELÖSCHT]',
|
||||||
bank_iban: null,
|
bank_iban: '[GELÖSCHT]',
|
||||||
bank_bic: null,
|
bank_bic: '[GELÖSCHT]',
|
||||||
bank_name: null,
|
bank_name: '[GELÖSCHT]',
|
||||||
bank_owner: null,
|
bank_owner: '[GELÖSCHT]',
|
||||||
is_anonymized: true,
|
is_anonymized: true,
|
||||||
})
|
})
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
|
||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
|
import { createHash } from 'crypto'
|
||||||
import { createAdminClient } from '@/lib/supabase/admin'
|
import { createAdminClient } from '@/lib/supabase/admin'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { sendMail } from '@/utils/mail'
|
import { sendMail } from '@/utils/mail'
|
||||||
@@ -25,18 +26,12 @@ function generateOrderNumber(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt einen einfachen deterministischen Hash aus dem Bestell-Snapshot.
|
* Erstellt einen SHA-256 Hash aus dem Bestell-Snapshot.
|
||||||
* Wird für den Idempotenz-Guard verwendet (verhindert Doppelbestellungen).
|
* Wird für den Idempotenz-Guard verwendet (verhindert Doppelbestellungen).
|
||||||
*/
|
*/
|
||||||
function hashOrderSnapshot(snapshot: object): string {
|
function hashOrderSnapshot(snapshot: object): string {
|
||||||
const str = JSON.stringify(snapshot)
|
const str = JSON.stringify(snapshot)
|
||||||
let hash = 0
|
return createHash('sha256').update(str).digest('hex')
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const char = str.charCodeAt(i)
|
|
||||||
hash = ((hash << 5) - hash) + char
|
|
||||||
hash = hash & hash // 32-bit integer
|
|
||||||
}
|
|
||||||
return Math.abs(hash).toString(36)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── submitOrder ──────────────────────────────────────────────────────────────
|
// ─── submitOrder ──────────────────────────────────────────────────────────────
|
||||||
@@ -155,16 +150,16 @@ export async function submitOrder(params: {
|
|||||||
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
|
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
|
||||||
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate)
|
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate)
|
||||||
|
|
||||||
// 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 30s existiert
|
// 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 5 Minuten existiert
|
||||||
const orderHash = hashOrderSnapshot(orderSnapshot)
|
const orderHash = hashOrderSnapshot(orderSnapshot)
|
||||||
const thirtySecondsAgo = new Date(Date.now() - 30_000).toISOString()
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60_000).toISOString()
|
||||||
|
|
||||||
const { data: existingOrder } = await supabase
|
const { data: existingOrder } = await supabase
|
||||||
.from('orders')
|
.from('orders')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('user_id', user.id)
|
.eq('user_id', user.id)
|
||||||
.eq('order_hash', orderHash)
|
.eq('order_hash', orderHash)
|
||||||
.gte('created_at', thirtySecondsAgo)
|
.gte('created_at', fiveMinutesAgo)
|
||||||
.maybeSingle()
|
.maybeSingle()
|
||||||
|
|
||||||
if (existingOrder) {
|
if (existingOrder) {
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { createClient } from '@supabase/supabase-js'
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
export function createAdminClient() {
|
export function createAdminClient() {
|
||||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU'
|
|
||||||
const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
|
||||||
|
if (!supabaseUrl || !serviceRoleKey) {
|
||||||
|
throw new Error('SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set in environment variables.');
|
||||||
|
}
|
||||||
|
|
||||||
return createClient(
|
return createClient(
|
||||||
supabaseUrl!,
|
supabaseUrl,
|
||||||
serviceRoleKey,
|
serviceRoleKey,
|
||||||
{
|
{
|
||||||
auth: {
|
auth: {
|
||||||
@@ -15,3 +19,4 @@ export function createAdminClient() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
-- Migration: Fix critical RLS security issues
|
||||||
|
-- 1. Restrict products/modules/categories CRUD to admin role only
|
||||||
|
-- 2. Restrict settings table to admin role only
|
||||||
|
-- 3. Remove USING(true) wildcard policies on companies/partners
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- 1. Products: Replace overly permissive CRUD policies with admin-only
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated insert on products" ON public.products;
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated update on products" ON public.products;
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated delete on products" ON public.products;
|
||||||
|
|
||||||
|
CREATE POLICY "Admin insert products" ON public.products FOR INSERT
|
||||||
|
WITH CHECK ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
CREATE POLICY "Admin update products" ON public.products FOR UPDATE
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
CREATE POLICY "Admin delete products" ON public.products FOR DELETE
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- 2. Product Modules: Same fix
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated insert on product_modules" ON public.product_modules;
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated update on product_modules" ON public.product_modules;
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated delete on product_modules" ON public.product_modules;
|
||||||
|
|
||||||
|
CREATE POLICY "Admin insert product_modules" ON public.product_modules FOR INSERT
|
||||||
|
WITH CHECK ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
CREATE POLICY "Admin update product_modules" ON public.product_modules FOR UPDATE
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
CREATE POLICY "Admin delete product_modules" ON public.product_modules FOR DELETE
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- 3. Categories: Same fix
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated insert on categories" ON public.categories;
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated update on categories" ON public.categories;
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated delete on categories" ON public.categories;
|
||||||
|
|
||||||
|
CREATE POLICY "Admin insert categories" ON public.categories FOR INSERT
|
||||||
|
WITH CHECK ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
CREATE POLICY "Admin update categories" ON public.categories FOR UPDATE
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
CREATE POLICY "Admin delete categories" ON public.categories FOR DELETE
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- 4. Settings: Restrict to admin-only (contains SMTP credentials!)
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated read on settings" ON public.settings;
|
||||||
|
DROP POLICY IF EXISTS "Allow authenticated write on settings" ON public.settings;
|
||||||
|
|
||||||
|
CREATE POLICY "Admin read settings" ON public.settings FOR SELECT
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
CREATE POLICY "Admin write settings" ON public.settings FOR ALL
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin')
|
||||||
|
WITH CHECK ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- 5. Companies: Remove USING(true) wildcard, service_role bypasses RLS anyway
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Service role kann alles Unternehmen" ON public.companies;
|
||||||
|
DROP POLICY IF EXISTS "Service role kann alles Partner" ON public.partners;
|
||||||
|
|
||||||
|
-- Admin-only write access for companies
|
||||||
|
CREATE POLICY "Admin manage companies" ON public.companies FOR ALL
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin')
|
||||||
|
WITH CHECK ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
-- Admin-only write access for partners
|
||||||
|
CREATE POLICY "Admin manage partners" ON public.partners FOR ALL
|
||||||
|
USING ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin')
|
||||||
|
WITH CHECK ((SELECT role FROM public.users WHERE id = auth.uid()) = 'admin');
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- 6. Orders: Add INSERT policy with company_id check
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Users can create their own orders" ON public.orders;
|
||||||
|
|
||||||
|
CREATE POLICY "Users can create their own orders" ON public.orders FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
auth.uid() = user_id
|
||||||
|
AND (
|
||||||
|
(SELECT role FROM public.users WHERE id = auth.uid()) = 'admin'
|
||||||
|
OR (SELECT company_id FROM public.users WHERE id = auth.uid()) IS NOT NULL
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Reload Schema Cache
|
||||||
|
NOTIFY pgrst, 'reload schema';
|
||||||
Reference in New Issue
Block a user