Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
54 lines
2.8 KiB
SQL
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);
|