diff --git a/frontend/project_plans/PHASE_F1_authentication.json b/frontend/project_plans/PHASE_F1_authentication.json
new file mode 100644
index 0000000..6cbcfa2
--- /dev/null
+++ b/frontend/project_plans/PHASE_F1_authentication.json
@@ -0,0 +1,379 @@
+{
+ "meta": {
+ "phaseId": "PHASE_F1",
+ "name": "Authentication Flow",
+ "version": "1.0.0",
+ "created": "2026-01-30",
+ "lastUpdated": "2026-01-30",
+ "totalTasks": 10,
+ "completedTasks": 0,
+ "status": "not_started",
+ "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": false,
+ "tested": false,
+ "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": false,
+ "tested": false,
+ "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"
+ ]
+}
diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue
deleted file mode 100644
index 09c7f19..0000000
--- a/frontend/src/components/AppHeader.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
+ {{ props.fallbackMessage }} +
++ {{ errorMessage }} +
+ ++ Sign in to continue your journey +
-
- Already have an account?
-