Complete the AuthCallbackPage to handle OAuth redirects by parsing tokens from URL fragment, fetching user profile, and redirecting based on starter deck status. Includes open-redirect protection and comprehensive tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
380 lines
15 KiB
JSON
380 lines
15 KiB
JSON
{
|
|
"meta": {
|
|
"phaseId": "PHASE_F1",
|
|
"name": "Authentication Flow",
|
|
"version": "1.0.0",
|
|
"created": "2026-01-30",
|
|
"lastUpdated": "2026-01-30",
|
|
"totalTasks": 10,
|
|
"completedTasks": 2,
|
|
"status": "in_progress",
|
|
"description": "Complete OAuth authentication flow including login, callback handling, starter deck selection, profile management, and app initialization."
|
|
},
|
|
"dependencies": {
|
|
"phases": ["PHASE_F0"],
|
|
"backend": [
|
|
"GET /api/auth/google - Start Google OAuth",
|
|
"GET /api/auth/discord - Start Discord OAuth",
|
|
"GET /api/auth/{provider}/callback - OAuth callback (returns tokens in URL fragment)",
|
|
"POST /api/auth/refresh - Refresh access token",
|
|
"POST /api/auth/logout - Revoke refresh token",
|
|
"POST /api/auth/logout-all - Revoke all tokens (requires auth)",
|
|
"GET /api/users/me - Get current user profile",
|
|
"PATCH /api/users/me - Update profile",
|
|
"GET /api/users/me/linked-accounts - List linked OAuth accounts",
|
|
"GET /api/users/me/starter-status - Check if user has starter deck",
|
|
"POST /api/users/me/starter-deck - Select starter deck",
|
|
"GET /api/auth/link/google - Link Google account (requires auth)",
|
|
"GET /api/auth/link/discord - Link Discord account (requires auth)",
|
|
"DELETE /api/users/me/link/{provider} - Unlink OAuth provider"
|
|
]
|
|
},
|
|
"tasks": [
|
|
{
|
|
"id": "F1-001",
|
|
"name": "Update LoginPage for OAuth",
|
|
"description": "Replace username/password form with OAuth provider buttons",
|
|
"category": "pages",
|
|
"priority": 1,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{"path": "src/pages/LoginPage.vue", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Remove username/password form (not used - OAuth only)",
|
|
"Add Google OAuth button with branded styling",
|
|
"Add Discord OAuth button with branded styling",
|
|
"Handle redirect to OAuth provider via auth store",
|
|
"Show error messages from URL query params (oauth_failed)",
|
|
"Responsive design for mobile/desktop",
|
|
"Add loading state during redirect"
|
|
],
|
|
"acceptance": [
|
|
"Page shows two OAuth buttons: Google and Discord",
|
|
"Clicking button redirects to backend OAuth endpoint",
|
|
"Error messages from failed OAuth are displayed",
|
|
"No username/password fields visible"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-002",
|
|
"name": "Implement AuthCallbackPage",
|
|
"description": "Handle OAuth callback and extract tokens from URL fragment",
|
|
"category": "pages",
|
|
"priority": 2,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F1-001"],
|
|
"files": [
|
|
{"path": "src/pages/AuthCallbackPage.vue", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Parse URL hash fragment for access_token, refresh_token, expires_in",
|
|
"Handle error query params (error, message)",
|
|
"Store tokens in auth store using setTokens()",
|
|
"Fetch user profile after successful auth",
|
|
"Check if user needs starter deck selection",
|
|
"Redirect to starter selection if no starter, else to dashboard",
|
|
"Show appropriate loading/error states",
|
|
"Handle edge cases (missing tokens, network errors)"
|
|
],
|
|
"acceptance": [
|
|
"Successfully extracts tokens from URL fragment",
|
|
"Stores tokens in auth store (persisted)",
|
|
"Fetches user profile after auth",
|
|
"Redirects to /starter if user has no starter deck",
|
|
"Redirects to / (dashboard) if user has starter deck",
|
|
"Shows error message if OAuth failed"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-003",
|
|
"name": "Create useAuth composable",
|
|
"description": "Vue composable for auth operations with loading/error states",
|
|
"category": "composables",
|
|
"priority": 3,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-002"],
|
|
"files": [
|
|
{"path": "src/composables/useAuth.ts", "status": "create"},
|
|
{"path": "src/composables/useAuth.spec.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Wrap auth store operations with loading/error handling",
|
|
"Provide initiateOAuth(provider) helper",
|
|
"Provide handleCallback() for AuthCallbackPage",
|
|
"Provide logout() with redirect to login",
|
|
"Provide logoutAll() for all-device logout",
|
|
"Track isInitialized state for app startup",
|
|
"Auto-fetch profile on initialization if tokens exist"
|
|
],
|
|
"acceptance": [
|
|
"initiateOAuth() redirects to correct OAuth URL",
|
|
"handleCallback() extracts tokens and fetches profile",
|
|
"logout() clears state and redirects to login",
|
|
"Loading and error states are properly tracked"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-004",
|
|
"name": "Implement app auth initialization",
|
|
"description": "Initialize auth state on app startup",
|
|
"category": "setup",
|
|
"priority": 4,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-003"],
|
|
"files": [
|
|
{"path": "src/App.vue", "status": "modify"},
|
|
{"path": "src/main.ts", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Call auth.init() on app startup (in main.ts or App.vue)",
|
|
"Show loading state while initializing auth",
|
|
"Validate existing tokens by refreshing if expired",
|
|
"Fetch user profile if authenticated",
|
|
"Handle initialization errors gracefully",
|
|
"Block navigation until auth is initialized"
|
|
],
|
|
"acceptance": [
|
|
"App shows loading spinner during auth init",
|
|
"Expired tokens are refreshed automatically",
|
|
"User profile is fetched if authenticated",
|
|
"Invalid/expired refresh tokens trigger logout",
|
|
"Navigation guards work after init completes"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-005",
|
|
"name": "Implement StarterSelectionPage",
|
|
"description": "Complete starter deck selection with API integration",
|
|
"category": "pages",
|
|
"priority": 5,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-003"],
|
|
"files": [
|
|
{"path": "src/pages/StarterSelectionPage.vue", "status": "modify"},
|
|
{"path": "src/composables/useStarter.ts", "status": "create"},
|
|
{"path": "src/composables/useStarter.spec.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Display 5 starter deck options: grass, fire, water, psychic, lightning",
|
|
"Show deck preview (card count, theme description)",
|
|
"Handle deck selection with confirmation",
|
|
"Call POST /api/users/me/starter-deck on selection",
|
|
"Show loading state during API call",
|
|
"Handle errors (already selected, network error)",
|
|
"Redirect to dashboard on success",
|
|
"Update auth store hasStarterDeck flag"
|
|
],
|
|
"starterTypes": [
|
|
{"type": "grass", "name": "Forest Guardians", "description": "Growth and healing focused deck"},
|
|
{"type": "fire", "name": "Flame Warriors", "description": "Aggressive damage-focused deck"},
|
|
{"type": "water", "name": "Tidal Force", "description": "Balanced control and damage"},
|
|
{"type": "psychic", "name": "Mind Masters", "description": "Status effects and manipulation"},
|
|
{"type": "lightning", "name": "Storm Riders", "description": "Fast, high-damage strikes"}
|
|
],
|
|
"acceptance": [
|
|
"5 starter deck options displayed with themes",
|
|
"Selection calls API with correct starter_type",
|
|
"Success updates user state and redirects to /",
|
|
"Errors are displayed to user",
|
|
"Already-selected error handled gracefully"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-006",
|
|
"name": "Implement ProfilePage",
|
|
"description": "User profile management with linked accounts",
|
|
"category": "pages",
|
|
"priority": 6,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-003"],
|
|
"files": [
|
|
{"path": "src/pages/ProfilePage.vue", "status": "modify"},
|
|
{"path": "src/composables/useProfile.ts", "status": "create"},
|
|
{"path": "src/composables/useProfile.spec.ts", "status": "create"},
|
|
{"path": "src/components/profile/LinkedAccountCard.vue", "status": "create"},
|
|
{"path": "src/components/profile/DisplayNameEditor.vue", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Display user avatar and display name",
|
|
"Editable display name with save button",
|
|
"List linked OAuth accounts (Google, Discord)",
|
|
"Link additional OAuth provider button",
|
|
"Unlink OAuth provider (if not primary)",
|
|
"Logout button (current session)",
|
|
"Logout All button (all devices)",
|
|
"Show active session count"
|
|
],
|
|
"acceptance": [
|
|
"Profile displays user info correctly",
|
|
"Display name can be edited and saved",
|
|
"Linked accounts are displayed",
|
|
"Can link additional OAuth provider",
|
|
"Can unlink non-primary provider",
|
|
"Logout works correctly",
|
|
"Logout All works correctly"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-007",
|
|
"name": "Update navigation for auth state",
|
|
"description": "Update NavSidebar and NavBottomTabs for auth state",
|
|
"category": "components",
|
|
"priority": 7,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-003"],
|
|
"files": [
|
|
{"path": "src/components/NavSidebar.vue", "status": "modify"},
|
|
{"path": "src/components/NavBottomTabs.vue", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Show user avatar in nav if available",
|
|
"Use actual display name instead of placeholder",
|
|
"Ensure logout button triggers proper logout flow",
|
|
"Handle loading state during logout"
|
|
],
|
|
"acceptance": [
|
|
"Nav shows actual user avatar if available",
|
|
"Nav shows actual display name",
|
|
"Logout triggers full logout flow with redirect"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-008",
|
|
"name": "Implement account linking flow",
|
|
"description": "Allow users to link additional OAuth providers",
|
|
"category": "features",
|
|
"priority": 8,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-006"],
|
|
"files": [
|
|
{"path": "src/composables/useAccountLinking.ts", "status": "create"},
|
|
{"path": "src/composables/useAccountLinking.spec.ts", "status": "create"},
|
|
{"path": "src/pages/LinkCallbackPage.vue", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Add route for /auth/link/callback to handle linking callbacks",
|
|
"Initiate linking via GET /api/auth/link/{provider}",
|
|
"Handle success/error query params on callback",
|
|
"Refresh linked accounts list after linking",
|
|
"Show success toast on link complete",
|
|
"Handle errors (already linked, etc.)"
|
|
],
|
|
"acceptance": [
|
|
"Can initiate link from profile page",
|
|
"Link callback handles success and error",
|
|
"Linked accounts list updates after linking",
|
|
"Appropriate feedback shown to user"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-009",
|
|
"name": "Add requireStarter guard implementation",
|
|
"description": "Implement the starter deck navigation guard",
|
|
"category": "router",
|
|
"priority": 9,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-005"],
|
|
"files": [
|
|
{"path": "src/router/guards.ts", "status": "modify"},
|
|
{"path": "src/router/guards.spec.ts", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"requireStarter checks if user has selected starter deck",
|
|
"If no starter, redirect to /starter page",
|
|
"Check auth.user?.hasStarterDeck flag",
|
|
"If flag is undefined, fetch starter status from API",
|
|
"Cache result to avoid repeated API calls"
|
|
],
|
|
"acceptance": [
|
|
"Users without starter deck are redirected to /starter",
|
|
"Users with starter deck can access protected routes",
|
|
"Guard waits for auth initialization before checking",
|
|
"API is called only when needed"
|
|
]
|
|
},
|
|
{
|
|
"id": "F1-010",
|
|
"name": "Write integration tests for auth flow",
|
|
"description": "End-to-end tests for complete auth flow",
|
|
"category": "testing",
|
|
"priority": 10,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["F1-001", "F1-002", "F1-003", "F1-004", "F1-005"],
|
|
"files": [
|
|
{"path": "src/pages/LoginPage.spec.ts", "status": "create"},
|
|
{"path": "src/pages/AuthCallbackPage.spec.ts", "status": "create"},
|
|
{"path": "src/pages/StarterSelectionPage.spec.ts", "status": "create"},
|
|
{"path": "src/pages/ProfilePage.spec.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Test LoginPage OAuth button redirects",
|
|
"Test AuthCallbackPage token extraction",
|
|
"Test AuthCallbackPage error handling",
|
|
"Test StarterSelectionPage deck selection flow",
|
|
"Test ProfilePage display and edit operations",
|
|
"Test navigation guards with various auth states",
|
|
"Mock API responses for all tests"
|
|
],
|
|
"acceptance": [
|
|
"All page components have test files",
|
|
"Tests cover happy path and error cases",
|
|
"Tests mock API calls appropriately",
|
|
"All tests pass"
|
|
]
|
|
}
|
|
],
|
|
"apiContracts": {
|
|
"oauthCallback": {
|
|
"description": "Backend redirects to frontend with tokens in URL fragment",
|
|
"format": "/auth/callback#access_token={token}&refresh_token={token}&expires_in={seconds}",
|
|
"errorFormat": "/auth/callback?error={code}&message={message}"
|
|
},
|
|
"tokenResponse": {
|
|
"accessToken": "JWT access token (short-lived)",
|
|
"refreshToken": "Opaque refresh token (long-lived)",
|
|
"expiresIn": "Access token expiry in seconds"
|
|
},
|
|
"userProfile": {
|
|
"id": "UUID",
|
|
"display_name": "string",
|
|
"avatar_url": "string | null",
|
|
"has_starter_deck": "boolean",
|
|
"created_at": "ISO datetime",
|
|
"linked_accounts": [
|
|
{
|
|
"provider": "google | discord",
|
|
"email": "string | null",
|
|
"linked_at": "ISO datetime"
|
|
}
|
|
]
|
|
},
|
|
"starterDeck": {
|
|
"request": {
|
|
"starter_type": "grass | fire | water | psychic | lightning"
|
|
},
|
|
"response": "DeckResponse with is_starter=true"
|
|
}
|
|
},
|
|
"notes": [
|
|
"OAuth flow uses URL fragment (hash) for tokens, not query params, for security",
|
|
"Tokens are persisted via pinia-plugin-persistedstate",
|
|
"Auth store already has most functionality, composables add loading/error handling",
|
|
"LoginPage currently has username/password form which needs to be replaced",
|
|
"AuthCallbackPage currently just redirects to home - needs full implementation",
|
|
"StarterSelectionPage has placeholder UI - needs API integration",
|
|
"ProfilePage needs to be created from scratch"
|
|
]
|
|
}
|