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.
66 lines
3.2 KiB
PL/PgSQL
66 lines
3.2 KiB
PL/PgSQL
-- 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
|
|
$$;
|