feat: Implement session management and routing for general creation

- Added session initialization in main.ts to manage user sessions.
- Updated router to include a new route for joining or creating a general.
- Enhanced session store with methods for managing session tokens and user profiles.
- Introduced new components for displaying city, general, nation, and command information.
- Implemented loading states and error handling in the main view.
- Created a skeleton loader component for better user experience during data fetching.
- Updated TypeScript definitions for route metadata to include new authentication requirements.
This commit is contained in:
2026-01-16 15:57:56 +00:00
parent 15625f2c28
commit f8159728ca
20 changed files with 1342 additions and 18 deletions
+165 -2
View File
@@ -1,23 +1,186 @@
import { defineStore } from 'pinia';
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 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;
};
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: {
setStatus(status: SessionStatus) {
this.status = status;
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';
},
async refreshGeneralStatus() {
if (!this.gameToken) {
this.status = 'authed';
return;
}
try {
const lobby = await gameTrpc.lobby.info.query();
this.status = lobby.myGeneral ? 'general' : 'authed';
} catch {
this.error = 'game_status_unavailable';
}
},
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 storedToken = this.sessionToken ?? readStorage(SESSION_TOKEN_KEY);
if (storedToken && storedToken !== this.sessionToken) {
this.setSessionToken(storedToken);
}
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.status = 'public';
this.initializing = false;
return;
}
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;
}
if (!this.profile) {
this.initializing = false;
return;
}
try {
const sessionToken = this.sessionToken;
if (!sessionToken) {
this.status = 'public';
this.initializing = false;
return;
}
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;
}
},
},
});