982 lines
No EOL
36 KiB
JSON
982 lines
No EOL
36 KiB
JSON
{
|
|
"meta": {
|
|
"title": "Veza Integration MVP Stability Todolist",
|
|
"description": "Complete actionable todolist to reach a stable MVP state for backend/frontend integration",
|
|
"created_at": "2025-01-27T00:00:00Z",
|
|
"target_health_score": "8/10",
|
|
"current_health_score": "4/10",
|
|
"estimated_total_effort": "8-12 days",
|
|
"source_documents": [
|
|
"INTEGRATION_AUDIT_BACKEND_FRONTEND.md",
|
|
"INTEGRATION_ISSUES_INDEX.json"
|
|
]
|
|
},
|
|
"mvp_definition": {
|
|
"description": "A stable MVP means: core authentication works reliably, API contracts are consistent, no security vulnerabilities, and the app can be deployed to production without critical failures",
|
|
"must_have": [
|
|
"Authentication flow works 100% (login, logout, token refresh)",
|
|
"CORS properly configured for production",
|
|
"No token desync issues",
|
|
"Type safety across frontend/backend boundary",
|
|
"All active API calls have corresponding backend endpoints",
|
|
"Basic security (CSRF protection)"
|
|
],
|
|
"nice_to_have": [
|
|
"Full error correlation (request IDs)",
|
|
"Retry logic with exponential backoff",
|
|
"Complete 2FA implementation",
|
|
"All collaboration features"
|
|
],
|
|
"out_of_scope_for_mvp": [
|
|
"HLS streaming endpoints",
|
|
"Advanced playlist features (recommendations, sharing)",
|
|
"Role management endpoints",
|
|
"Notification system"
|
|
]
|
|
},
|
|
"phases": [
|
|
{
|
|
"id": "PHASE-1",
|
|
"name": "Critical Blockers",
|
|
"description": "Without these fixes, the app will not work in production",
|
|
"priority": "CRITICAL",
|
|
"estimated_effort": "3-4 days",
|
|
"tasks": [
|
|
{
|
|
"id": "MVP-001",
|
|
"source_issue": "INT-000001",
|
|
"title": "Fix CORS Production Configuration",
|
|
"description": "CORS rejects ALL requests in production if CORS_ALLOWED_ORIGINS is not set. Add fail-fast validation.",
|
|
"owner": "backend",
|
|
"estimated_hours": 2,
|
|
"status": "done",
|
|
"priority": 1,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "veza-backend-api/internal/config/config.go",
|
|
"action": "Add validation function",
|
|
"lines": "L638-L664"
|
|
},
|
|
{
|
|
"path": "veza-backend-api/cmd/api/main.go",
|
|
"action": "Call validation on startup"
|
|
},
|
|
{
|
|
"path": "docker-compose.production.yml",
|
|
"action": "Add CORS_ALLOWED_ORIGINS example"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Create validation function in config.go",
|
|
"code_snippet": "func (c *Config) ValidateForProduction() error {\n if c.Environment == EnvProduction && len(c.CORSOrigins) == 0 {\n return fmt.Errorf(\"FATAL: CORS_ALLOWED_ORIGINS must be set in production\")\n }\n return nil\n}"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Call validation in main.go before server starts",
|
|
"code_snippet": "if err := cfg.ValidateForProduction(); err != nil {\n log.Fatal(err)\n}"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Update docker-compose with documented example"
|
|
},
|
|
{
|
|
"step": 4,
|
|
"action": "Add unit test for validation"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
"APP_ENV=production CORS_ALLOWED_ORIGINS='' go run ./cmd/api # Should fail with clear error",
|
|
"APP_ENV=production CORS_ALLOWED_ORIGINS='https://app.veza.com' go run ./cmd/api # Should start"
|
|
],
|
|
"expected_outcome": "Server refuses to start with empty CORS in production mode"
|
|
},
|
|
"acceptance_criteria": [
|
|
"Server fails fast with clear error message if CORS empty in production",
|
|
"Server starts normally with CORS configured",
|
|
"Documentation updated with required env vars"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-002",
|
|
"source_issue": "INT-000002",
|
|
"title": "Unify Token Storage to Single Source of Truth",
|
|
"description": "Three competing token storage mechanisms cause auth failures. Consolidate to TokenStorage only.",
|
|
"owner": "frontend",
|
|
"estimated_hours": 4,
|
|
"status": "done",
|
|
"priority": 2,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/stores/auth.ts",
|
|
"action": "Remove token storage, keep only user and isAuthenticated"
|
|
},
|
|
{
|
|
"path": "apps/web/src/utils/token-manager.ts",
|
|
"action": "DELETE or redirect to TokenStorage"
|
|
},
|
|
{
|
|
"path": "apps/web/src/services/api/client.ts",
|
|
"action": "Remove Zustand fallback (L48-L64), use only TokenStorage"
|
|
},
|
|
{
|
|
"path": "apps/web/src/services/tokenStorage.ts",
|
|
"action": "Verify this is the canonical source"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Audit all token access points",
|
|
"command": "grep -rn 'localStorage.*token\\|getAccessToken\\|setAccessToken\\|auth-storage' apps/web/src/"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Update Zustand store to remove token storage",
|
|
"details": "Keep only: user, isAuthenticated, isLoading, error. Remove: accessToken, refreshToken"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Delete apps/web/src/utils/token-manager.ts"
|
|
},
|
|
{
|
|
"step": 4,
|
|
"action": "Update apiClient to remove Zustand fallback",
|
|
"details": "Remove lines 48-64 that parse auth-storage"
|
|
},
|
|
{
|
|
"step": 5,
|
|
"action": "Update all login/logout flows to use TokenStorage exclusively"
|
|
},
|
|
{
|
|
"step": 6,
|
|
"action": "Test token persistence across page reloads"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
"grep -r 'auth-storage' apps/web/src/services/api/ # Should return 0 results",
|
|
"grep -r 'token-manager' apps/web/src/ # Should return 0 results"
|
|
],
|
|
"manual_tests": [
|
|
"Login → Refresh page → Still logged in",
|
|
"Login → Open new tab → Still logged in",
|
|
"Logout → Token cleared from localStorage"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"Only TokenStorage class manages tokens",
|
|
"No token references in Zustand state",
|
|
"token-manager.ts deleted or re-exports TokenStorage",
|
|
"Auth persists across page reloads"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-003",
|
|
"source_issue": "INT-000003",
|
|
"title": "Fix User.id Type Mismatch (string everywhere)",
|
|
"description": "Backend sends UUID (string) but some frontend types expect number. Causes runtime comparison bugs.",
|
|
"owner": "frontend",
|
|
"estimated_hours": 3,
|
|
"status": "completed",
|
|
"priority": 3,
|
|
"dependencies": [],
|
|
"completion": {
|
|
"completed_at": "2025-01-27T16:00:00Z",
|
|
"completed_by": "cursor-agent",
|
|
"actual_effort_hours": 2.5,
|
|
"commits": [],
|
|
"notes": "Updated all userId/user_id parameters from number to string. Updated Zod schemas to validate UUID format with z.string().uuid(). Fixed TypeScript compilation errors.",
|
|
"issues_encountered": []
|
|
},
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/features/auth/types/index.ts",
|
|
"action": "Change id: number to id: string",
|
|
"lines": "L8"
|
|
},
|
|
{
|
|
"path": "apps/web/src/types/api.ts",
|
|
"action": "Verify id: string (already correct)"
|
|
},
|
|
{
|
|
"path": "apps/web/src/schemas/validation.ts",
|
|
"action": "Update Zod schemas to use z.string().uuid()"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Find all User type definitions",
|
|
"command": "grep -rn 'id:\\s*number' apps/web/src/ --include='*.ts' --include='*.tsx'"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Update each occurrence to id: string"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Update Zod schemas for user ID validation",
|
|
"code_snippet": "id: z.string().uuid()"
|
|
},
|
|
{
|
|
"step": 4,
|
|
"action": "Run TypeScript compiler to find remaining errors",
|
|
"command": "cd apps/web && npx tsc --noEmit"
|
|
},
|
|
{
|
|
"step": 5,
|
|
"action": "Fix all type errors"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
"grep -rn 'id:\\s*number' apps/web/src/ # Should return 0 User-related results",
|
|
"cd apps/web && npx tsc --noEmit # Should pass"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"All User type definitions use id: string",
|
|
"Zod schemas validate UUID format",
|
|
"TypeScript compiles without User.id errors"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-004",
|
|
"source_issue": "INT-000004",
|
|
"title": "Remove Deprecated ApiService, Migrate to apiClient",
|
|
"description": "Deprecated ApiService expects wrong response format. Remove entirely and use apiClient.",
|
|
"owner": "frontend",
|
|
"estimated_hours": 4,
|
|
"status": "completed",
|
|
"priority": 4,
|
|
"dependencies": [
|
|
"MVP-002"
|
|
],
|
|
"completion": {
|
|
"completed_at": "2025-01-27T17:30:00Z",
|
|
"completed_by": "cursor-agent",
|
|
"actual_effort_hours": 3.5,
|
|
"commits": [],
|
|
"notes": "Migrated all ApiService usages to apiClient. Updated library.ts, chat.ts, ProfileForm.tsx, LibraryManager.tsx, UploadModal.tsx, VirtualizedChatMessages.tsx, ChatInterface.tsx. Deleted api.ts and api.test.ts. Updated test mocks.",
|
|
"issues_encountered": []
|
|
},
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/services/api.ts",
|
|
"action": "DELETE this file after migration"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Find all ApiService usages",
|
|
"command": "grep -rn 'ApiService\\|apiService\\|from.*api[\"'\\']' apps/web/src/ | grep -v 'api/client\\|api/auth\\|apiClient'"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "For each usage, migrate to apiClient or specific API module",
|
|
"migration_map": {
|
|
"apiService.login()": "authApi.login()",
|
|
"apiService.register()": "authApi.register()",
|
|
"apiService.getUser()": "apiClient.get('/users/:id')",
|
|
"apiService.refreshToken()": "authApi.refresh()"
|
|
}
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Update imports in all affected files"
|
|
},
|
|
{
|
|
"step": 4,
|
|
"action": "Delete apps/web/src/services/api.ts"
|
|
},
|
|
{
|
|
"step": 5,
|
|
"action": "Verify no references remain",
|
|
"command": "grep -rn 'ApiService\\|apiService' apps/web/src/"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
"grep -rn 'ApiService' apps/web/src/ # Should return 0 results",
|
|
"ls apps/web/src/services/api.ts # Should fail (file deleted)",
|
|
"cd apps/web && npx tsc --noEmit # Should pass"
|
|
],
|
|
"manual_tests": [
|
|
"Login flow works",
|
|
"Registration flow works",
|
|
"User profile loads"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"ApiService class completely removed",
|
|
"All API calls use apiClient or typed API modules",
|
|
"No regressions in auth or user flows"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-005",
|
|
"source_issue": "INT-000005",
|
|
"title": "Implement CSRF Protection",
|
|
"description": "No CSRF protection exists. Implement token generation and validation for state-changing operations.",
|
|
"owner": "backend + frontend",
|
|
"estimated_hours": 6,
|
|
"status": "completed",
|
|
"priority": 5,
|
|
"dependencies": [
|
|
"MVP-001"
|
|
],
|
|
"completion": {
|
|
"completed_at": "2025-01-27T18:00:00Z",
|
|
"completed_by": "cursor-agent",
|
|
"actual_effort_hours": 5.5,
|
|
"commits": [],
|
|
"notes": "Implemented CSRF protection using Redis for token storage. Created middleware and handler in backend. Added CSRF service in frontend with automatic token refresh. Integrated with apiClient interceptor. Login/register correctly excluded from CSRF check.",
|
|
"issues_encountered": []
|
|
},
|
|
"files_to_modify": [
|
|
{
|
|
"path": "veza-backend-api/internal/middleware/csrf.go",
|
|
"action": "CREATE - CSRF middleware"
|
|
},
|
|
{
|
|
"path": "veza-backend-api/internal/handlers/csrf.go",
|
|
"action": "CREATE - CSRF token endpoint"
|
|
},
|
|
{
|
|
"path": "veza-backend-api/internal/api/router.go",
|
|
"action": "Add CSRF routes and middleware"
|
|
},
|
|
{
|
|
"path": "apps/web/src/services/csrf.ts",
|
|
"action": "Implement refreshCsrfToken()"
|
|
},
|
|
{
|
|
"path": "apps/web/src/services/api/client.ts",
|
|
"action": "Add CSRF header interceptor"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Create CSRF middleware in backend",
|
|
"code_snippet": "func CSRFMiddleware() gin.HandlerFunc {\n return func(c *gin.Context) {\n if c.Request.Method == \"GET\" || c.Request.Method == \"OPTIONS\" {\n c.Next()\n return\n }\n token := c.GetHeader(\"X-CSRF-Token\")\n sessionToken := getSessionCSRFToken(c)\n if token == \"\" || token != sessionToken {\n c.AbortWithStatusJSON(403, gin.H{\"error\": \"Invalid CSRF token\"})\n return\n }\n c.Next()\n }\n}"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Create CSRF token endpoint",
|
|
"route": "GET /api/v1/csrf-token"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Apply middleware to router (after auth routes)",
|
|
"note": "Exclude login/register from CSRF check"
|
|
},
|
|
{
|
|
"step": 4,
|
|
"action": "Implement frontend csrf.ts",
|
|
"code_snippet": "async refreshToken(): Promise<void> {\n const response = await fetch('/api/v1/csrf-token', { credentials: 'include' });\n const data = await response.json();\n this.token = data.csrf_token;\n}"
|
|
},
|
|
{
|
|
"step": 5,
|
|
"action": "Add interceptor to apiClient",
|
|
"code_snippet": "apiClient.interceptors.request.use((config) => {\n if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase() || '')) {\n config.headers['X-CSRF-Token'] = csrfService.getToken();\n }\n return config;\n});"
|
|
},
|
|
{
|
|
"step": 6,
|
|
"action": "Fetch CSRF token on app initialization"
|
|
}
|
|
],
|
|
"validation": {
|
|
"manual_tests": [
|
|
"POST request without CSRF token → 403 error",
|
|
"POST request with valid CSRF token → Success",
|
|
"GET requests work without CSRF token"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"CSRF endpoint returns token",
|
|
"All POST/PUT/DELETE requests include X-CSRF-Token header",
|
|
"Requests without valid token are rejected with 403",
|
|
"Login/register still work (excluded from CSRF)"
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "PHASE-2",
|
|
"name": "API Contract Alignment",
|
|
"description": "Fix mismatches between frontend calls and backend routes",
|
|
"priority": "HIGH",
|
|
"estimated_effort": "2-3 days",
|
|
"tasks": [
|
|
{
|
|
"id": "MVP-006",
|
|
"source_issue": "INT-000007",
|
|
"title": "Standardize Environment Variable Names",
|
|
"description": "VITE_API_BASE_URL vs VITE_API_URL inconsistency causes build failures",
|
|
"owner": "frontend",
|
|
"estimated_hours": 1,
|
|
"status": "completed",
|
|
"priority": 6,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/scripts/check_backend.sh",
|
|
"action": "Replace VITE_API_BASE_URL with VITE_API_URL"
|
|
},
|
|
{
|
|
"path": "apps/web/Dockerfile",
|
|
"action": "Replace ARG VITE_API_BASE_URL with VITE_API_URL"
|
|
},
|
|
{
|
|
"path": "apps/web/.env.example",
|
|
"action": "Ensure only VITE_API_URL is documented"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Find all VITE_API_BASE_URL references",
|
|
"command": "grep -rn 'VITE_API_BASE_URL' apps/web/"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Replace all with VITE_API_URL"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Update .env.example with correct variable"
|
|
},
|
|
{
|
|
"step": 4,
|
|
"action": "Update deployment documentation"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
"grep -rn 'VITE_API_BASE_URL' apps/web/ # Should return 0 results"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"Only VITE_API_URL used everywhere",
|
|
"Scripts and Dockerfile updated",
|
|
"Documentation reflects correct variable name"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-007",
|
|
"source_issue": "INT-000008",
|
|
"title": "Fix Profile Endpoint Path Mismatch",
|
|
"description": "Frontend calls /users/:userId/profile but backend uses /users/:id",
|
|
"owner": "frontend",
|
|
"estimated_hours": 2,
|
|
"status": "completed",
|
|
"priority": 7,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/features/profile/services/profileService.ts",
|
|
"action": "Update paths to match backend routes"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Update getProfile path",
|
|
"from": "GET /api/v1/users/${userId}/profile",
|
|
"to": "GET /api/v1/users/${userId}"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Update updateProfile path",
|
|
"from": "PUT /api/v1/users/${userId}/profile",
|
|
"to": "PUT /api/v1/users/${userId}"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Verify response handling matches backend format"
|
|
}
|
|
],
|
|
"validation": {
|
|
"manual_tests": [
|
|
"Profile page loads correctly",
|
|
"Profile update saves successfully"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"Profile endpoints match backend routes",
|
|
"Profile CRUD operations work"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-008",
|
|
"source_issue": "INT-000006",
|
|
"title": "Handle Missing Endpoints - Decide and Clean",
|
|
"description": "18 frontend calls target non-existent endpoints. For MVP: remove calls for non-essential features, stub essential ones.",
|
|
"owner": "frontend + backend",
|
|
"estimated_hours": 4,
|
|
"status": "completed",
|
|
"priority": 8,
|
|
"dependencies": [],
|
|
"sub_tasks": [
|
|
{
|
|
"id": "MVP-008a",
|
|
"title": "Remove 2FA service calls (not MVP)",
|
|
"action": "Comment out or remove 2fa-service.ts usage until backend implemented",
|
|
"files": [
|
|
"apps/web/src/services/2fa-service.ts"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-008b",
|
|
"title": "Remove playlist collaboration features (not MVP)",
|
|
"action": "Disable UI for collaborators, search, share, recommendations",
|
|
"files": [
|
|
"apps/web/src/features/playlists/services/playlistService.ts"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-008c",
|
|
"title": "Remove HLS service calls (not MVP)",
|
|
"action": "Remove or stub hlsService until streaming implemented",
|
|
"files": [
|
|
"apps/web/src/features/streaming/services/hlsService.ts"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-008d",
|
|
"title": "Remove role management service (not MVP)",
|
|
"action": "Disable role management UI",
|
|
"files": [
|
|
"apps/web/src/features/admin/services/roleService.ts"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-008e",
|
|
"title": "Remove notifications API calls (not MVP)",
|
|
"action": "Disable notifications until implemented",
|
|
"files": [
|
|
"apps/web/src/features/notifications/api/notificationsApi.ts"
|
|
]
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Create feature flags or environment checks",
|
|
"code_snippet": "const FEATURES = {\n TWO_FACTOR_AUTH: false,\n PLAYLIST_COLLABORATION: false,\n HLS_STREAMING: false,\n ROLE_MANAGEMENT: false,\n NOTIFICATIONS: false\n};"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Wrap non-MVP service calls with feature flags"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Hide UI elements for disabled features"
|
|
},
|
|
{
|
|
"step": 4,
|
|
"action": "Add TODO comments for post-MVP implementation"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
"cd apps/web && npx tsc --noEmit # Should pass without endpoint errors"
|
|
],
|
|
"manual_tests": [
|
|
"App loads without 404 errors in console",
|
|
"Core features (auth, tracks, playlists CRUD) work"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"No 404 errors from frontend API calls",
|
|
"Non-MVP features gracefully disabled",
|
|
"Core MVP features fully functional"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-009",
|
|
"source_issue": "INT-000015",
|
|
"title": "Fix GetMe Endpoint to Return Full User",
|
|
"description": "GET /auth/me returns only id, email, role but frontend needs full user object",
|
|
"owner": "backend",
|
|
"estimated_hours": 2,
|
|
"status": "completed",
|
|
"priority": 9,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "veza-backend-api/internal/handlers/auth.go",
|
|
"action": "Update GetMe to fetch and return full user",
|
|
"lines": "L369-L373"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Update GetMe handler to fetch user from database"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Return full UserResponse instead of minimal fields"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Ensure response matches frontend User type"
|
|
}
|
|
],
|
|
"validation": {
|
|
"manual_tests": [
|
|
"Call GET /api/v1/auth/me",
|
|
"Response includes: id, email, username, avatar, role, created_at, etc."
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"GetMe returns complete user object",
|
|
"Frontend can display all user fields after login"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-010",
|
|
"source_issue": "INT-000009",
|
|
"title": "Fix Error Code Type in Zod Schemas",
|
|
"description": "Backend sends error code as number, but Zod schema expects string",
|
|
"owner": "frontend",
|
|
"estimated_hours": 1,
|
|
"status": "completed",
|
|
"priority": 10,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/schemas/validation.ts",
|
|
"action": "Change code: z.string() to code: z.number()",
|
|
"lines": "L338"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Update Zod error schema",
|
|
"from": "code: z.string()",
|
|
"to": "code: z.number()"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Verify error handling still works"
|
|
}
|
|
],
|
|
"validation": {
|
|
"manual_tests": [
|
|
"Trigger API error",
|
|
"Error displays correctly with error code"
|
|
]
|
|
},
|
|
"acceptance_criteria": [
|
|
"Error codes parsed as numbers",
|
|
"Error display works correctly"
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "PHASE-3",
|
|
"name": "Reliability & Polish",
|
|
"description": "Improve robustness for production deployment",
|
|
"priority": "MEDIUM",
|
|
"estimated_effort": "2-3 days",
|
|
"tasks": [
|
|
{
|
|
"id": "MVP-011",
|
|
"source_issue": "INT-000011",
|
|
"title": "Simplify Token Refresh Response Handling",
|
|
"description": "Frontend checks 3 different formats for token refresh. Simplify to single expected format.",
|
|
"owner": "frontend",
|
|
"estimated_hours": 2,
|
|
"status": "completed",
|
|
"priority": 11,
|
|
"dependencies": [
|
|
"MVP-002",
|
|
"MVP-004"
|
|
],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/services/tokenRefresh.ts",
|
|
"action": "Remove fallback format checks, use only correct format"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Document correct format: { success: true, data: { access_token, refresh_token, expires_in } }"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Remove fallback parsing logic (lines 70-84)"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Add clear error if format unexpected"
|
|
}
|
|
],
|
|
"acceptance_criteria": [
|
|
"Token refresh handles single documented format",
|
|
"Clear error on unexpected format",
|
|
"Token refresh works reliably"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-012",
|
|
"source_issue": "INT-000012",
|
|
"title": "Add Retry Logic for 503/502 Errors",
|
|
"description": "Transient errors cause immediate failure. Add retry with exponential backoff.",
|
|
"owner": "frontend",
|
|
"estimated_hours": 3,
|
|
"status": "completed",
|
|
"priority": 12,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/services/api/client.ts",
|
|
"action": "Add retry logic for 502/503 errors"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Create retry utility function",
|
|
"code_snippet": "async function retryWithBackoff<T>(\n fn: () => Promise<T>,\n maxRetries: number = 3,\n baseDelay: number = 1000\n): Promise<T> {\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n if (attempt === maxRetries - 1) throw error;\n if (!isRetryableError(error)) throw error;\n await sleep(baseDelay * Math.pow(2, attempt));\n }\n }\n throw new Error('Max retries exceeded');\n}"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Apply retry logic to 502/503 responses"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Respect Retry-After header if present"
|
|
}
|
|
],
|
|
"acceptance_criteria": [
|
|
"502/503 errors retried up to 3 times",
|
|
"Exponential backoff between retries",
|
|
"Retry-After header respected"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-013",
|
|
"source_issue": "INT-000013",
|
|
"title": "Add Error Correlation with Request IDs",
|
|
"description": "Backend returns request_id but frontend doesn't log it. Add for debugging.",
|
|
"owner": "frontend",
|
|
"estimated_hours": 2,
|
|
"status": "completed",
|
|
"priority": 13,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/services/api/client.ts",
|
|
"action": "Extract and log request_id from error responses"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Update error handler to extract request_id"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Include request_id in error logs"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Optionally show request_id in user-facing error messages"
|
|
}
|
|
],
|
|
"acceptance_criteria": [
|
|
"Error logs include request_id",
|
|
"Can correlate frontend errors with backend logs"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-014",
|
|
"source_issue": "INT-000014",
|
|
"title": "Validate CORS Credentials Configuration",
|
|
"description": "Credentials=true is hardcoded. Add validation to prevent security issues.",
|
|
"owner": "backend",
|
|
"estimated_hours": 1,
|
|
"status": "completed",
|
|
"priority": 14,
|
|
"dependencies": [
|
|
"MVP-001"
|
|
],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "veza-backend-api/internal/middleware/cors.go",
|
|
"action": "Add validation: reject wildcard origins with credentials"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Check if any origin contains wildcard"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Log warning if credentials=true with weak origins"
|
|
},
|
|
{
|
|
"step": 3,
|
|
"action": "Optionally fail startup if insecure configuration detected"
|
|
}
|
|
],
|
|
"acceptance_criteria": [
|
|
"Warning logged for weak CORS configuration",
|
|
"No wildcard origins with credentials=true"
|
|
]
|
|
},
|
|
{
|
|
"id": "MVP-015",
|
|
"source_issue": "INT-000010",
|
|
"title": "Standardize remember_me Field Name",
|
|
"description": "Mixed naming: rememberMe in forms, remember_me in API. Standardize to snake_case.",
|
|
"owner": "frontend",
|
|
"estimated_hours": 1,
|
|
"status": "completed",
|
|
"priority": 15,
|
|
"dependencies": [],
|
|
"files_to_modify": [
|
|
{
|
|
"path": "apps/web/src/features/auth/types/index.ts",
|
|
"action": "Ensure remember_me naming"
|
|
},
|
|
{
|
|
"path": "apps/web/src/features/auth/components/LoginForm.tsx",
|
|
"action": "Update form field naming"
|
|
}
|
|
],
|
|
"implementation_steps": [
|
|
{
|
|
"step": 1,
|
|
"action": "Find all rememberMe references",
|
|
"command": "grep -rn 'rememberMe' apps/web/src/"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"action": "Replace with remember_me to match backend"
|
|
}
|
|
],
|
|
"acceptance_criteria": [
|
|
"Consistent snake_case naming for remember_me",
|
|
"Login with remember me works correctly"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"summary": {
|
|
"total_tasks": 15,
|
|
"by_owner": {
|
|
"backend": 4,
|
|
"frontend": 9,
|
|
"backend + frontend": 2
|
|
},
|
|
"by_phase": {
|
|
"PHASE-1 (Critical)": 5,
|
|
"PHASE-2 (API Contract)": 5,
|
|
"PHASE-3 (Reliability)": 5
|
|
},
|
|
"estimated_total_hours": "38-42 hours",
|
|
"estimated_calendar_days": "8-12 days (solo developer)",
|
|
"critical_path": [
|
|
"MVP-001 (CORS)",
|
|
"MVP-002 (Token Storage)",
|
|
"MVP-005 (CSRF)",
|
|
"MVP-004 (ApiService removal)"
|
|
]
|
|
},
|
|
"progress_tracking": {
|
|
"completed": 15,
|
|
"in_progress": 0,
|
|
"todo": 0,
|
|
"blocked": 0,
|
|
"last_updated": "2025-01-28T04:00:00Z",
|
|
"completion_percentage": 100
|
|
},
|
|
"validation_checklist": {
|
|
"description": "Run these checks after all tasks complete to verify MVP stability",
|
|
"checks": [
|
|
{
|
|
"id": "VAL-001",
|
|
"name": "TypeScript Compilation",
|
|
"command": "cd apps/web && npx tsc --noEmit",
|
|
"expected": "Exit code 0, no errors"
|
|
},
|
|
{
|
|
"id": "VAL-002",
|
|
"name": "Go Build",
|
|
"command": "cd veza-backend-api && go build ./...",
|
|
"expected": "Exit code 0, no errors"
|
|
},
|
|
{
|
|
"id": "VAL-003",
|
|
"name": "Frontend Tests",
|
|
"command": "cd apps/web && npm test",
|
|
"expected": "All tests pass"
|
|
},
|
|
{
|
|
"id": "VAL-004",
|
|
"name": "Backend Tests",
|
|
"command": "cd veza-backend-api && go test ./...",
|
|
"expected": "All tests pass"
|
|
},
|
|
{
|
|
"id": "VAL-005",
|
|
"name": "CORS Production Check",
|
|
"command": "APP_ENV=production CORS_ALLOWED_ORIGINS='' go run ./cmd/api",
|
|
"expected": "Server fails with clear error"
|
|
},
|
|
{
|
|
"id": "VAL-006",
|
|
"name": "No Deprecated ApiService",
|
|
"command": "grep -r 'ApiService' apps/web/src/",
|
|
"expected": "No results"
|
|
},
|
|
{
|
|
"id": "VAL-007",
|
|
"name": "No Token Storage Fragmentation",
|
|
"command": "grep -r 'auth-storage' apps/web/src/services/",
|
|
"expected": "No results"
|
|
},
|
|
{
|
|
"id": "VAL-008",
|
|
"name": "E2E Auth Flow",
|
|
"manual": true,
|
|
"steps": [
|
|
"Register new user",
|
|
"Logout",
|
|
"Login with new user",
|
|
"Refresh page (should stay logged in)",
|
|
"Token refresh (wait for expiry or force)",
|
|
"Logout"
|
|
],
|
|
"expected": "All steps complete without errors"
|
|
},
|
|
{
|
|
"id": "VAL-009",
|
|
"name": "No Console 404 Errors",
|
|
"manual": true,
|
|
"steps": [
|
|
"Open browser dev tools",
|
|
"Navigate through main app flows",
|
|
"Check network tab for 404s"
|
|
],
|
|
"expected": "No 404 errors from API calls"
|
|
}
|
|
]
|
|
}
|
|
} |