-- ============================================================= -- Speisekarte – Initiales Schema -- Rollen: admin (Benutzerverwaltung, Branding) / staff (Kantine) -- Öffentliche Speisekarte: lesbar ohne Login (anon) -- ============================================================= -- ---------- Profile (1:1 zu auth.users) ---------- create table public.profiles ( id uuid primary key references auth.users (id) on delete cascade, email text not null, full_name text not null default '', role text not null default 'staff' check (role in ('admin', 'staff')), created_at timestamptz not null default now() ); -- Rolle des eingeloggten Benutzers ermitteln. -- security definer: umgeht RLS auf profiles und verhindert Policy-Rekursion. create or replace function public.app_role() returns text language sql stable security definer set search_path = public as $$ select role from public.profiles where id = auth.uid() $$; -- Profil automatisch anlegen, wenn ein Auth-Benutzer erstellt wird. create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = public as $$ begin insert into public.profiles (id, email, full_name, role) values ( new.id, new.email, coalesce(new.raw_user_meta_data ->> 'full_name', ''), coalesce(new.raw_user_meta_data ->> 'role', 'staff') ) on conflict (id) do nothing; return new; end; $$; create trigger on_auth_user_created after insert on auth.users for each row execute function public.handle_new_user(); -- ---------- Bereiche der Speisekarte ---------- create table public.sections ( key text primary key, label text not null, time_range text not null, sort int not null ); insert into public.sections (key, label, time_range, sort) values ('brotzeit', 'Brotzeit', '09:00 - 10:00', 1), ('mittagessen', 'Mittagessen', '11:30 - 13:30', 2), ('mittagessen_veg', 'Mittagessen vegetarisch', '11:30 - 13:30', 3), ('suppe', 'Suppe', '11:30 - 13:30', 4); -- ---------- Allergene & Zusatzstoffe ---------- -- Gelten für die komplette Speisekarte (keine Zuordnung pro Gericht) -- und werden unterhalb der Karte angezeigt. Pflege durch Kantinen-Team. create table public.allergens ( code text primary key, name text not null, sort int not null default 0 ); create table public.additives ( code text primary key, name text not null, sort int not null default 0 ); -- ---------- Gerichte ---------- create table public.dishes ( id uuid primary key default gen_random_uuid(), name text not null, price numeric(6, 2) not null default 0, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create or replace function public.touch_updated_at() returns trigger language plpgsql as $$ begin new.updated_at := now(); return new; end; $$; create trigger dishes_touch_updated_at before update on public.dishes for each row execute function public.touch_updated_at(); -- ---------- Zuweisung Gericht -> Tag/Bereich ---------- create table public.menu_entries ( id uuid primary key default gen_random_uuid(), menu_date date not null, section_key text not null references public.sections (key), dish_id uuid not null references public.dishes (id) on delete cascade, position int not null default 0, created_at timestamptz not null default now(), unique (menu_date, section_key, dish_id) ); create index menu_entries_date_idx on public.menu_entries (menu_date); -- ---------- Einstellungen (Farben, Logo) – genau eine Zeile ---------- create table public.settings ( id int primary key check (id = 1), colors jsonb not null default '{}'::jsonb, logo_path text, updated_at timestamptz not null default now() ); insert into public.settings (id, colors) values ( 1, '{ "primary": "#003388", "primaryDark": "#0d315c", "accent": "#ff6900", "headerText": "#ffffff", "rowBg": "#e3e4f1", "rowBgAlt": "#f4f4fa", "addBtn": "#6da54c", "deleteBtn": "#e2371d" }'::jsonb ); create trigger settings_touch_updated_at before update on public.settings for each row execute function public.touch_updated_at(); -- ============================================================= -- Row Level Security -- ============================================================= alter table public.profiles enable row level security; alter table public.sections enable row level security; alter table public.allergens enable row level security; alter table public.additives enable row level security; alter table public.dishes enable row level security; alter table public.menu_entries enable row level security; alter table public.settings enable row level security; -- Profiles: eigenes Profil lesen, Admin liest alle. -- Anlegen/Ändern/Löschen von Benutzern läuft serverseitig über den -- Service-Role-Key (umgeht RLS) und wird dort auf Admins beschränkt. create policy "profiles_select_own_or_admin" on public.profiles for select to authenticated using (id = (select auth.uid()) or public.app_role() = 'admin'); -- Öffentlich lesbare Tabellen (Speisekarte ohne Login) create policy "sections_public_read" on public.sections for select to anon, authenticated using (true); create policy "allergens_public_read" on public.allergens for select to anon, authenticated using (true); create policy "additives_public_read" on public.additives for select to anon, authenticated using (true); create policy "dishes_public_read" on public.dishes for select to anon, authenticated using (true); create policy "menu_entries_public_read" on public.menu_entries for select to anon, authenticated using (true); create policy "settings_public_read" on public.settings for select to anon, authenticated using (true); -- Kantinenmitarbeiter (staff) und Admin: Gerichte & Zuweisungen pflegen create policy "dishes_staff_write" on public.dishes for all to authenticated using (public.app_role() in ('staff', 'admin')) with check (public.app_role() in ('staff', 'admin')); create policy "menu_entries_staff_write" on public.menu_entries for all to authenticated using (public.app_role() in ('staff', 'admin')) with check (public.app_role() in ('staff', 'admin')); create policy "allergens_staff_write" on public.allergens for all to authenticated using (public.app_role() in ('staff', 'admin')) with check (public.app_role() in ('staff', 'admin')); create policy "additives_staff_write" on public.additives for all to authenticated using (public.app_role() in ('staff', 'admin')) with check (public.app_role() in ('staff', 'admin')); -- Einstellungen: nur Admin ändert create policy "settings_admin_update" on public.settings for update to authenticated using (public.app_role() = 'admin') with check (public.app_role() = 'admin'); -- ============================================================= -- Storage: Bucket "branding" für das Logo (öffentlich lesbar) -- ============================================================= insert into storage.buckets (id, name, public) values ('branding', 'branding', true); create policy "branding_public_read" on storage.objects for select to anon, authenticated using (bucket_id = 'branding'); create policy "branding_admin_insert" on storage.objects for insert to authenticated with check (bucket_id = 'branding' and public.app_role() = 'admin'); create policy "branding_admin_update" on storage.objects for update to authenticated using (bucket_id = 'branding' and public.app_role() = 'admin'); create policy "branding_admin_delete" on storage.objects for delete to authenticated using (bucket_id = 'branding' and public.app_role() = 'admin');