-- 988_dmca_notices.sql -- v1.0.9 W3 Day 14 — DMCA notice handler + workflow. -- -- Creates the table that holds incoming DMCA takedown requests + the -- admin's response trail, and adds a `dmca_blocked` flag on tracks -- so a takedown is enforced both at the visibility layer (is_public) -- AND at the playback layer (dmca_blocked stops authenticated owners -- from playing back too). -- -- Workflow states : -- pending — submitted by claimant, waiting for admin review -- takedown — admin honored the takedown ; track is_public=false + dmca_blocked=true -- dismissed — admin rejected the notice (insufficient grounds, fraud, ...) -- counter_notice — uploader filed a counter-notice ; tracking only, no auto-restore -- restored — admin reviewed the counter-notice and restored the track -- -- audit_log : JSONB array of {ts, actor_user_id, action, note} entries, -- appended on every state change. Lets us reconstruct the trail without -- joining audit_logs (which has cardinality issues on hot queries). CREATE TABLE IF NOT EXISTS public.dmca_notices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), status VARCHAR(32) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'takedown', 'dismissed', 'counter_notice', 'restored')), -- Claimant identity (required by 17 USC § 512(c)(3) — DMCA safe harbor). claimant_email VARCHAR(255) NOT NULL, claimant_name VARCHAR(255) NOT NULL, claimant_address TEXT NOT NULL, work_description TEXT NOT NULL, -- The allegedly infringing track. ON DELETE SET NULL — keep the -- record if the track is deleted later (audit trail must persist). infringing_track_id UUID REFERENCES public.tracks(id) ON DELETE SET NULL, -- "Under penalty of perjury" statement — the claimant must check -- a box; we record the timestamp it was acknowledged. Required by -- DMCA § 512(c)(3)(A)(vi). sworn_statement_at TIMESTAMPTZ NOT NULL, -- Admin action trail. takedown_at TIMESTAMPTZ, counter_notice_at TIMESTAMPTZ, restored_at TIMESTAMPTZ, -- Free-form audit trail. Each entry : {ts, actor_user_id, action, note}. audit_log JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Pending queue index — admin lists pending notices oldest-first. CREATE INDEX IF NOT EXISTS idx_dmca_notices_pending ON public.dmca_notices(created_at ASC) WHERE status = 'pending'; -- Lookup by track — when an admin opens a track they should see if any -- DMCA history exists. CREATE INDEX IF NOT EXISTS idx_dmca_notices_track ON public.dmca_notices(infringing_track_id) WHERE infringing_track_id IS NOT NULL; COMMENT ON TABLE public.dmca_notices IS 'DMCA takedown requests + admin response trail. v1.0.9 W3 Day 14.'; COMMENT ON COLUMN public.dmca_notices.audit_log IS 'JSONB array of {ts, actor_user_id, action, note} appended on each state change.'; -- --------------------------------------------------------------------- -- Track flag : dmca_blocked. When TRUE, no playback path serves the -- track regardless of `is_public`. The visibility flag (is_public) -- gets flipped too, but `dmca_blocked` is the authoritative gate -- because an owner can re-public their own track in the UI ; only -- a `restored_at` action on the notice clears the block. -- --------------------------------------------------------------------- ALTER TABLE public.tracks ADD COLUMN IF NOT EXISTS dmca_blocked BOOLEAN NOT NULL DEFAULT FALSE; COMMENT ON COLUMN public.tracks.dmca_blocked IS 'TRUE when a DMCA takedown is in force ; gates playback regardless of is_public. v1.0.9 W3 Day 14.'; -- updated_at maintenance trigger, mirroring the pattern used elsewhere. CREATE OR REPLACE FUNCTION dmca_notices_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS dmca_notices_updated_at_trg ON public.dmca_notices; CREATE TRIGGER dmca_notices_updated_at_trg BEFORE UPDATE ON public.dmca_notices FOR EACH ROW EXECUTE FUNCTION dmca_notices_updated_at();