Some checks failed
Veza CI / Notify on failure (push) Blocked by required conditions
Veza CI / Rust (Stream Server) (push) Successful in 5m33s
Security Scan / Secret Scanning (gitleaks) (push) Failing after 1m0s
Veza CI / Backend (Go) (push) Failing after 9m37s
Veza CI / Frontend (Web) (push) Has been cancelled
E2E Playwright / e2e (full) (push) Has been cancelled
End-to-end DMCA workflow. Public submission, admin queue, takedown
flips track to is_public=false + dmca_blocked=true, playback paths
return 451 Unavailable For Legal Reasons.
Backend
- migrations/988_dmca_notices.sql + rollback : table dmca_notices
(id, status, claimant_*, work_description, infringing_track_id FK,
sworn_statement_at, takedown_at, counter_notice_at, restored_at,
audit_log JSONB, created_at, updated_at). Adds tracks.dmca_blocked
BOOLEAN. Partial indexes for the pending queue + per-track lookup.
Status enum constrained via CHECK.
- internal/models/dmca_notice.go + DmcaBlocked field on Track.
- internal/services/dmca_service.go : CreateNotice + ListPending +
Takedown + Dismiss. Takedown is a single transaction that flips the
track's flags AND appends an audit_log entry — partial state can't
happen if the track was deleted between fetch and update.
- internal/handlers/dmca_handler.go : POST /api/v1/dmca/notice (public),
GET /api/v1/admin/dmca/notices (paginated), POST /:id/takedown,
POST /:id/dismiss. sworn_statement=false → 400. Conflict → 409.
Track gone after notice → 410.
- internal/api/routes_legal.go : route registration. Admin chain :
RequireAuth + RequireAdmin + RequireMFA (same as moderation routes).
- internal/core/track/track_hls_handler.go : both StreamTrack +
DownloadTrack now early-return 451 when track.DmcaBlocked. Owner
cannot bypass — only an admin restoring the notice clears the gate.
- internal/services/dmca_service_test.go : audit_log append helpers,
malformed-JSON rejection, ordering preservation.
Frontend
- apps/web/src/features/legal/pages/DmcaNoticePage.tsx : public form
at /legal/dmca/notice. Validates sworn-statement checkbox client-side.
Receipt panel shows the notice ID after submission.
- apps/web/src/services/api/dmca.ts : thin client (POST /dmca/notice).
- routeConfig + lazy registry updated for the new route.
- DmcaPage now links to /legal/dmca/notice instead of saying "form
pending".
E2E
- tests/e2e/29-dmca-notice.spec.ts : 3 tests. (1) anonymous submit
yields 201 + pending receipt. (2) sworn_statement=false rejected
with 400. (3) admin takedown gates playback with 451 — gated behind
E2E_DMCA_ADMIN=1 because admin path requires MFA-bearing seed.
Acceptance (Day 14) : public submission produces a pending notice,
admin takedown blocks playback at 451. Lab-side validation pending
admin MFA seed for the e2e admin pathway.
W3 progress : Redis Sentinel ✓ · MinIO distribué ✓ · CDN ✓ · DMCA ✓ ·
embed ⏳ Day 15.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
4.2 KiB
PL/PgSQL
95 lines
4.2 KiB
PL/PgSQL
-- 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();
|