mantimon-tcg/frontend/project_plans/PHASE_F1_authentication.json
Cal Corum f687909f91 Implement OAuth callback with token handling and profile fetch (F1-002)
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>
2026-01-30 12:39:18 -06:00

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"
]
}