-- 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 $$;