Tactical Media Broadcast Hub — initial release
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/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
89
apps/api/migrations/006_publishing.sql
Normal file
89
apps/api/migrations/006_publishing.sql
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
-- 006: publishing pipeline (drafts -> approval -> scheduling -> publication)
|
||||
CREATE TABLE drafts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
author_user_id uuid NOT NULL REFERENCES users(id),
|
||||
post jsonb NOT NULL,
|
||||
state text NOT NULL DEFAULT 'editing'
|
||||
CHECK (state IN ('editing', 'in_review', 'approved', 'rejected', 'scheduled', 'published')),
|
||||
reviewer_user_id uuid REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE scheduled_publications (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
draft_id uuid NOT NULL REFERENCES drafts(id) ON DELETE CASCADE,
|
||||
connection_id uuid NOT NULL REFERENCES connections(id),
|
||||
publish_at timestamptz NOT NULL,
|
||||
idempotency_key text NOT NULL UNIQUE,
|
||||
state text NOT NULL DEFAULT 'pending'
|
||||
CHECK (state IN ('pending', 'in_flight', 'done', 'failed')),
|
||||
error text,
|
||||
UNIQUE (draft_id)
|
||||
);
|
||||
|
||||
CREATE TABLE publications (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
draft_id uuid NOT NULL REFERENCES drafts(id) ON DELETE CASCADE,
|
||||
connection_id uuid NOT NULL REFERENCES connections(id),
|
||||
idempotency_key text NOT NULL UNIQUE,
|
||||
remote_id text NOT NULL,
|
||||
remote_url text,
|
||||
published_at timestamptz NOT NULL,
|
||||
provider_metadata jsonb NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX drafts_workspace_state ON drafts (workspace_id, state);
|
||||
CREATE INDEX scheduled_publications_due ON scheduled_publications (publish_at)
|
||||
WHERE state = 'pending';
|
||||
CREATE INDEX publications_draft ON publications (draft_id);
|
||||
|
||||
CREATE TRIGGER trg_draft_ws_immutable BEFORE UPDATE ON drafts
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
||||
CREATE TRIGGER trg_scheduled_pub_ws_immutable BEFORE UPDATE ON scheduled_publications
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
||||
CREATE TRIGGER trg_publication_ws_immutable BEFORE UPDATE ON publications
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
||||
|
||||
ALTER TABLE drafts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE scheduled_publications ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE publications ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE drafts FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE scheduled_publications FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE publications FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY tenant_isolation ON drafts
|
||||
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
||||
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
||||
CREATE POLICY tenant_isolation ON scheduled_publications
|
||||
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
||||
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
||||
CREATE POLICY tenant_isolation ON publications
|
||||
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 drafts, scheduled_publications, publications TO sp_app;
|
||||
|
||||
-- Narrow SECURITY DEFINER lookups let the scheduler discover tenant context
|
||||
-- without weakening FORCE RLS for ordinary table access.
|
||||
CREATE FUNCTION publishing_schedule_workspace(p_idempotency_key text) RETURNS uuid
|
||||
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT workspace_id
|
||||
FROM scheduled_publications
|
||||
WHERE idempotency_key = p_idempotency_key
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION publishing_due_schedules() RETURNS TABLE(idempotency_key text, publish_at timestamptz)
|
||||
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT sp.idempotency_key, sp.publish_at
|
||||
FROM scheduled_publications sp
|
||||
WHERE sp.state = 'pending' AND sp.publish_at <= now()
|
||||
ORDER BY sp.publish_at
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION publishing_schedule_workspace(text) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION publishing_due_schedules() FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION publishing_schedule_workspace(text), publishing_due_schedules() TO sp_app;
|
||||
Loading…
Add table
Add a link
Reference in a new issue