From 59a4b850ec25445e125af71d47dc31d42a020dce Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Wed, 13 May 2026 00:41:17 -0500 Subject: [PATCH] chore: migrate API client to new /v1 route prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api-template moved every application route under /v1 (ag-tech-group/api-template#21). Infrastructure routes (/, /health, /docs, /openapi.json) stay unversioned. The refresh cookie is now path-scoped to /v1/auth/refresh, so silent-refresh requests must hit that exact path for the cookie to be sent. Updated hand-written API paths: - src/api/api.ts: silent-refresh fetches /v1/auth/refresh; afterResponse loop guard recognizes /v1/auth/refresh and /v1/auth/jwt/logout - src/lib/auth.tsx: checkAuth -> /v1/auth/me; logout -> /v1/auth/jwt/logout - src/lib/feature-flags.tsx: FeatureFlagProvider -> /v1/flags - src/api/handlers.ts: MSW default handler -> /v1/auth/me - README: corresponding path references and cookie-path note baseUrl stays the bare origin (VITE_API_URL); /v1 lives in the request paths so orval-generated hooks and hand-written ky calls share the same contract and there's no /v1/v1/... duplication when pnpm generate-api is re-run against the updated spec. No vite proxy entries to rewrite — this template uses CORS via VITE_API_URL, not a dev proxy. Closes #24 --- README.md | 11 ++++++----- src/api/api.ts | 7 +++++-- src/api/handlers.ts | 4 ++-- src/lib/auth.tsx | 4 ++-- src/lib/feature-flags.tsx | 2 +- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d88f4b3..ab78fdb 100644 --- a/README.md +++ b/README.md @@ -136,14 +136,15 @@ The template includes a complete auth setup designed to work with the companion - **AuthProvider** (`src/lib/auth.tsx`) — React context tracking `isAuthenticated`, `isLoading`, `email`, and `userId` - **Automatic token refresh** (`src/api/api.ts`) — 401 responses trigger a refresh attempt; concurrent requests are coalesced into a single refresh call -- **Session check on load** — `GET /auth/me` validates the session on mount +- **Session check on load** — `GET /v1/auth/me` validates the session on mount - **Auth in router context** — `auth` is available in route `beforeLoad` for route guards ### How it works -1. Backend sets httpOnly cookies (`app_access` + `app_refresh`) on login +1. Backend sets httpOnly cookies (`app_access` + `app_refresh`) on login; the + refresh cookie is path-scoped to `/v1/auth/refresh` 2. All API requests include cookies via `credentials: "include"` -3. On 401, the client POSTs to `/auth/refresh` to rotate tokens +3. On 401, the client POSTs to `/v1/auth/refresh` to rotate tokens 4. If refresh succeeds, the original request is retried transparently 5. If refresh fails, `onUnauthorized` fires and auth state is cleared @@ -254,7 +255,7 @@ await renderWithFileRoutes(
, { }) ``` -MSW handlers are configured in `src/api/handlers.ts`. A default handler for `/auth/me` (returns 401) is included to suppress warnings during tests. +MSW handlers are configured in `src/api/handlers.ts`. A default handler for `/v1/auth/me` (returns 401) is included to suppress warnings during tests. ## Project Structure @@ -316,7 +317,7 @@ Set `VITE_LOG_LEVEL` to control the minimum level (default: `debug` in dev, `war ### Feature Flags -`FeatureFlagProvider` fetches flags from the API's `GET /flags` endpoint (cached via TanStack Query, refetches on window focus). If the API call fails, it falls back to `VITE_FEATURE_*` env vars. +`FeatureFlagProvider` fetches flags from the API's `GET /v1/flags` endpoint (cached via TanStack Query, refetches on window focus). If the API call fails, it falls back to `VITE_FEATURE_*` env vars. Use the `useFeatureFlag("flag_name")` hook or the `` component for conditional rendering. diff --git a/src/api/api.ts b/src/api/api.ts index 6266029..23ad210 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -14,7 +14,7 @@ let refreshPromise: Promise | null = null async function attemptRefresh(): Promise { try { - const res = await fetch(`${baseUrl}/auth/refresh`, { + const res = await fetch(`${baseUrl}/v1/auth/refresh`, { method: "POST", credentials: "include", }) @@ -37,7 +37,10 @@ export const api = ky.create({ // Don't try to refresh if this IS the refresh request (prevent loop) const url = request.url - if (url.includes("/auth/refresh") || url.includes("/auth/jwt/logout")) { + if ( + url.includes("/v1/auth/refresh") || + url.includes("/v1/auth/jwt/logout") + ) { if (onUnauthorized) onUnauthorized() return } diff --git a/src/api/handlers.ts b/src/api/handlers.ts index cad6578..34ddf28 100644 --- a/src/api/handlers.ts +++ b/src/api/handlers.ts @@ -9,6 +9,6 @@ import { http, HttpResponse, type HttpHandler } from "msw" */ export const handlers: HttpHandler[] = [ - // Return 401 for /auth/me by default (unauthenticated) - http.get("*/auth/me", () => HttpResponse.json(null, { status: 401 })), + // Return 401 for /v1/auth/me by default (unauthenticated) + http.get("*/v1/auth/me", () => HttpResponse.json(null, { status: 401 })), ] diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx index 073100c..75b6141 100644 --- a/src/lib/auth.tsx +++ b/src/lib/auth.tsx @@ -45,7 +45,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const logout = useCallback(async () => { try { - await api.post("auth/jwt/logout") + await api.post("v1/auth/jwt/logout") } catch { // Clear state regardless of fetch success } @@ -61,7 +61,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const checkAuth = useCallback(async () => { try { const user = await api - .get("auth/me") + .get("v1/auth/me") .json<{ id: string; email: string; name: string | null }>() setIsAuthenticated(true) setEmail(user.email) diff --git a/src/lib/feature-flags.tsx b/src/lib/feature-flags.tsx index e50cc80..3b47282 100644 --- a/src/lib/feature-flags.tsx +++ b/src/lib/feature-flags.tsx @@ -39,7 +39,7 @@ export function FeatureFlagProvider({ queryKey: ["feature-flags"], queryFn: async () => { try { - return await api.get("flags").json() + return await api.get("v1/flags").json() } catch { return getEnvFlags() }