- 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>
175 lines
4.8 KiB
TypeScript
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,
|
|
}
|
|
})
|