aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/api/client.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-13 23:46:16 +0900
committernsfisis <nsfisis@gmail.com>2026-02-13 23:46:16 +0900
commit7258ca81812a24edd382438ce6e9ebc538549427 (patch)
tree9bbc034be62777a2412d871211188268d7c56da4 /frontend/app/api/client.ts
parent7757f26295cbf19c4d6fa068e2cb6bdc2589d01a (diff)
downloadphperkaigi-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.ts37
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>(