import { defineStore } from 'pinia'; import { takeGameSessionTransfer, type GameSessionTransfer } from '@sammo-ts/common/auth/gameSessionTransfer'; import { gatewayTrpc } from '../utils/gatewayTrpc'; import { trpc as gameTrpc } from '../utils/trpc'; export type SessionStatus = 'unknown' | 'public' | 'authed' | 'general'; export interface SessionUser { id: string; username: string; displayName: string; } interface SessionState { status: SessionStatus; user: SessionUser | null; sessionToken: string | null; gameToken: string | null; profile: string | null; initializing: boolean; error: string | null; } const SESSION_TOKEN_KEY = 'sammo-session-token'; const PROFILE_KEY = 'sammo-game-profile'; const GAME_TOKEN_KEY = 'sammo-game-token'; const ACCESS_TOKEN_PREFIX = 'ga_'; const readStorage = (key: string): string | null => { if (typeof window === 'undefined') { return null; } return window.localStorage.getItem(key); }; const writeStorage = (key: string, value: string | null): void => { if (typeof window === 'undefined') { return; } if (value) { window.localStorage.setItem(key, value); } else { window.localStorage.removeItem(key); } }; const readQueryParam = (key: string): string | null => { if (typeof window === 'undefined') { return null; } const url = new URL(window.location.href); const value = url.searchParams.get(key); if (!value) { return null; } url.searchParams.delete(key); window.history.replaceState({}, '', url.toString()); return value; }; const readTransferredGameSession = (): GameSessionTransfer | null => { if (typeof window === 'undefined') { return null; } try { return takeGameSessionTransfer(window.sessionStorage); } catch { return null; } }; const isAccessToken = (token: string | null): boolean => { if (!token) { return false; } return token.startsWith(ACCESS_TOKEN_PREFIX); }; export const useSessionStore = defineStore('session', { state: (): SessionState => ({ status: 'unknown', user: null, sessionToken: null, gameToken: null, profile: null, initializing: false, error: null, }), getters: { isReady: (state) => state.status !== 'unknown', isAuthed: (state) => state.status === 'authed' || state.status === 'general', hasGeneral: (state) => state.status === 'general', needsGeneral: (state) => state.status === 'authed', }, actions: { setSessionToken(sessionToken: string | null) { this.sessionToken = sessionToken; writeStorage(SESSION_TOKEN_KEY, sessionToken); }, setProfile(profile: string | null) { this.profile = profile; writeStorage(PROFILE_KEY, profile); }, setGameToken(gameToken: string | null) { this.gameToken = gameToken; writeStorage(GAME_TOKEN_KEY, gameToken); }, clearSession() { this.user = null; this.setSessionToken(null); this.setGameToken(null); this.status = 'public'; }, leaveGame() { this.setGameToken(null); this.status = this.sessionToken ? 'authed' : 'public'; }, async refreshGeneralStatus() { if (!this.gameToken) { this.status = this.sessionToken ? 'authed' : 'public'; return; } if (!isAccessToken(this.gameToken)) { const exchanged = await this.exchangeGatewayToken(); if (!exchanged) { this.status = this.sessionToken ? 'authed' : 'public'; return; } } try { await gameTrpc.auth.status.query(); } catch { this.setGameToken(null); if (this.sessionToken && this.profile) { try { const issued = await gatewayTrpc.auth.issueGameSession.mutate({ sessionToken: this.sessionToken, profile: this.profile, }); this.setGameToken(issued.gameToken); if (await this.exchangeGatewayToken()) { await gameTrpc.auth.status.query(); } else { throw new Error('Game token exchange failed.'); } } catch { this.setGameToken(null); this.error = 'game_session_invalid'; this.status = 'public'; return; } } else { this.error = 'game_session_invalid'; this.status = 'public'; return; } } try { const lobby = await gameTrpc.lobby.info.query(); this.status = lobby.myGeneral ? 'general' : 'authed'; } catch { this.error = 'game_lobby_unavailable'; if (this.status === 'unknown' || this.status === 'public') { this.status = 'authed'; } } }, async exchangeGatewayToken(): Promise { if (!this.gameToken || isAccessToken(this.gameToken)) { return true; } try { const exchanged = await gameTrpc.auth.exchangeGatewayToken.mutate({ gatewayToken: this.gameToken, }); this.setGameToken(exchanged.accessToken); return true; } catch { this.error = 'game_token_exchange_failed'; this.setGameToken(null); return false; } }, async initialize() { if (this.initializing || this.status !== 'unknown') { return; } this.initializing = true; this.error = null; const tokenFromQuery = readQueryParam('sessionToken'); if (tokenFromQuery) { this.setSessionToken(tokenFromQuery); } const profileFromQuery = readQueryParam('profile'); if (profileFromQuery) { this.setProfile(profileFromQuery); } const gatewayTokenFromQuery = readQueryParam('gameToken'); if (gatewayTokenFromQuery) { this.setGameToken(gatewayTokenFromQuery); } const transferredGameSession = readTransferredGameSession(); if (transferredGameSession) { this.setProfile(transferredGameSession.profile); this.setGameToken(transferredGameSession.gatewayToken); } const storedToken = this.sessionToken ?? readStorage(SESSION_TOKEN_KEY); if (storedToken && storedToken !== this.sessionToken) { this.setSessionToken(storedToken); } const storedGameToken = this.gameToken ?? readStorage(GAME_TOKEN_KEY); if (storedGameToken && storedGameToken !== this.gameToken) { this.setGameToken(storedGameToken); } const storedProfile = this.profile ?? readStorage(PROFILE_KEY) ?? import.meta.env.VITE_GAME_PROFILE; if (storedProfile && storedProfile !== this.profile) { this.setProfile(storedProfile); } if (!this.sessionToken && !this.gameToken) { this.status = 'public'; this.initializing = false; return; } if (this.sessionToken) { try { const me = await gatewayTrpc.me.query(); if (!me) { this.clearSession(); this.initializing = false; return; } this.user = { id: me.id, username: me.username, displayName: me.displayName, }; this.status = 'authed'; } catch { this.error = 'gateway_unavailable'; this.status = 'public'; this.initializing = false; return; } } else { this.status = 'authed'; } if (!this.profile) { this.initializing = false; return; } try { if (!this.gameToken || !isAccessToken(this.gameToken)) { const sessionToken = this.sessionToken; if (sessionToken) { const issued = await gatewayTrpc.auth.issueGameSession.mutate({ sessionToken, profile: this.profile, }); this.setGameToken(issued.gameToken); } } await this.refreshGeneralStatus(); } catch { this.error = 'game_session_unavailable'; this.status = 'authed'; } finally { this.initializing = false; } }, }, });