Supabase: Schema, RLS-Policies und Seed-Daten

This commit is contained in:
2026-07-17 18:18:10 +02:00
parent d11d2a0687
commit 63a30a3835
3 changed files with 363 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
-- =============================================================
-- 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');