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>
187 lines
7.3 KiB
TypeScript
187 lines
7.3 KiB
TypeScript
import React from 'react';
|
|
import { Navigate, useNavigate } from 'react-router-dom';
|
|
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
|
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
|
|
import {
|
|
LazySharedPlaylistPage,
|
|
LazyLogin,
|
|
LazyRegister,
|
|
LazyForgotPassword,
|
|
LazyVerifyEmail,
|
|
LazyResetPassword,
|
|
LazyDashboard,
|
|
LazyChat,
|
|
LazyChatJoin,
|
|
LazyLibrary,
|
|
LazySettings,
|
|
LazySessions,
|
|
LazyNotFound,
|
|
LazyServerError,
|
|
LazyUserProfile,
|
|
LazyRoles,
|
|
LazyTrackDetail,
|
|
LazyPlaylistRoutes,
|
|
LazyMarketplace,
|
|
LazySearch,
|
|
LazyNotifications,
|
|
LazyAnalytics,
|
|
LazyWebhooks,
|
|
LazyAdminDashboard,
|
|
LazyAdminModeration,
|
|
LazyAdminPlatform,
|
|
LazyAdminTransfers,
|
|
LazyDesignSystemDemo,
|
|
LazySocial,
|
|
LazyFeed,
|
|
LazyDiscover,
|
|
LazySellerDashboard,
|
|
LazyWishlist,
|
|
LazyPurchases,
|
|
LazyProductDetail,
|
|
LazyCheckoutComplete,
|
|
LazyQueue,
|
|
LazyDeveloper,
|
|
LazyGear,
|
|
LazyLive,
|
|
LazyGoLive,
|
|
LazyListenTogether,
|
|
LazyCloud,
|
|
LazySubscription,
|
|
LazyDistribution,
|
|
LazyEducation,
|
|
LazySupport,
|
|
LazyLanding,
|
|
LazyDmca,
|
|
LazyDmcaNotice,
|
|
} from '@/components/ui/LazyComponent';
|
|
const LazyPrototype = React.lazy(() => import('@/features/prototype/PrototypePage'));
|
|
import { PublicRoute } from './PublicRoute';
|
|
import { ProtectedLayoutRoute } from './ProtectedLayoutRoute';
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
|
import type { RouteEntry } from './types';
|
|
|
|
/** Redirects /profile to /u/<current_username> */
|
|
function ProfileRedirect() {
|
|
const { data: user } = useUser();
|
|
const navigate = useNavigate();
|
|
React.useEffect(() => {
|
|
if (user?.username) {
|
|
navigate(`/u/${user.username}`, { replace: true });
|
|
}
|
|
}, [user?.username, navigate]);
|
|
return null;
|
|
}
|
|
|
|
function wrapPublic(element: React.ReactNode): React.ReactNode {
|
|
return (
|
|
<PublicRoute>
|
|
<ErrorBoundary>{element}</ErrorBoundary>
|
|
</PublicRoute>
|
|
);
|
|
}
|
|
|
|
function wrapProtected(element: React.ReactNode): React.ReactNode {
|
|
return (
|
|
<ProtectedRoute>
|
|
<ProtectedLayoutRoute>
|
|
<ErrorBoundary>{element}</ErrorBoundary>
|
|
</ProtectedLayoutRoute>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|
|
|
|
function wrapAdminProtected(element: React.ReactNode): React.ReactNode {
|
|
return (
|
|
<ProtectedRoute requireAdmin>
|
|
<ProtectedLayoutRoute>
|
|
<ErrorBoundary>{element}</ErrorBoundary>
|
|
</ProtectedLayoutRoute>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|
|
|
|
export function getPublicRoutes(): RouteEntry[] {
|
|
return [
|
|
{ path: '/login', element: wrapPublic(<LazyLogin />) },
|
|
{ path: '/register', element: wrapPublic(<LazyRegister />) },
|
|
{ path: '/forgot-password', element: wrapPublic(<LazyForgotPassword />) },
|
|
{ path: '/verify-email', element: wrapPublic(<LazyVerifyEmail />) },
|
|
{ path: '/reset-password', element: wrapPublic(<LazyResetPassword />) },
|
|
];
|
|
}
|
|
|
|
export function getPublicStandaloneRoutes(): RouteEntry[] {
|
|
return [
|
|
{ path: '/launch', element: <ErrorBoundary><LazyLanding /></ErrorBoundary> },
|
|
{ path: '/design-system', element: <ErrorBoundary><LazyDesignSystemDemo /></ErrorBoundary> },
|
|
{ path: '/prototype/*', element: <ErrorBoundary><React.Suspense fallback={null}><LazyPrototype /></React.Suspense></ErrorBoundary> },
|
|
{ path: '/u/:username', element: <ErrorBoundary><LazyUserProfile /></ErrorBoundary> },
|
|
{ path: '/playlists/shared/:token', element: <ErrorBoundary><LazySharedPlaylistPage /></ErrorBoundary> },
|
|
{ path: '/legal/dmca', element: <ErrorBoundary><LazyDmca /></ErrorBoundary> },
|
|
{ path: '/legal/dmca/notice', element: <ErrorBoundary><LazyDmcaNotice /></ErrorBoundary> },
|
|
];
|
|
}
|
|
|
|
export function getProtectedRoutes(): RouteEntry[] {
|
|
return [
|
|
{ path: '/dashboard', element: wrapProtected(<LazyDashboard />) },
|
|
{ path: '/marketplace', element: wrapProtected(<LazyMarketplace />) },
|
|
{ path: '/marketplace/products/:id', element: wrapProtected(<LazyProductDetail />) },
|
|
{ path: '/sell', element: wrapProtected(<LazySellerDashboard onCreateProduct={() => {}} />) },
|
|
{ path: '/wishlist', element: wrapProtected(<LazyWishlist />) },
|
|
{ path: '/purchases', element: wrapProtected(<LazyPurchases />) },
|
|
{ path: '/checkout/complete', element: wrapProtected(<LazyCheckoutComplete />) },
|
|
{ path: '/chat/join/:token', element: wrapProtected(<LazyChatJoin />) },
|
|
{ path: '/chat', element: wrapProtected(<LazyChat />) },
|
|
{ path: '/library', element: wrapProtected(<LazyLibrary />) },
|
|
{ path: '/profile', element: wrapProtected(<ProfileRedirect />) },
|
|
{ path: '/settings', element: wrapProtected(<LazySettings />) },
|
|
{ path: '/settings/sessions', element: wrapProtected(<LazySessions />) },
|
|
{ path: '/admin/roles', element: wrapAdminProtected(<LazyRoles />) },
|
|
{ path: '/tracks/:id', element: wrapProtected(<LazyTrackDetail />) },
|
|
{ path: '/playlists/*', element: wrapProtected(<LazyPlaylistRoutes />) },
|
|
{ path: '/search', element: wrapProtected(<LazySearch />) },
|
|
{ path: '/notifications', element: wrapProtected(<LazyNotifications />) },
|
|
{ path: '/analytics', element: wrapProtected(<LazyAnalytics />) },
|
|
{ path: '/webhooks', element: wrapProtected(<LazyWebhooks />) },
|
|
{ path: '/admin', element: wrapAdminProtected(<LazyAdminDashboard />) },
|
|
{ path: '/admin/moderation', element: wrapAdminProtected(<LazyAdminModeration />) },
|
|
{ path: '/admin/platform', element: wrapAdminProtected(<LazyAdminPlatform />) },
|
|
{ path: '/admin/transfers', element: wrapAdminProtected(<LazyAdminTransfers />) },
|
|
{ path: '/social', element: wrapProtected(<LazySocial />) },
|
|
{ path: '/feed', element: wrapProtected(<LazyFeed />) },
|
|
{ path: '/discover', element: wrapProtected(<LazyDiscover />) },
|
|
{ path: '/queue', element: wrapProtected(<LazyQueue />) },
|
|
{ path: '/developer', element: wrapProtected(<LazyDeveloper />) },
|
|
// Gear: connected to backend inventory API
|
|
{ path: '/gear', element: wrapProtected(<LazyGear />) },
|
|
// Live: connected to backend live streams API
|
|
{ path: '/live/go-live', element: wrapProtected(<LazyGoLive />) },
|
|
{ path: '/live', element: wrapProtected(<LazyLive />) },
|
|
// Co-listening (v0.10.7 F481)
|
|
{ path: '/listen-together/:sessionId', element: wrapProtected(<LazyListenTogether />) },
|
|
// Cloud: connected to backend cloud storage API
|
|
{ path: '/cloud', element: wrapProtected(<LazyCloud />) },
|
|
// v0.12.1: Subscription Plans & Management
|
|
{ path: '/subscription', element: wrapProtected(<LazySubscription />) },
|
|
// v0.12.2: Distribution to External Platforms
|
|
{ path: '/distribution', element: wrapProtected(<LazyDistribution />) },
|
|
// v0.12.3: Formation & Éducation
|
|
{ path: '/education', element: wrapProtected(<LazyEducation />) },
|
|
// v0.13.5 TASK-MKT-004: Support page
|
|
{ path: '/support', element: wrapProtected(<LazySupport />) },
|
|
|
|
// Redirect aliases — intuitive URLs that map to actual routes
|
|
{ path: '/tracks', element: <Navigate to="/library" replace /> },
|
|
{ path: '/community', element: <Navigate to="/social" replace /> },
|
|
{ path: '/favorites', element: <Navigate to="/playlists/favoris" replace /> },
|
|
{ path: '/home', element: <Navigate to="/dashboard" replace /> },
|
|
];
|
|
}
|
|
|
|
export function getErrorRoutes(): RouteEntry[] {
|
|
return [
|
|
{ path: '/404', element: <ErrorBoundary><LazyNotFound /></ErrorBoundary> },
|
|
{ path: '/500', element: <ErrorBoundary><LazyServerError /></ErrorBoundary> },
|
|
];
|
|
}
|