-- E-Mail-Versand, Passwort-Rücksetzung und Teilen-Links ------------------- -- Alles vorwärts und idempotent, wie die übrigen Migrationen. -- SMTP-Zugang. Das Passwort wird wie die übrigen Geheimnisse AES-256-GCM -- verschlüsselt abgelegt (src/lib/crypto.ts) — nie im Klartext. ALTER TABLE settings ADD COLUMN IF NOT EXISTS smtp_host text; ALTER TABLE settings ADD COLUMN IF NOT EXISTS smtp_port integer NOT NULL DEFAULT 465; ALTER TABLE settings ADD COLUMN IF NOT EXISTS smtp_secure boolean NOT NULL DEFAULT true; ALTER TABLE settings ADD COLUMN IF NOT EXISTS smtp_user text; ALTER TABLE settings ADD COLUMN IF NOT EXISTS smtp_password text; ALTER TABLE settings ADD COLUMN IF NOT EXISTS smtp_from text; -- Wie lange ein neuer Teilen-Link standardmäßig gilt (0 = unbegrenzt). ALTER TABLE settings ADD COLUMN IF NOT EXISTS share_default_days integer NOT NULL DEFAULT 30; -- E-Mail am Nutzerkonto: für „Passwort vergessen" und optionale Hinweise. ALTER TABLE users ADD COLUMN IF NOT EXISTS email text; ALTER TABLE users ADD COLUMN IF NOT EXISTS notify_jobs boolean NOT NULL DEFAULT false; CREATE UNIQUE INDEX IF NOT EXISTS users_email_idx ON users (lower(email)) WHERE email IS NOT NULL; -- Passwort zurücksetzen. Gespeichert wird nur der **Hash** des Tokens — wer die -- Datenbank liest, kann damit kein Konto übernehmen. CREATE TABLE IF NOT EXISTS password_resets ( token_hash text PRIMARY KEY, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at timestamptz NOT NULL, used_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS password_resets_user_idx ON password_resets(user_id); -- Teilen-Links: ein Bild oder eine Sammlung, öffentlich erreichbar unter /s/. CREATE TABLE IF NOT EXISTS shares ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), slug text UNIQUE NOT NULL, kind text NOT NULL DEFAULT 'item' CHECK (kind IN ('item','collection')), title text, note text, created_by uuid REFERENCES users(id) ON DELETE SET NULL, expires_at timestamptz, revoked_at timestamptz, allow_download boolean NOT NULL DEFAULT true, -- Optionales Zugangswort (argon2), damit ein weitergeleiteter Link allein nicht reicht. password_hash text, views integer NOT NULL DEFAULT 0, downloads integer NOT NULL DEFAULT 0, last_seen_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS shares_created_by_idx ON shares(created_by); CREATE TABLE IF NOT EXISTS share_items ( share_id uuid NOT NULL REFERENCES shares(id) ON DELETE CASCADE, item_id uuid NOT NULL REFERENCES items(id) ON DELETE CASCADE, position integer NOT NULL DEFAULT 0, PRIMARY KEY (share_id, item_id) ); CREATE INDEX IF NOT EXISTS share_items_item_idx ON share_items(item_id); -- Zugriffe je Link. Die IP wird nur **gehasht** abgelegt (mit dem Server-Geheimnis -- gesalzen): Man kann damit wiederkehrende Besucher zählen, aber niemanden -- identifizieren oder die Adresse zurückrechnen. CREATE TABLE IF NOT EXISTS share_views ( id bigserial PRIMARY KEY, share_id uuid NOT NULL REFERENCES shares(id) ON DELETE CASCADE, kind text NOT NULL DEFAULT 'view' CHECK (kind IN ('view','download')), ip_hash text, user_agent text, at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS share_views_share_idx ON share_views(share_id, at DESC);