feat: anonymous group submission boxes — public intake, CC licence, moderation
WS0 of the video-producer plan: every group can open a public, accountless upload box (random token, rotatable, disable switch). Submitters tick the Creative Commons qualifier; credit name and a licensing contact are optional free text — contact is visible only in the moderation surface and never copied onto assets. Intake sniffs magic bytes (mp4/webm/jpeg/png), caps video 200 MiB / photo 50 MiB, rate-limits 5/hour per salted IP hash (hash never rendered), dedups by sha256, caps 5 pending per box, and quarantines everything as pending. Group managers approve (video → video library via the register+probe path, photo → group asset) or reject with a note; every step audited. Migration 035 (RLS + submission_box_lookup/submission_rate_count definer fns). 4 unit + 4 live-DB tests.
This commit is contained in:
parent
1069f20c54
commit
599a68cc41
8 changed files with 793 additions and 1 deletions
66
apps/api/migrations/035_submissions.sql
Normal file
66
apps/api/migrations/035_submissions.sql
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
-- Anonymous group submission boxes (owner request): a public, accountless
|
||||
-- intake for footage/photos. Submitters tick a Creative Commons qualifier;
|
||||
-- credit name and a licensing contact are OPTIONAL free text. Submissions are
|
||||
-- quarantined (pending) until a workspace group manager approves them.
|
||||
|
||||
CREATE TABLE submission_boxes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
|
||||
group_id uuid NOT NULL,
|
||||
public_token text NOT NULL UNIQUE,
|
||||
license_default text NOT NULL DEFAULT 'CC BY-SA 4.0',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (group_id, workspace_id),
|
||||
UNIQUE (id, workspace_id),
|
||||
FOREIGN KEY (group_id, workspace_id) REFERENCES groups (id, workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_isolation ON submission_boxes
|
||||
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
||||
|
||||
CREATE TABLE submissions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
|
||||
box_id uuid NOT NULL,
|
||||
group_id uuid NOT NULL,
|
||||
kind text NOT NULL CHECK (kind IN ('video', 'photo')),
|
||||
storage_key text NOT NULL,
|
||||
mime text NOT NULL,
|
||||
size_bytes bigint NOT NULL CHECK (size_bytes > 0),
|
||||
sha256 text NOT NULL,
|
||||
license text NOT NULL,
|
||||
credit_name text CHECK (credit_name IS NULL OR (btrim(credit_name) <> '' AND length(credit_name) <= 100)),
|
||||
contact text CHECK (contact IS NULL OR (btrim(contact) <> '' AND length(contact) <= 200)),
|
||||
state text NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'approved', 'rejected')),
|
||||
rejection_note text CHECK (rejection_note IS NULL OR length(rejection_note) <= 500),
|
||||
submitter_ip_hash text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
decided_at timestamptz,
|
||||
decided_by uuid REFERENCES users (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (box_id, workspace_id) REFERENCES submission_boxes (id, workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_isolation ON submissions
|
||||
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
||||
|
||||
CREATE INDEX submissions_box_state ON submissions (box_id, state, created_at);
|
||||
CREATE INDEX submissions_rate ON submissions (submitter_ip_hash, created_at) WHERE submitter_ip_hash IS NOT NULL;
|
||||
|
||||
-- The public submit route is unauthenticated: the box lookup runs before any
|
||||
-- tenant context exists, so it goes through a definer-rights function (the
|
||||
-- audit_append / oidc_refresh_lookup pattern).
|
||||
CREATE FUNCTION submission_box_lookup(p_token text) RETURNS TABLE (
|
||||
id uuid, workspace_id uuid, group_id uuid, license_default text, enabled boolean
|
||||
) LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT id, workspace_id, group_id, license_default, enabled
|
||||
FROM submission_boxes
|
||||
WHERE public_token = p_token
|
||||
$$;
|
||||
|
||||
-- Rate limiting needs the workspace of an ip-hash without a tenant context.
|
||||
CREATE FUNCTION submission_rate_count(p_ip_hash text, p_since timestamptz) RETURNS integer
|
||||
LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT count(*)::integer FROM submissions
|
||||
WHERE submitter_ip_hash = p_ip_hash AND created_at > p_since
|
||||
$$;
|
||||
Loading…
Add table
Add a link
Reference in a new issue