From c6b23de481c9f754cc22b2fd804f55d8797e8c71 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:19:11 +0100 Subject: [PATCH] feat(api): add `TagService` and enhance API error handling Introduce `TagService` to manage tag-related API interactions. Add SSR cookie interceptor for API requests and implement token refresh logic on 401 errors. Update `FavoriteService` to use `favoritesOnly` filter for exploring content. --- frontend/src/lib/api.ts | 42 +++++++++++++++++++++-- frontend/src/services/favorite.service.ts | 12 +++++-- frontend/src/services/tag.service.ts | 16 +++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 frontend/src/services/tag.service.ts diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 2b4a899..83e5f1e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -8,6 +8,23 @@ const api = axios.create({ }, }); +// Interceptor for Server-Side Rendering to pass cookies +api.interceptors.request.use(async (config) => { + if (typeof window === "undefined") { + try { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + const cookieHeader = cookieStore.toString(); + if (cookieHeader) { + config.headers.Cookie = cookieHeader; + } + } catch (_error) { + // Fail silently if cookies() is not available (e.g. during build) + } + } + return config; +}); + // Système anti-spam rudimentaire pour les erreurs répétitives const errorCache = new Map(); const SPAM_THRESHOLD_MS = 2000; // 2 secondes de silence après une erreur sur le même endpoint @@ -19,14 +36,35 @@ api.interceptors.response.use( errorCache.delete(url); return response; }, - (error) => { + async (error) => { + const originalRequest = error.config; + + // Handle Token Refresh (401 Unauthorized) + if ( + error.response?.status === 401 && + !originalRequest._retry && + !originalRequest.url?.includes("/auth/refresh") && + !originalRequest.url?.includes("/auth/login") + ) { + originalRequest._retry = true; + try { + await api.post("/auth/refresh"); + return api(originalRequest); + } catch (refreshError) { + // If refresh fails, we might want to redirect to login on the client + if (typeof window !== "undefined") { + window.location.href = "/login"; + } + return Promise.reject(refreshError); + } + } + const url = error.config?.url || "unknown"; const now = Date.now(); const lastErrorTime = errorCache.get(url); if (lastErrorTime && now - lastErrorTime < SPAM_THRESHOLD_MS) { // Ignorer l'erreur si elle se produit trop rapidement (déjà signalée) - // On retourne une promesse qui ne se résout jamais ou on rejette avec une marque spéciale return new Promise(() => {}); } diff --git a/frontend/src/services/favorite.service.ts b/frontend/src/services/favorite.service.ts index 2bbc77b..99ba6f2 100644 --- a/frontend/src/services/favorite.service.ts +++ b/frontend/src/services/favorite.service.ts @@ -14,9 +14,15 @@ export const FavoriteService = { limit: number; offset: number; }): Promise> { - const { data } = await api.get>("/favorites", { - params, - }); + const { data } = await api.get>( + "/contents/explore", + { + params: { + ...params, + favoritesOnly: true, + }, + }, + ); return data; }, }; diff --git a/frontend/src/services/tag.service.ts b/frontend/src/services/tag.service.ts new file mode 100644 index 0000000..4bdb8a3 --- /dev/null +++ b/frontend/src/services/tag.service.ts @@ -0,0 +1,16 @@ +import api from "@/lib/api"; +import type { Tag } from "@/types/content"; + +export const TagService = { + async getAll( + params: { + limit?: number; + offset?: number; + query?: string; + sort?: "popular" | "recent"; + } = {}, + ): Promise { + const { data } = await api.get("/tags", { params }); + return data; + }, +};