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');
+114
View File
@@ -0,0 +1,114 @@
-- =============================================================
-- Seed-Daten: Allergene, Zusatzstoffe, Testgerichte, Beispielwochen
-- =============================================================
-- ---------- Allergene (EU-Kennzeichnung, Buchstaben) ----------
insert into public.allergens (code, name, sort) values
('A', 'Glutenhaltiges Getreide', 1),
('B', 'Krebstiere', 2),
('C', 'Eier', 3),
('D', 'Fisch', 4),
('E', 'Erdnüsse', 5),
('F', 'Sojabohnen', 6),
('G', 'Milch (einschließlich Laktose)', 7),
('H', 'Schalenfrüchte (Nüsse)', 8),
('I', 'Sellerie', 9),
('J', 'Senf', 10),
('K', 'Sesamsamen', 11),
('L', 'Schwefeldioxid und Sulfite', 12),
('M', 'Lupinen', 13),
('N', 'Weichtiere', 14);
-- ---------- Zusatzstoffe (Ziffern) ----------
insert into public.additives (code, name, sort) values
('1', 'mit Farbstoff', 1),
('2', 'mit Konservierungsstoff', 2),
('3', 'mit Antioxidationsmittel', 3),
('4', 'mit Geschmacksverstärker', 4),
('5', 'geschwefelt', 5),
('6', 'geschwärzt', 6),
('7', 'gewachst', 7),
('8', 'mit Phosphat', 8),
('9', 'mit Süßungsmittel(n)', 9),
('10', 'enthält eine Phenylalaninquelle', 10),
('11', 'koffeinhaltig', 11);
-- ---------- Testgerichte ----------
insert into public.dishes (name, price) values
-- Brotzeit
('1 Stk. Rindskäsewurst mit Brötchen', 2.30),
('Hackfleischbaguette', 3.20),
('Knusperhähnchen Wrap', 3.70),
('Leberkäse im Brötchen', 2.30),
('Chili con Carne mit Brötchen', 3.90),
-- Mittagessen
('Schnitzel Baguette', 3.50),
('Penne mit Grünen Spargel und Garnelenspieße', 5.90),
('Bratkartoffel Gemüse Pfanne mit Hähnchenbrust und Dip', 5.90),
('Schweinefilet im Speckmantel dazu Rahmsoße und Butterspätzle', 5.90),
-- Mittagessen vegetarisch
('Penne mit Grünen Spargel, get. Tomaten und Cherrytomaten', 5.30),
('Spaghetti mit fruchtiger Tomatensauce und Parmesan', 5.30),
('Gebackener Feta mit griechischen Bauernsalat und Baguette', 5.30),
('Gemüse-Curry mit Basmatireis', 4.90),
-- Suppen
('Kartoffelcremesuppe', 1.50),
('Gulaschsuppe', 2.50),
('Tomatensuppe mit Croutons', 1.80),
('Kürbiscremesuppe', 1.80),
('Linseneintopf mit Würstchen', 2.20);
-- ---------- Beispielwoche 13.–17. Juli 2026 (wie Screenshot) ----------
with d as (select id, name from public.dishes)
insert into public.menu_entries (menu_date, section_key, dish_id)
select x.menu_date::date, x.section_key, d.id
from (values
-- Brotzeit
('2026-07-13', 'brotzeit', '1 Stk. Rindskäsewurst mit Brötchen'),
('2026-07-14', 'brotzeit', 'Hackfleischbaguette'),
('2026-07-15', 'brotzeit', 'Knusperhähnchen Wrap'),
('2026-07-16', 'brotzeit', 'Leberkäse im Brötchen'),
('2026-07-17', 'brotzeit', 'Chili con Carne mit Brötchen'),
-- Mittagessen
('2026-07-13', 'mittagessen', 'Schnitzel Baguette'),
('2026-07-14', 'mittagessen', 'Penne mit Grünen Spargel und Garnelenspieße'),
('2026-07-15', 'mittagessen', 'Bratkartoffel Gemüse Pfanne mit Hähnchenbrust und Dip'),
('2026-07-16', 'mittagessen', 'Schweinefilet im Speckmantel dazu Rahmsoße und Butterspätzle'),
-- Mittagessen vegetarisch
('2026-07-14', 'mittagessen_veg', 'Penne mit Grünen Spargel, get. Tomaten und Cherrytomaten'),
('2026-07-15', 'mittagessen_veg', 'Spaghetti mit fruchtiger Tomatensauce und Parmesan'),
('2026-07-16', 'mittagessen_veg', 'Gebackener Feta mit griechischen Bauernsalat und Baguette'),
-- Suppe
('2026-07-13', 'suppe', 'Kartoffelcremesuppe'),
('2026-07-14', 'suppe', 'Gulaschsuppe'),
('2026-07-15', 'suppe', 'Tomatensuppe mit Croutons'),
('2026-07-16', 'suppe', 'Kürbiscremesuppe'),
('2026-07-17', 'suppe', 'Linseneintopf mit Würstchen')
) as x (menu_date, section_key, name)
join d on d.name = x.name;
-- ---------- Beispielwoche 20.–24. Juli 2026 ("Nächste Woche") ----------
with d as (select id, name from public.dishes)
insert into public.menu_entries (menu_date, section_key, dish_id)
select x.menu_date::date, x.section_key, d.id
from (values
('2026-07-20', 'brotzeit', 'Leberkäse im Brötchen'),
('2026-07-21', 'brotzeit', 'Knusperhähnchen Wrap'),
('2026-07-22', 'brotzeit', '1 Stk. Rindskäsewurst mit Brötchen'),
('2026-07-23', 'brotzeit', 'Hackfleischbaguette'),
('2026-07-24', 'brotzeit', 'Chili con Carne mit Brötchen'),
('2026-07-20', 'mittagessen', 'Bratkartoffel Gemüse Pfanne mit Hähnchenbrust und Dip'),
('2026-07-21', 'mittagessen', 'Schweinefilet im Speckmantel dazu Rahmsoße und Butterspätzle'),
('2026-07-22', 'mittagessen', 'Schnitzel Baguette'),
('2026-07-23', 'mittagessen', 'Penne mit Grünen Spargel und Garnelenspieße'),
('2026-07-20', 'mittagessen_veg', 'Gemüse-Curry mit Basmatireis'),
('2026-07-21', 'mittagessen_veg', 'Spaghetti mit fruchtiger Tomatensauce und Parmesan'),
('2026-07-22', 'mittagessen_veg', 'Gebackener Feta mit griechischen Bauernsalat und Baguette'),
('2026-07-23', 'mittagessen_veg', 'Penne mit Grünen Spargel, get. Tomaten und Cherrytomaten'),
('2026-07-20', 'suppe', 'Gulaschsuppe'),
('2026-07-21', 'suppe', 'Kartoffelcremesuppe'),
('2026-07-22', 'suppe', 'Linseneintopf mit Würstchen'),
('2026-07-23', 'suppe', 'Tomatensuppe mit Croutons'),
('2026-07-24', 'suppe', 'Kürbiscremesuppe')
) as x (menu_date, section_key, name)
join d on d.name = x.name;