286 lines
9.4 KiB
TypeScript
286 lines
9.4 KiB
TypeScript
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<boolean> {
|
|
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;
|
|
}
|
|
},
|
|
},
|
|
});
|