strat-gameplay-webapp/frontend-sba/store/auth.ts
Cal Corum 1f5e290d8b CLAUDE: Add game page tabs with lineup persistence and per-team submission
- Refactor game page into tab container with Game, Lineups, Stats tabs
- Extract GamePlay, LineupBuilder, GameStats components from page
- Add manager-aware default tab logic (pending + manager → Lineups tab)
- Implement per-team lineup submission (each team submits independently)
- Add lineup-status endpoint with actual lineup data for recovery
- Fix lineup persistence across tab switches and page refreshes
- Query database directly for pending games (avoids GameState validation)
- Add userTeamIds to auth store for manager detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:08:39 -06:00

175 lines
4.8 KiB
TypeScript

/**
* Authentication Store - Cookie-Based
*
* Manages user authentication state via HttpOnly cookies.
* All tokens are stored server-side in cookies, not accessible to JavaScript.
*/
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { DiscordUser, Team } from '~/types'
export const useAuthStore = defineStore('auth', () => {
// ============================================================================
// State
// ============================================================================
const user = ref<DiscordUser | null>(null)
const teams = ref<Team[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
// ============================================================================
// Getters
// ============================================================================
const isAuthenticated = computed(() => {
return user.value !== null
})
const currentUser = computed(() => user.value)
const userTeams = computed(() => teams.value)
const userTeamIds = computed(() => teams.value.map(t => t.id))
const userId = computed(() => user.value?.id ?? null)
// ============================================================================
// Actions
// ============================================================================
/**
* Check authentication status by calling /api/auth/me
* Cookies are sent automatically with credentials: 'include'
*/
async function checkAuth(): Promise<boolean> {
isLoading.value = true
error.value = null
try {
const config = useRuntimeConfig()
const url = `${config.public.apiUrl}/api/auth/me`
console.log('[Auth Store] checkAuth() calling:', url)
// Get cookies from incoming request (for SSR) or use browser cookies (for client)
const headers: Record<string, string> = {}
if (import.meta.server) {
// Server-side: forward cookies from incoming request
const event = useRequestEvent()
const cookieHeader = event?.node.req.headers.cookie
if (cookieHeader) {
headers['Cookie'] = cookieHeader
console.log('[Auth Store] SSR: Forwarding cookies')
} else {
console.log('[Auth Store] SSR: No cookies to forward')
}
}
const response = await $fetch<{
user: DiscordUser
teams: Team[]
}>(url, {
credentials: 'include', // Send cookies (client-side)
headers, // Forward cookies (server-side)
})
console.log('[Auth Store] checkAuth() success:', response.user?.username)
user.value = response.user
teams.value = response.teams
return true
} catch (err: any) {
// Not authenticated or token expired
console.log('[Auth Store] checkAuth() failed:', err.message || err)
user.value = null
teams.value = []
return false
} finally {
isLoading.value = false
}
}
/**
* Redirect to backend OAuth initiation endpoint
* Backend handles state generation and Discord redirect
*/
function loginWithDiscord(returnUrl: string = '/') {
const config = useRuntimeConfig()
if (import.meta.client) {
const loginUrl = `${config.public.apiUrl}/api/auth/discord/login?return_url=${encodeURIComponent(
returnUrl
)}`
window.location.href = loginUrl
}
}
/**
* Logout user by clearing server-side cookies
*/
async function logout() {
isLoading.value = true
error.value = null
try {
const config = useRuntimeConfig()
await $fetch(`${config.public.apiUrl}/api/auth/logout`, {
method: 'POST',
credentials: 'include', // Send cookies
})
user.value = null
teams.value = []
// Redirect to home page
if (import.meta.client) {
navigateTo('/auth/login')
}
} catch (err: any) {
console.error('Logout failed:', err)
error.value = err.message || 'Logout failed'
} finally {
isLoading.value = false
}
}
/**
* Set user teams (loaded separately from login)
*/
function setTeams(userTeams: Team[]) {
teams.value = userTeams
}
/**
* Clear local auth state (does NOT clear server-side cookies)
*/
function clearAuth() {
user.value = null
teams.value = []
error.value = null
}
// ============================================================================
// Return Store API
// ============================================================================
return {
// State
user: readonly(user),
teams: readonly(teams),
isLoading: readonly(isLoading),
error: readonly(error),
// Getters
isAuthenticated,
currentUser,
userTeams,
userTeamIds,
userId,
// Actions
checkAuth,
loginWithDiscord,
logout,
setTeams,
clearAuth,
}
})