Files
archivdms/internal/storage/migrations/009_shares.sql
T
patrick 9a24ea29e1 FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
2026-08-11 21:27:53 +02:00

54 lines
2.8 KiB
SQL

-- 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);