Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -254,7 +255,7 @@ await renderWithFileRoutes(<div />, {
})
```

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

Expand Down Expand Up @@ -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 `<Feature flag="flag_name">` component for conditional rendering.

Expand Down
7 changes: 5 additions & 2 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let refreshPromise: Promise<boolean> | null = null

async function attemptRefresh(): Promise<boolean> {
try {
const res = await fetch(`${baseUrl}/auth/refresh`, {
const res = await fetch(`${baseUrl}/v1/auth/refresh`, {
method: "POST",
credentials: "include",
})
Expand All @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions src/api/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })),
]
4 changes: 2 additions & 2 deletions src/lib/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/lib/feature-flags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function FeatureFlagProvider({
queryKey: ["feature-flags"],
queryFn: async () => {
try {
return await api.get("flags").json<FlagMap>()
return await api.get("v1/flags").json<FlagMap>()
} catch {
return getEnvFlags()
}
Expand Down
Loading