From 01d347bce3ad911b457fb764b4bf9e111fc52cc9 Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Sat, 14 Mar 2026 22:37:36 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20ApiError=20typ=C3=A9e=20+=20error=20hand?= =?UTF-8?q?ling=20pages=20video/playlists/admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api.ts : ApiError class (status: number) — remplace Error générique - VideoPage/PlaylistPage : instanceof ApiError au lieu de message.includes() - PlaylistsPage : fetchError + createError — silent catch supprimé - AdminPage : guard roles.some() aligné Header (super_admin inclus) --- frontend/src/lib/api.ts | 8 +++++++- frontend/src/pages/AdminPage.tsx | 2 +- frontend/src/pages/PlaylistPage.tsx | 6 +++--- frontend/src/pages/PlaylistsPage.tsx | 16 ++++++++++++++-- frontend/src/pages/VideoPage.tsx | 8 ++++---- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 017667b..639a438 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2,6 +2,12 @@ // En prod : VITE_API_URL=https://origins.tetardtek.com/api const BASE = import.meta.env.VITE_API_URL || '/api'; +export class ApiError extends Error { + constructor(public readonly status: number, path: string) { + super(`API ${status}: ${path}`); + } +} + export async function apiFetch(path: string, init?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { credentials: 'include', // transmet le cookie httpOnly automatiquement @@ -13,7 +19,7 @@ export async function apiFetch(path: string, init?: RequestInit): Promise }); if (!res.ok) { - throw new Error(`API ${res.status}: ${path}`); + throw new ApiError(res.status, path); } return res.json() as Promise; diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index eeddb74..e57c965 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -50,7 +50,7 @@ export default function AdminPage() { const [tab, setTab] = useState('videos'); if (authLoading) return null; - if (!user?.roles?.includes('admin')) return ; + if (!user?.roles?.some((r) => r === 'admin' || r === 'super_admin')) return ; return (
diff --git a/frontend/src/pages/PlaylistPage.tsx b/frontend/src/pages/PlaylistPage.tsx index 3a0e4d9..5715ae8 100644 --- a/frontend/src/pages/PlaylistPage.tsx +++ b/frontend/src/pages/PlaylistPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { apiFetch } from '../lib/api'; +import { apiFetch, ApiError } from '../lib/api'; interface Video { id: string; @@ -35,8 +35,8 @@ export default function PlaylistPage() { if (!id) return; apiFetch(`/playlists/${id}`) .then((res) => setData(res.data)) - .catch((err: Error) => { - if (err.message.includes('403')) setError('forbidden'); + .catch((err: unknown) => { + if (err instanceof ApiError && err.status === 403) setError('forbidden'); else setError('not_found'); }) .finally(() => setLoading(false)); diff --git a/frontend/src/pages/PlaylistsPage.tsx b/frontend/src/pages/PlaylistsPage.tsx index acd7dea..159f9fc 100644 --- a/frontend/src/pages/PlaylistsPage.tsx +++ b/frontend/src/pages/PlaylistsPage.tsx @@ -21,8 +21,10 @@ export default function PlaylistsPage() { const [owned, setOwned] = useState([]); const [shared, setShared] = useState<(Playlist & { permission: 'view' | 'edit' })[]>([]); const [loading, setLoading] = useState(true); + const [fetchError, setFetchError] = useState(null); const [createTitle, setCreateTitle] = useState(''); const [creating, setCreating] = useState(false); + const [createError, setCreateError] = useState(null); useEffect(() => { apiFetch('/playlists') @@ -30,7 +32,7 @@ export default function PlaylistsPage() { setOwned(res.data.owned); setShared(res.data.shared); }) - .catch(() => {}) + .catch(() => setFetchError('Impossible de charger les playlists.')) .finally(() => setLoading(false)); }, []); @@ -38,6 +40,7 @@ export default function PlaylistsPage() { e.preventDefault(); if (!createTitle.trim() || creating) return; setCreating(true); + setCreateError(null); try { const res = await apiFetch<{ success: boolean; data: { playlist: Playlist } }>( '/playlists', @@ -45,7 +48,9 @@ export default function PlaylistsPage() { ); setOwned((prev) => [res.data.playlist, ...prev]); setCreateTitle(''); - } catch {} + } catch { + setCreateError('Impossible de créer la playlist.'); + } setCreating(false); } @@ -59,6 +64,10 @@ export default function PlaylistsPage() { ); } + if (fetchError) { + return

{fetchError}

; + } + return (
@@ -67,6 +76,7 @@ export default function PlaylistsPage() { {/* Créer */} +
+ {createError &&

{createError}

} +
{/* Mes playlists */} {owned.length > 0 && ( diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index 39f9fdd..e4e6554 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { apiFetch } from '../lib/api'; +import { apiFetch, ApiError } from '../lib/api'; import VideoPlayer from '../components/VideoPlayer'; interface Video { @@ -30,9 +30,9 @@ export default function VideoPage() { if (!id) return; apiFetch(`/videos/${id}`) .then((res) => setVideo(res.data.video)) - .catch((err: Error) => { - if (err.message.includes('403')) setError('forbidden'); - else if (err.message.includes('404')) setError('not_found'); + .catch((err: unknown) => { + if (err instanceof ApiError && err.status === 403) setError('forbidden'); + else if (err instanceof ApiError && err.status === 404) setError('not_found'); else setError('unknown'); }) .finally(() => setLoading(false));