Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
64 lines
3.3 KiB
PL/PgSQL
64 lines
3.3 KiB
PL/PgSQL
-- 017: multi-use, expiring workspace invite links (Telegram-style).
|
|
-- One link, many claimants: anyone holding the token may join with the link's
|
|
-- role until the link expires, is revoked, or exhausts max_uses (null =
|
|
-- unlimited until expiry). Claimers arrive unauthenticated, so the claim path
|
|
-- is the SECURITY DEFINER analogue of login (see invite_link_for_claim below):
|
|
-- workspace_invite_links is FORCE RLS and no tenant context exists pre-join.
|
|
CREATE TABLE workspace_invite_links (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
role text NOT NULL,
|
|
token text NOT NULL UNIQUE,
|
|
created_by uuid NOT NULL REFERENCES users(id),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
expires_at timestamptz NOT NULL,
|
|
max_uses int CHECK (max_uses IS NULL OR max_uses > 0),
|
|
use_count int NOT NULL DEFAULT 0 CHECK (use_count >= 0),
|
|
revoked_at timestamptz
|
|
);
|
|
|
|
CREATE TRIGGER trg_invite_link_ws_immutable BEFORE UPDATE ON workspace_invite_links
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
|
|
-- Same fail-closed tenant isolation as every tenant table (001/006 style):
|
|
-- no app.workspace_id => NULL => no rows. FORCE binds sp_app too.
|
|
ALTER TABLE workspace_invite_links ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE workspace_invite_links FORCE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation ON workspace_invite_links
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON workspace_invite_links TO sp_app;
|
|
|
|
-- Claim runs with NO tenant context (the claimer has no membership yet), and
|
|
-- the table is FORCE RLS: ordinary SELECTs see zero rows. Like
|
|
-- auth_user_memberships (007) and publishing_schedule_workspace (006), this
|
|
-- narrow SECURITY DEFINER lookup exposes exactly one row keyed by the secret
|
|
-- token — workspace_id discovery only; the caller enters the tenant context
|
|
-- afterwards and every subsequent statement is RLS-bound as sp_app.
|
|
CREATE FUNCTION invite_link_for_claim(p_token text)
|
|
RETURNS TABLE(id uuid, workspace_id uuid, role text, expires_at timestamptz,
|
|
max_uses int, use_count int, revoked_at timestamptz)
|
|
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
|
SELECT l.id, l.workspace_id, l.role, l.expires_at, l.max_uses, l.use_count, l.revoked_at
|
|
FROM workspace_invite_links l
|
|
WHERE l.token = p_token
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION invite_link_for_claim(text) FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION invite_link_for_claim(text) TO sp_app;
|
|
|
|
-- Token lookup at claim time: point lookup served by the backing index of the
|
|
-- inline UNIQUE constraint on token (auto-named workspace_invite_links_token_key).
|
|
-- Statement for documentation/parity with 016's users_username_key — do NOT run twice:
|
|
-- CREATE UNIQUE INDEX workspace_invite_links_token_key ON workspace_invite_links (token);
|
|
|
|
-- Workspace listing (admin GET /team/invite-links), newest first:
|
|
CREATE INDEX workspace_invite_links_ws_created
|
|
ON workspace_invite_links (workspace_id, created_at DESC);
|
|
|
|
-- Expiry sweep / active-link diagnostics, mirroring the partial-index style of
|
|
-- audit_log (001) and scheduled_publications_due (006):
|
|
CREATE INDEX workspace_invite_links_expiry
|
|
ON workspace_invite_links (expires_at) WHERE revoked_at IS NULL;
|