-- PROJ: external-share-links -- Doku-only (siehe README.md in diesem Verzeichnis) — die tatsächliche -- Ausführung passiert idempotent über internal/storage/shares.go -- initSharesSchema(), aufgerufen aus (*Store).New(). -- -- Externe Share-Links: tenant-scoped, ablaufender, optional passwortgeschützter -- öffentlicher Link auf genau EIN Dokument. -- * Token: 32 Byte crypto/rand, base64url; wird beim Erzeugen genau EINMAL -- zurückgegeben. Persistiert wird NUR der SHA-256-Hash (token_hash). Der -- öffentliche Abruf sucht immer über token_hash, nie über id. -- * expires_at ist Pflicht (kein unbegrenzter Share). password_hash optional -- (bcrypt, Cost 12). -- * Kein Hard-Delete: Revoke setzt nur revoked_at/revoked_by. -- * Prüfreihenfolge beim öffentlichen Abruf: revoked -> expired -> -- max_accesses erreicht -> Passwort -> ausliefern (access_count++ atomar -- mit WHERE-Guard gegen Race). Datei wird serverseitig aus dem WORM-Store -- gestreamt; storage_path/content_hash NIE im Response. -- * Jeder Zugriffsversuch (Erfolg/Fehlschlag) landet in -- document_share_accesses mit passendem result-Wert; die öffentlichen -- Endpunkte sind zusätzlich per-IP rate-limited (Token-Bucket in-memory). -- -- Hinweis: tenant_id / created_by / revoked_by tragen bewusst KEINE FK auf -- tenants(id) bzw. users(id) — konsistent mit documents/taxonomy/permissions -- (plain BIGINT) und weil tenants/users von anderen Stores NACH storage.New() -- angelegt werden (Reihenfolge in cmd/archivdms/main.go). document_id behält -- seine FK, da documents die eigene Tabelle dieses Stores ist. CREATE TABLE IF NOT EXISTS document_shares ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, document_id BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, created_by BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL, max_accesses INT, access_count INT NOT NULL DEFAULT 0, password_hash TEXT, revoked_at TIMESTAMPTZ, revoked_by BIGINT ); CREATE INDEX IF NOT EXISTS idx_document_shares_document ON document_shares(document_id); CREATE INDEX IF NOT EXISTS idx_document_shares_tenant ON document_shares(tenant_id); CREATE TABLE IF NOT EXISTS document_share_accesses ( id BIGSERIAL PRIMARY KEY, share_id BIGINT NOT NULL REFERENCES document_shares(id) ON DELETE CASCADE, accessed_at TIMESTAMPTZ NOT NULL DEFAULT now(), ip_address INET, user_agent TEXT, result TEXT NOT NULL CHECK (result IN ('success','expired','revoked','max_reached','bad_password','rate_limited')) ); CREATE INDEX IF NOT EXISTS idx_share_accesses_share ON document_share_accesses(share_id, accessed_at DESC);