-- ============================================================================ -- RAPPORT — Storage Buckets (Supabase Storage / S3) -- ============================================================================ -- Zweck: Datei-Uploads (Quittungen, Studio-Logos) liegen NICHT als Base64 in -- der DB, sondern in Supabase Storage. Die DB hält nur den Pfad -- (z.B. expenses.receipt_url = 'receipts//2025/abc.pdf'). -- -- Konvention für Pfade: '//.' -- → erste Path-Komponente = studio_id (für RLS) -- -- Buckets sind PRIVATE — Zugriff nur über signierte URLs (zeitlich begrenzt). -- ============================================================================ -- Hinweis: KEINE `public`-Spalte angeben. Beim Postgres-Init existiert sie in -- storage.buckets noch nicht (die fügt die Storage-API erst beim Boot per -- eigener Migration hinzu). Default ist `false` → Buckets sind privat, wie -- gewünscht. Würden wir `public` referenzieren, bräche der Init hier ab und -- ALLE folgenden Migrations (inkl. ensure_profile in 0005) liefen nicht mehr. insert into storage.buckets (id, name) values ('receipts', 'receipts'), ('logos', 'logos') on conflict (id) do nothing; -- ──────────────────────────────────────────────────────────────────────────── -- RLS-Policies auf storage.objects -- ──────────────────────────────────────────────────────────────────────────── -- Prinzip: erste Pfad-Komponente ist studio_id; Zugriff nur wenn Member. -- `split_part(name, '/', 1)` gibt die erste Pfad-Komponente zurück. -- -- Bewusst NICHT storage.foldername() benutzen: die Storage-API droppt/erstellt -- diese Funktion bei ihren eigenen Boot-Migrations neu. Eine Policy-Abhängigkeit -- darauf würde diesen Drop blockieren ("cannot drop function foldername") und -- die Storage-API in eine Crash-Loop schicken. split_part ist ein eingebautes -- Postgres-Builtin ohne diese Kopplung. create policy "rapport_storage_read" on storage.objects for select using ( bucket_id in ('receipts','logos') and is_studio_member( split_part(name, '/', 1)::uuid ) ); create policy "rapport_storage_insert" on storage.objects for insert with check ( bucket_id in ('receipts','logos') and is_studio_member( split_part(name, '/', 1)::uuid ) ); create policy "rapport_storage_update" on storage.objects for update using ( bucket_id in ('receipts','logos') and is_studio_member( split_part(name, '/', 1)::uuid ) ); create policy "rapport_storage_delete" on storage.objects for delete using ( bucket_id in ('receipts','logos') and is_studio_member( split_part(name, '/', 1)::uuid ) ); -- ──────────────────────────────────────────────────────────────────────────── -- Hinweise für den Adapter (kein SQL, nur Doku): -- ──────────────────────────────────────────────────────────────────────────── -- Upload (Frontend, in SupabaseAdapter): -- const path = `${studioId}/${year}/${uuid()}.${ext}` -- await supabase.storage.from('receipts').upload(path, file) -- // Pfad in expenses.receipt_url speichern -- -- Anzeige: -- const { data } = await supabase.storage -- .from('receipts') -- .createSignedUrl(receipt_url, 60) // 60 Sekunden gültig -- // -- -- Migration localStorage → Cloud (im Push-Wizard): -- for jede expense mit receiptData (Base64): -- blob = base64ToBlob(receiptData) -- path = upload(blob) -- row.receipt_url = path; delete row.receiptData -- ============================================================================