# Integration Audit: Backend ↔ Frontend **Generated**: 2025-01-27 **Scope**: veza-backend-api (Go) ↔ apps/web (React/TypeScript) **Auditor**: Cursor AI **Health Score**: 4/10 --- ## Executive Snapshot **Overall Integration Health**: 4/10 The integration surface between backend and frontend shows significant contract drift, authentication inconsistencies, and configuration mismatches. While core flows (login, basic CRUD) function in development, production deployment will expose critical failures due to CORS misconfiguration, missing CSRF protection, type mismatches, and environment variable inconsistencies. **What is working**: - Basic authentication flow (login/logout) works in dev - API client interceptors handle token refresh - Response unwrapping in `apiClient` correctly handles `{ success, data }` format - Health check endpoints accessible **What is fragile**: - CORS configuration will break in production (empty origins = reject all) - Multiple auth storage mechanisms cause token sync issues - Type mismatches (User.id: string vs number) will cause runtime errors - Environment variable naming inconsistencies (VITE_API_BASE_URL vs VITE_API_URL) - No CSRF protection for state-changing operations - Response format expectations mismatch between deprecated `ApiService` and `apiClient` - Missing error correlation IDs in frontend error boundaries --- ## Real Architecture ### Development Setup **Frontend** (`apps/web`): - Vite dev server on `http://localhost:3000` - API calls via `apiClient` (axios) to `VITE_API_URL` (default: `http://127.0.0.1:8080/api/v1`) - No proxy configuration in `vite.config.ts` (direct CORS requests) - Token storage: `localStorage` (access_token, refresh_token) **Backend** (`veza-backend-api`): - Gin server on `:8080` - Routes under `/api/v1/*` (v1 group) - CORS middleware: allows `http://localhost:3000`, `http://127.0.0.1:3000`, `http://localhost:5173`, `http://127.0.0.1:5173` (dev defaults) - Auth: JWT tokens in `Authorization: Bearer ` header ### Production Setup (Inferred) **Frontend**: - Static build served via nginx (see `apps/web/nginx.conf`) - Nginx proxies `/api/*` to `backend-api:8080` - Environment variables baked at build time (`VITE_*`) **Backend**: - CORS origins: **REQUIRED** via `CORS_ALLOWED_ORIGINS` env var - If `CORS_ALLOWED_ORIGINS` empty → **rejects all origins** (strict mode) - No proxy rewrite assumptions (routes expect `/api/v1` prefix) **Critical Gap**: Frontend expects `/api/v1` in base URL, but nginx proxy may rewrite paths. No evidence of path rewriting in nginx config. --- ## Contract Map | Frontend Module/Hook | HTTP Method + Path | Backend Handler | Request Shape | Response Shape | Status | |----------------------|-------------------|-----------------|---------------|----------------|--------| | `authApi.login()` | `POST /api/v1/auth/login` | `handlers.Login()` | `{ email, password, remember_me? }` | `{ success: true, data: { user: {...}, token: {...} } }` | ✅ Match | | `authApi.register()` | `POST /api/v1/auth/register` | `handlers.Register()` | `{ email, username, password }` | `{ success: true, data: { user: {...} } }` | ✅ Match | | `authApi.refresh()` | `POST /api/v1/auth/refresh` | `handlers.Refresh()` | `{ refresh_token }` | `{ success: true, data: { access_token, refresh_token, expires_in } }` | ✅ Match | | `authApi.getMe()` | `GET /api/v1/auth/me` | `handlers.GetMe()` | (auth header) | `{ success: true, data: { id, email, role } }` | ⚠️ Partial (missing user fields) | | `apiClient.get('/tracks')` | `GET /api/v1/tracks` | `trackHandler.ListTracks()` | Query params | `{ success: true, data: [...] }` | ✅ Match | | `apiClient.get('/tracks/:id')` | `GET /api/v1/tracks/:id` | `trackHandler.GetTrack()` | (path param) | `{ success: true, data: {...} }` | ✅ Match | | `apiClient.post('/tracks')` | `POST /api/v1/tracks` | `trackHandler.UploadTrack()` | FormData | `{ success: true, data: {...} }` | ✅ Match | | `apiClient.get('/users/:id')` | `GET /api/v1/users/:id` | `profileHandler.GetProfile()` | (path param) | `{ success: true, data: { profile: {...} } }` | ⚠️ Nested `profile` key | | `apiClient.get('/playlists')` | `GET /api/v1/playlists` | `playlistHandler.GetPlaylists()` | Query params | `{ success: true, data: [...] }` | ✅ Match | | `apiClient.post('/chat/token')` | `POST /api/v1/chat/token` | `chatHandler.GetToken()` | `{}` | `{ success: true, data: { token } }` | ✅ Match | | `apiClient.get('/conversations')` | `GET /api/v1/conversations` | `roomHandler.GetUserRooms()` | (auth header) | `{ success: true, data: [...] }` | ✅ Match | | `apiClient.post('/sessions/logout')` | `POST /api/v1/sessions/logout` | `sessionHandler.Logout()` | (auth header) | `{ success: true, data: {...} }` | ✅ Match | | `apiClient.get('/health')` | `GET /api/v1/health` | `healthHandler.Check()` | - | `{ success: true, data: {...} }` | ✅ Match | | `apiClient.get('/upload/limits')` | `GET /api/v1/upload/limits` | `uploadHandler.GetUploadLimits()` | - | `{ success: true, data: {...} }` | ✅ Match | | `apiService.login()` (deprecated) | `POST /api/v1/auth/login` | `handlers.Login()` | `{ email, password }` | Expects `{ user, token }` in `data` | ❌ Mismatch (expects flat structure) | | `apiService.getUser()` | `GET /api/v1/users/:id` | `profileHandler.GetProfile()` | (path param) | Expects `profile` or direct user | ⚠️ Handles nested but fragile | | `2fa-service.ts` | `GET /api/v1/2fa/status` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `2fa-service.ts` | `POST /api/v1/2fa/setup` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `playlistService.addCollaborator()` | `POST /api/v1/playlists/:id/collaborators` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `playlistService.removeCollaborator()` | `DELETE /api/v1/playlists/:id/collaborators/:userId` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `playlistService.updateCollaborator()` | `PUT /api/v1/playlists/:id/collaborators/:userId` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `playlistService.search()` | `GET /api/v1/playlists/search` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `playlistService.share()` | `POST /api/v1/playlists/:id/share` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `playlistService.getRecommendations()` | `GET /api/v1/playlists/recommendations` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `trackApi.getTrackDownload()` | `GET /api/v1/tracks/:id/download` | `trackHandler.DownloadTrack()` | (path param) | File stream | ✅ Match | | `trackApi.likeTrack()` | `POST /api/v1/tracks/:id/like` | `trackHandler.LikeTrack()` | (path param) | `{ success: true, data: {...} }` | ✅ Match | | `trackApi.unlikeTrack()` | `DELETE /api/v1/tracks/:id/like` | `trackHandler.UnlikeTrack()` | (path param) | `{ success: true, data: {...} }` | ✅ Match | | `trackApi.deleteTrack()` | `DELETE /api/v1/tracks/:id` | `trackHandler.DeleteTrack()` | (path param) | `{ success: true, data: {...} }` | ✅ Match | | `hlsService.getHLSInfo()` | `GET /api/v1/tracks/:id/hls/info` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `hlsService.getHLSStatus()` | `GET /api/v1/tracks/:id/hls/status` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `roleService.assignRole()` | `POST /api/v1/users/:userId/roles` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `roleService.removeRole()` | `DELETE /api/v1/users/:userId/roles/:roleId` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `roleService.updateRole()` | `PUT /api/v1/roles/:roleId` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `roleService.deleteRole()` | `DELETE /api/v1/roles/:roleId` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `profileService.getProfile()` | `GET /api/v1/users/:userId/profile` | ❌ **NOT FOUND** (uses `/users/:id` instead) | - | - | ⚠️ Path mismatch | | `profileService.updateProfile()` | `PUT /api/v1/users/:userId/profile` | ❌ **NOT FOUND** (uses `/users/:id` instead) | - | - | ⚠️ Path mismatch | | `avatarService.deleteAvatar()` | `DELETE /api/v1/users/:userId/avatar` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `settingsService.updateSettings()` | `PUT /api/v1/users/:userId/settings` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `notificationsApi.markRead()` | `POST /api/v1/notifications/:id/read` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | | `notificationsApi.markAllRead()` | `POST /api/v1/notifications/read-all` | ❌ **NOT FOUND** | - | - | ❌ Endpoint missing | **Summary**: 18 endpoints called by frontend are missing or have path mismatches. --- ## Top 5 Critical Issues (P0) ### 1. CORS Configuration Will Break Production **Severity**: P0 **Impact**: All API requests will fail in production if `CORS_ALLOWED_ORIGINS` is not set. **Evidence**: - `veza-backend-api/internal/api/router.go:119-128`: CORS middleware applied with `config.CORSOrigins` - `veza-backend-api/internal/config/config.go:638-664`: `getCORSOrigins()` returns empty list `[]` in production if `CORS_ALLOWED_ORIGINS` unset - `veza-backend-api/internal/middleware/cors.go:34-37`: Empty origins list → `isAllowedOrigin()` returns `false` → all CORS requests rejected **Root Cause**: Production defaults to strict mode (reject all) but frontend expects CORS to work. **Fix Plan**: 1. Document `CORS_ALLOWED_ORIGINS` as **REQUIRED** in production 2. Add startup validation: fail fast if `APP_ENV=production` and `CORS_ALLOWED_ORIGINS` empty 3. Update deployment docs with example: `CORS_ALLOWED_ORIGINS=https://app.veza.com,https://www.veza.com` **Tests**: E2E test with production-like CORS config. --- ### 2. Multiple Auth Storage Mechanisms Cause Token Sync Failures **Severity**: P0 **Impact**: Tokens stored in multiple places (localStorage, Zustand persist, TokenStorage) can desync, causing auth failures. **Evidence**: - `apps/web/src/services/tokenStorage.ts:35-36`: Stores in `localStorage` (keys: `veza_access_token`, `veza_refresh_token`) - `apps/web/src/services/api/client.ts:48-64`: Falls back to parsing `auth-storage` from Zustand if TokenStorage empty - `apps/web/src/stores/auth.ts`: Zustand store also manages auth state - `apps/web/src/utils/token-manager.ts:25`: Also stores in `localStorage` with different key (`veza_access_token`) **Root Cause**: No single source of truth for token storage. **Fix Plan**: 1. Standardize on `TokenStorage` class as single source 2. Remove Zustand token storage (keep only `user` and `isAuthenticated` flags) 3. Remove `token-manager.ts` or migrate to `TokenStorage` 4. Update all code to use `TokenStorage.getAccessToken()` / `TokenStorage.setTokens()` **Tests**: Test token persistence across page reloads, multiple tabs. --- ### 3. Type Mismatch: User.id (string vs number) **Severity**: P0 **Impact**: Runtime errors when accessing `user.id` in TypeScript, type coercion bugs. **Evidence**: - `apps/web/src/types/api.ts:3`: `User.id: string` ✅ - `apps/web/src/features/auth/types/index.ts`: `id: number` ❌ (inferred from usage) - `apps/web/src/services/api/auth.ts`: Uses `number` in some places - Backend: `internal/dto/user_response.go`: `ID: uuid.UUID` (serializes as string) **Root Cause**: Inconsistent type definitions across frontend modules. **Fix Plan**: 1. Audit all `User` type definitions, standardize on `id: string` 2. Update `apps/web/src/features/auth/types/index.ts` to use `string` 3. Add TypeScript strict mode checks 4. Update Zod schemas to validate UUID format **Tests**: TypeScript compilation, runtime validation. --- ### 4. Deprecated ApiService Expects Wrong Response Format **Severity**: P0 **Impact**: Code using deprecated `ApiService` will break when backend response format changes. **Evidence**: - `apps/web/src/services/api.ts:238-239`: Expects `{ user, token }` directly in `data` - Backend `handlers.Login()`: Returns `{ success: true, data: { user: {...}, token: {...} } }` ✅ - `apps/web/src/services/api.ts:74-80`: Marked as `@deprecated` but still used in some places **Root Cause**: Legacy code expects flat structure, new code expects nested. **Fix Plan**: 1. Complete migration from `ApiService` to `apiClient` (see `MIGRATION_GUIDE.md`) 2. Remove `ApiService` class entirely 3. Update all imports to use `apiClient` from `@/services/api/client` **Tests**: Verify all API calls use `apiClient`. --- ### 5. Missing CSRF Protection for State-Changing Operations **Severity**: P0 (Security) **Impact**: Vulnerable to CSRF attacks on POST/PUT/DELETE requests. **Evidence**: - `apps/web/src/services/csrf.ts:14-17`: `refreshCsrfToken()` is a placeholder (not implemented) - `apps/web/src/services/secure-auth.ts:119`: Calls `csrfService.refreshCsrfToken()` but it does nothing - Backend: No CSRF middleware found in `veza-backend-api` - `veza-chat-server/src/security/csrf.rs`: CSRF exists in chat server but not in backend API **Root Cause**: CSRF protection not implemented for REST API. **Fix Plan**: 1. Implement CSRF token generation endpoint: `GET /api/v1/csrf-token` 2. Backend: Add CSRF middleware for POST/PUT/DELETE (exclude GET, OPTIONS) 3. Frontend: Fetch CSRF token on app init, include in `X-CSRF-Token` header 4. Update `apiClient` interceptor to add CSRF header **Tests**: CSRF attack simulation, verify tokens validated. --- ## Risk Register ### Security Risks | Risk | Severity | Evidence | Mitigation | |------|----------|----------|------------| | **CORS misconfiguration** | High | Empty origins in prod → reject all | Document required env var, add validation | | **No CSRF protection** | High | `csrf.ts` placeholder, no backend middleware | Implement CSRF tokens (see P0 #5) | | **Tokens in localStorage** | Medium | XSS can steal tokens | Consider httpOnly cookies (requires backend changes) | | **No SameSite cookie flags** | Medium | Cookies not used, but if added later | Set `SameSite=Lax` for cookies | | **CORS allows credentials** | Low | `Access-Control-Allow-Credentials: true` | OK if origins whitelisted correctly | ### Reliability Risks | Risk | Severity | Evidence | Mitigation | |------|----------|----------|------------| | **Token storage desync** | High | Multiple storage mechanisms | Standardize on TokenStorage (see P0 #2) | | **Type mismatches** | High | User.id string vs number | Fix type definitions (see P0 #3) | | **Missing endpoints** | Medium | 18 endpoints called but not implemented | Implement or remove frontend calls | | **Error parsing inconsistencies** | Medium | Multiple error formats handled | Standardize on backend error format | | **No retry logic for 503/502** | Low | Handled in `apiClient` but no exponential backoff | Add retry with backoff | ### Drift Risks | Risk | Severity | Evidence | Mitigation | |------|----------|----------|------------| | **Response format changes** | High | Deprecated ApiService expects different format | Remove ApiService (see P0 #4) | | **Environment variable naming** | Medium | `VITE_API_BASE_URL` vs `VITE_API_URL` | Standardize on `VITE_API_URL` | | **Path mismatches** | Medium | `/users/:userId/profile` vs `/users/:id` | Align frontend paths with backend | | **Field name mismatches** | Low | `cover_art_path` vs `cover_art_url` | Document field mappings | --- ## Recommended Fix Order (1-2 weeks) ### Week 1: Critical Fixes **Day 1-2: CORS & Environment** - [ ] Add CORS validation on backend startup (fail if prod + empty origins) - [ ] Document `CORS_ALLOWED_ORIGINS` requirement - [ ] Standardize env var names (`VITE_API_URL` everywhere) - [ ] Update deployment configs **Day 3-4: Auth Storage** - [ ] Audit all token storage usage - [ ] Remove Zustand token storage, keep only flags - [ ] Remove `token-manager.ts` or migrate to `TokenStorage` - [ ] Test token persistence across reloads **Day 5: Type Safety** - [ ] Fix `User.id` type (string everywhere) - [ ] Update Zod schemas - [ ] Run TypeScript strict checks - [ ] Fix type errors ### Week 2: API Contract & Security **Day 1-2: CSRF Protection** - [ ] Implement backend CSRF endpoint - [ ] Add CSRF middleware - [ ] Update frontend to fetch/include tokens - [ ] Test CSRF protection **Day 3-4: Missing Endpoints** - [ ] Audit all frontend API calls - [ ] Implement missing endpoints OR remove frontend calls - [ ] Fix path mismatches (`/profile` vs `/users/:id`) - [ ] Update API documentation **Day 5: Deprecated Code Removal** - [ ] Complete migration from `ApiService` to `apiClient` - [ ] Remove `ApiService` class - [ ] Update all imports - [ ] Verify no regressions --- ## Appendix ### Commands Executed ```bash # Searched for route definitions grep -r "router\.(GET|POST|PUT|DELETE)" veza-backend-api/internal/api/ # Searched for API calls grep -r "apiClient\.(get|post|put|delete)" apps/web/src/ # Searched for environment variables grep -r "VITE_" apps/web/ # Searched for CORS configuration grep -r "CORS" veza-backend-api/internal/ ``` ### Search Hits Summary - **Backend routes**: 50+ endpoints found in `router.go` - **Frontend API calls**: 100+ calls found across services - **Environment variables**: 8 `VITE_*` vars found (inconsistent naming) - **CORS config**: 1 middleware, 1 config function - **Auth storage**: 3 different mechanisms found - **Type definitions**: 5+ `User` type definitions found ### Logs Captured No runtime logs captured (audit based on code analysis only). --- **End of Report**