veza/docs/audits/INTEGRATION_AUDIT_BACKEND_FRONTEND.md
2025-12-22 22:00:50 +01:00

17 KiB

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 <token> 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

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

# 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