Files
Speisekarte/supabase/migrations/0001_init.sql
T
florian.wilde 0f20ad6527 "Bereiche" zu "Essenszeiten" umbenannt
Betrifft UI-Texte (DE/EN/PL), Route (/intern/bereiche ->
/intern/essenszeiten), Code-Kommentare, Changelog und README.
Fachlich unverändert (weiterhin die Tabelle/Komponente "sections"/
SectionManager im Code), da "Essenszeiten" den Zeitbezug des Features
klarer ausdrückt als der bisherige Sammelbegriff.
2026-07-18 08:26:15 +02:00

218 lines
7.7 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =============================================================
-- 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();
-- ---------- Essenszeiten 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/Essenszeit ----------
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');