diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-13 23:46:16 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-13 23:46:16 +0900 |
| commit | 7258ca81812a24edd382438ce6e9ebc538549427 (patch) | |
| tree | 9bbc034be62777a2412d871211188268d7c56da4 /frontend/app/api/client.ts | |
| parent | 7757f26295cbf19c4d6fa068e2cb6bdc2589d01a (diff) | |
| download | phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.tar.gz phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.tar.zst phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.zip | |
feat(auth): store JWT in HTTP-only cookie instead of JS-accessible cookie
Prevent XSS-based token theft by making the JWT inaccessible to
JavaScript. The backend now sets/clears the cookie via Set-Cookie
headers, and the frontend retrieves user info from /api/me instead
of decoding the JWT directly.
- Add JWTCookieMiddleware to parse cookie and inject claims into context
- Add /me and /logout endpoints to OpenAPI spec and handlers
- Update PostLogin to return user object + Set-Cookie header
- Replace Authorization header auth with cookie-based auth throughout
- Rewrite frontend auth to use /api/me instead of jwt-decode
- Remove jwt-decode dependency
- Configure CORS with credentials for local dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'frontend/app/api/client.ts')
| -rw-r--r-- | frontend/app/api/client.ts | 37 |
1 files changed, 17 insertions, 20 deletions
diff --git a/frontend/app/api/client.ts b/frontend/app/api/client.ts index 86f2506..26c20d1 100644 --- a/frontend/app/api/client.ts +++ b/frontend/app/api/client.ts @@ -9,6 +9,7 @@ const apiOrigin = const client = createClient<paths>({ baseUrl: `${apiOrigin}${API_BASE_PATH}`, + credentials: "include", }); export async function apiLogin(username: string, password: string) { @@ -22,15 +23,20 @@ export async function apiLogin(username: string, password: string) { return data; } -class AuthenticatedApiClient { - constructor(public readonly token: string) {} +export async function apiLogout() { + const { error } = await client.POST("/logout"); + if (error) throw new Error(error.message); +} +export async function apiGetMe() { + const { data, error } = await client.GET("/me"); + if (error) return null; + return data; +} + +class AuthenticatedApiClient { async getGames() { - const { data, error } = await client.GET("/games", { - params: { - header: this._getAuthorizationHeader(), - }, - }); + const { data, error } = await client.GET("/games"); if (error) throw new Error(error.message); return data; } @@ -38,7 +44,6 @@ class AuthenticatedApiClient { async getGame(gameId: number) { const { data, error } = await client.GET("/games/{game_id}", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }); @@ -51,7 +56,6 @@ class AuthenticatedApiClient { "/games/{game_id}/play/latest_state", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }, @@ -63,7 +67,6 @@ class AuthenticatedApiClient { async postGamePlayCode(gameId: number, code: string) { const { error } = await client.POST("/games/{game_id}/play/code", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, body: { code }, @@ -74,7 +77,6 @@ class AuthenticatedApiClient { async postGamePlaySubmit(gameId: number, code: string) { const { data, error } = await client.POST("/games/{game_id}/play/submit", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, body: { code }, @@ -86,7 +88,6 @@ class AuthenticatedApiClient { async getGameWatchRanking(gameId: number) { const { data, error } = await client.GET("/games/{game_id}/watch/ranking", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }); @@ -99,7 +100,6 @@ class AuthenticatedApiClient { "/games/{game_id}/watch/latest_states", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }, @@ -117,21 +117,18 @@ class AuthenticatedApiClient { ) { const { data, error } = await client.GET("/tournament", { params: { - header: this._getAuthorizationHeader(), query: { game1, game2, game3, game4, game5 }, }, }); if (error) throw new Error(error.message); return data; } - - _getAuthorizationHeader() { - return { Authorization: `Bearer ${this.token}` }; - } } -export function createApiClient(token: string) { - return new AuthenticatedApiClient(token); +const apiClient = new AuthenticatedApiClient(); + +export function createApiClient() { + return apiClient; } export const ApiClientContext = createContext<AuthenticatedApiClient | null>( |
