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.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
-- =============================================================
|
||||
-- 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()
|
||||
);
|
||||
Reference in New Issue
Block a user