Files
Speisekarte/db/migrations/0001_init.sql
T
florian.wilde 28c9ec15c8 v1.5: Selbst gehostete Auth (Passwort + Passkeys), Postgres statt Supabase
Fundament: db/migrations/0001_init.sql (users/sessions/passkey_credentials/
webauthn_challenges + Inhaltstabellen ohne RLS), db/migrate.mjs
(idempotenter Migrationsrunner), lib/db (pg-Pool + query-Wrapper),
lib/auth/password.ts (Node-crypto.scrypt, keine neue Abhängigkeit),
lib/auth/session.ts (opake DB-Session-Tokens, Sliding-Refresh in proxy.ts).

Passkeys: @simplewebauthn/server + /browser ersetzen Supabase's
experimentelles WebAuthn-API (dafür gab es bisher kein eigenes
Credential-Schema); RP-Konfiguration kommt jetzt direkt aus APP_URL statt
über scripts/configure-passkeys.mjs und die Supabase-Management-API.

scripts/bootstrap-admin.mjs legt bei leerer users-Tabelle automatisch
einen Admin mit zufälligem Passwort an und hängt die Zugangsdaten an
ZUGANGSDATEN.md an (idempotent, via predev/prestart-Hooks aufgerufen).

Bugfix im Zuge des Umbaus: "mind. 1 Passkey nötig, um Passwort-Login zu
deaktivieren" wurde bisher nur clientseitig geprüft – jetzt zusätzlich
serverseitig in PATCH /api/account.

E-Mail-Änderung im eigenen Konto läuft jetzt direkt nach
Passwort-Bestätigung statt über eine Supabase-Bestätigungsmail (kein
SMTP-Server vorhanden).

Rest der Anwendung (Gerichte/Essenszeiten/Kennzeichnung/Vorschläge/
Benutzerverwaltung/Branding) nutzt noch den alten Supabase-Client –
folgt in weiteren Commits.
2026-07-18 10:42:21 +02:00

157 lines
5.1 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.
-- =============================================================
-- Canteen Menu – Initiales Schema (selbst gehostet, ohne Supabase)
-- Rollen: admin (Benutzerverwaltung, Branding) / staff (Kantine)
-- Öffentliche Speisekarte: lesbar ohne Login
-- =============================================================
-- ---------- Benutzer & Authentifizierung ----------
create table users (
id uuid primary key default gen_random_uuid(),
email text not null unique,
password_hash text not null,
full_name text not null default '',
role text not null default 'staff' check (role in ('admin', 'staff')),
password_auth_enabled boolean not null default true,
created_at timestamptz not null default now()
);
-- Angemeldete Sitzungen. Es wird nur der Hash des Tokens gespeichert –
-- der Rohwert im Cookie ist damit auch bei einem DB-Zugriff allein nicht
-- verwendbar.
create table sessions (
token_hash text primary key,
user_id uuid not null references users (id) on delete cascade,
created_at timestamptz not null default now(),
expires_at timestamptz not null,
last_seen_at timestamptz not null default now()
);
create index sessions_user_id_idx on sessions (user_id);
-- WebAuthn-Credentials (ersetzt Supabase's internes, für die App bisher
-- unsichtbares Passkey-Schema).
create table passkey_credentials (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users (id) on delete cascade,
credential_id text not null unique,
public_key bytea not null,
counter bigint not null default 0,
transports text[] not null default '{}',
name text,
created_at timestamptz not null default now(),
last_used_at timestamptz
);
create index passkey_credentials_user_id_idx on passkey_credentials (user_id);
-- Kurzlebige Challenges für laufende WebAuthn-Zeremonien (Registrierung
-- oder Login). user_id ist beim Login-Flow anfangs null (Discoverable-
-- Credential-Ansatz: die Identität steht erst nach der Antwort fest).
create table webauthn_challenges (
id uuid primary key default gen_random_uuid(),
user_id uuid references users (id) on delete cascade,
challenge text not null,
type text not null check (type in ('registration', 'authentication')),
expires_at timestamptz not null,
created_at timestamptz not null default now()
);
-- ---------- Essenszeiten der Speisekarte ----------
create table sections (
key text primary key default (gen_random_uuid())::text,
label text not null,
label_en text,
label_pl text,
time_range text not null,
sort int not null
);
-- ---------- Allergene & Zusatzstoffe ----------
create table allergens (
code text primary key,
name text not null,
name_en text,
name_pl text,
sort int not null default 0
);
create table additives (
code text primary key,
name text not null,
name_en text,
name_pl text,
sort int not null default 0
);
-- ---------- Gerichte ----------
create table dishes (
id uuid primary key default gen_random_uuid(),
name text not null,
name_en text,
name_pl text,
price numeric(6, 2) not null default 0,
allergens text[] not null default '{}',
additives text[] not null default '{}',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create or replace function 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 dishes
for each row execute function touch_updated_at();
-- ---------- Zuweisung Gericht -> Tag/Essenszeit ----------
create table menu_entries (
id uuid primary key default gen_random_uuid(),
menu_date date not null,
section_key text not null references sections (key) on delete cascade,
dish_id uuid not null references 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 menu_entries (menu_date);
-- ---------- Einstellungen (Farben, Logo, Favicon) – genau eine Zeile ----------
create table settings (
id int primary key check (id = 1),
colors jsonb not null default '{}'::jsonb,
logo_path text,
favicon_path text,
updated_at timestamptz not null default now()
);
insert into 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 settings
for each row execute function touch_updated_at();
-- ---------- Rezeptvorschläge ----------
create table dish_suggestions (
id uuid primary key default gen_random_uuid(),
name text not null,
description text,
created_at timestamptz not null default now()
);