Supabase: Schema, RLS-Policies und Seed-Daten
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
// Führt eine SQL-Datei gegen das Supabase-Projekt aus (Management API).
|
||||
// Aufruf: SUPABASE_ACCESS_TOKEN=... node scripts/db-apply.mjs <pfad-zur-sql-datei>
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const PROJECT_REF = "hvzikwjoyhoakedlywhj";
|
||||
const token = process.env.SUPABASE_ACCESS_TOKEN;
|
||||
const file = process.argv[2];
|
||||
|
||||
if (!token || !file) {
|
||||
console.error("Nutzung: SUPABASE_ACCESS_TOKEN=... node scripts/db-apply.mjs <datei.sql>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const query = readFileSync(file, "utf8");
|
||||
const res = await fetch(
|
||||
`https://api.supabase.com/v1/projects/${PROJECT_REF}/database/query`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
}
|
||||
);
|
||||
|
||||
const body = await res.text();
|
||||
if (!res.ok) {
|
||||
console.error(`Fehler ${res.status}: ${body}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("OK:", body.slice(0, 500));
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user