-- ============================================================================ -- 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). -- ============================================================================ insert into storage.buckets (id, name, public) values ('receipts', 'receipts', false), ('logos', 'logos', false) on conflict (id) do nothing; -- ──────────────────────────────────────────────────────────────────────────── -- RLS-Policies auf storage.objects -- ──────────────────────────────────────────────────────────────────────────── -- Prinzip: erste Pfad-Komponente ist studio_id; Zugriff nur wenn Member. -- `(storage.foldername(name))[1]` gibt die erste Pfad-Komponente zurück. create policy "rapport_storage_read" on storage.objects for select using ( bucket_id in ('receipts','logos') and is_studio_member( (storage.foldername(name))[1]::uuid ) ); create policy "rapport_storage_insert" on storage.objects for insert with check ( bucket_id in ('receipts','logos') and is_studio_member( (storage.foldername(name))[1]::uuid ) ); create policy "rapport_storage_update" on storage.objects for update using ( bucket_id in ('receipts','logos') and is_studio_member( (storage.foldername(name))[1]::uuid ) ); create policy "rapport_storage_delete" on storage.objects for delete using ( bucket_id in ('receipts','logos') and is_studio_member( (storage.foldername(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 -- ============================================================================