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
80
apps/api/migrations/010_audit_hash_v2.sql
Normal file
80
apps/api/migrations/010_audit_hash_v2.sql
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
-- 010: hash the audit diff and serialize appends to each chain.
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE audit_log ADD COLUMN hash_version smallint NOT NULL DEFAULT 1
|
||||
CHECK (hash_version IN (1, 2));
|
||||
|
||||
CREATE FUNCTION audit_hash_v2(p_prev text, p_actor uuid, p_action text, p_target_id text,
|
||||
p_at timestamptz, p_diff jsonb)
|
||||
RETURNS text LANGUAGE sql IMMUTABLE AS $$
|
||||
SELECT encode(digest(
|
||||
coalesce(p_prev, '') || '|' || coalesce(p_actor::text, '') || '|' ||
|
||||
p_action || '|' || p_target_id || '|' || extract(epoch FROM p_at)::text || '|' ||
|
||||
coalesce(p_diff::text, ''),
|
||||
'sha256'), 'hex')
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_append(
|
||||
p_workspace_id uuid, p_actor_user_id uuid, p_actor_role text, p_action text,
|
||||
p_target_type text, p_target_id text, p_diff jsonb DEFAULT NULL
|
||||
) RETURNS uuid
|
||||
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
||||
DECLARE
|
||||
v_id uuid; v_prev text; v_hash text; v_now timestamptz := clock_timestamp();
|
||||
BEGIN
|
||||
PERFORM fn_assert_tenant(p_workspace_id);
|
||||
PERFORM pg_advisory_xact_lock(hashtext('audit:' || p_workspace_id::text));
|
||||
SELECT hash INTO v_prev FROM audit_log
|
||||
WHERE workspace_id = p_workspace_id ORDER BY seq DESC LIMIT 1;
|
||||
v_hash := audit_hash_v2(v_prev, p_actor_user_id, p_action, p_target_id, v_now, p_diff);
|
||||
INSERT INTO audit_log (workspace_id, actor_user_id, actor_role, action, target_type, target_id,
|
||||
diff, at, prev_hash, hash, hash_version)
|
||||
VALUES (p_workspace_id, p_actor_user_id, p_actor_role, p_action, p_target_type, p_target_id,
|
||||
p_diff, v_now, v_prev, v_hash, 2)
|
||||
RETURNING id INTO v_id;
|
||||
RETURN v_id;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_append_system(
|
||||
p_actor_user_id uuid, p_action text, p_target_type text, p_target_id text,
|
||||
p_diff jsonb DEFAULT NULL
|
||||
) RETURNS uuid
|
||||
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
||||
DECLARE
|
||||
v_id uuid; v_prev text; v_hash text; v_now timestamptz := clock_timestamp();
|
||||
BEGIN
|
||||
IF coalesce(current_setting('app.workspace_id', true), '') <> '' THEN
|
||||
RAISE EXCEPTION 'system append must not run inside a tenant context (use audit_append)';
|
||||
END IF;
|
||||
PERFORM pg_advisory_xact_lock(hashtext('audit:system'));
|
||||
SELECT hash INTO v_prev FROM audit_log
|
||||
WHERE workspace_id IS NULL ORDER BY seq DESC LIMIT 1;
|
||||
v_hash := audit_hash_v2(v_prev, p_actor_user_id, p_action, p_target_id, v_now, p_diff);
|
||||
INSERT INTO audit_log (workspace_id, actor_user_id, action, target_type, target_id,
|
||||
diff, at, prev_hash, hash, hash_version)
|
||||
VALUES (NULL, p_actor_user_id, p_action, p_target_type, p_target_id,
|
||||
p_diff, v_now, v_prev, v_hash, 2)
|
||||
RETURNING id INTO v_id;
|
||||
RETURN v_id;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_verify_chain(p_workspace_id uuid DEFAULT NULL) RETURNS boolean
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
DECLARE r record; v_prev text := NULL; v_expected text;
|
||||
BEGIN
|
||||
FOR r IN SELECT * FROM audit_log
|
||||
WHERE workspace_id IS NOT DISTINCT FROM p_workspace_id
|
||||
ORDER BY seq ASC LOOP
|
||||
IF r.prev_hash IS DISTINCT FROM v_prev THEN RETURN false; END IF;
|
||||
IF r.hash_version = 1 THEN
|
||||
v_expected := audit_hash(r.prev_hash, r.actor_user_id, r.action, r.target_id, r.at);
|
||||
ELSE
|
||||
v_expected := audit_hash_v2(r.prev_hash, r.actor_user_id, r.action, r.target_id, r.at, r.diff);
|
||||
END IF;
|
||||
IF v_expected IS DISTINCT FROM r.hash THEN RETURN false; END IF;
|
||||
v_prev := r.hash;
|
||||
END LOOP;
|
||||
RETURN true;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Add a link
Reference in a new issue