diff --git a/.agents/skills/second-brain/skill.md b/.agents/skills/second-brain/skill.md new file mode 100644 index 0000000..1fc0914 --- /dev/null +++ b/.agents/skills/second-brain/skill.md @@ -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. diff --git a/funktionen.md b/.agentsspec similarity index 100% rename from funktionen.md rename to .agentsspec diff --git a/shop/app/my-orders/page.tsx b/shop/app/my-orders/page.tsx index ffa7154..38292af 100644 --- a/shop/app/my-orders/page.tsx +++ b/shop/app/my-orders/page.tsx @@ -10,12 +10,14 @@ import type { Order } from '@/lib/types' const statusLabel: Record = { pending: 'Eingegangen', active: 'In Bearbeitung', + completed: 'Abgeschlossen', cancelled: 'Storniert', } const statusClass: Record = { pending: 'bg-amber-500/20 text-amber-400 border-amber-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', } diff --git a/shop/lib/actions/end-customers.ts b/shop/lib/actions/end-customers.ts index e52ce57..5b10828 100644 --- a/shop/lib/actions/end-customers.ts +++ b/shop/lib/actions/end-customers.ts @@ -167,17 +167,17 @@ export async function anonymizeEndCustomer(id: string): Promise { .from('end_customers') .update({ company_name: '[GELÖSCHT]', - vat_id: null, + vat_id: '[GELÖSCHT]', first_name: '[GELÖSCHT]', last_name: '[GELÖSCHT]', street: '[GELÖSCHT]', - zip: null, - city: null, - email: null, - bank_iban: null, - bank_bic: null, - bank_name: null, - bank_owner: null, + zip: '[GELÖSCHT]', + city: '[GELÖSCHT]', + email: '[GELÖSCHT]', + bank_iban: '[GELÖSCHT]', + bank_bic: '[GELÖSCHT]', + bank_name: '[GELÖSCHT]', + bank_owner: '[GELÖSCHT]', is_anonymized: true, }) .eq('id', id) diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index e5c9850..48fc5ac 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -1,6 +1,7 @@ 'use server' import { createClient } from '@/lib/supabase/server' +import { createHash } from 'crypto' import { createAdminClient } from '@/lib/supabase/admin' import { revalidatePath } from 'next/cache' 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). */ function hashOrderSnapshot(snapshot: object): string { const str = JSON.stringify(snapshot) - let hash = 0 - 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) + return createHash('sha256').update(str).digest('hex') } // ─── submitOrder ────────────────────────────────────────────────────────────── @@ -155,16 +150,16 @@ export async function submitOrder(params: { const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null) 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 thirtySecondsAgo = new Date(Date.now() - 30_000).toISOString() + const fiveMinutesAgo = new Date(Date.now() - 5 * 60_000).toISOString() const { data: existingOrder } = await supabase .from('orders') .select('*') .eq('user_id', user.id) .eq('order_hash', orderHash) - .gte('created_at', thirtySecondsAgo) + .gte('created_at', fiveMinutesAgo) .maybeSingle() if (existingOrder) { diff --git a/shop/lib/supabase/admin.ts b/shop/lib/supabase/admin.ts index b6915f2..734d5ce 100644 --- a/shop/lib/supabase/admin.ts +++ b/shop/lib/supabase/admin.ts @@ -1,11 +1,15 @@ import { createClient } from '@supabase/supabase-js' 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 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( - supabaseUrl!, + supabaseUrl, serviceRoleKey, { auth: { @@ -15,3 +19,4 @@ export function createAdminClient() { } ) } + diff --git a/shop/supabase/migrations/20260706220000_fix_critical_rls_policies.sql b/shop/supabase/migrations/20260706220000_fix_critical_rls_policies.sql new file mode 100644 index 0000000..215837a --- /dev/null +++ b/shop/supabase/migrations/20260706220000_fix_critical_rls_policies.sql @@ -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';