From e4c57e24673229520a785ef8763e9f69cb70c680 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Tue, 30 Dec 2025 04:05:12 +0000 Subject: [PATCH] feat: add date-fns library for improved date handling and validation --- app/game-api/package.json | 1 + app/game-api/src/auth/tokenVerifier.ts | 10 ++++------ app/gateway-api/package.json | 1 + app/gateway-api/src/router.ts | 25 +++++++++++++------------ package.json | 2 +- pnpm-lock.yaml | 13 ++++++++++++- 6 files changed, 32 insertions(+), 20 deletions(-) diff --git a/app/game-api/package.json b/app/game-api/package.json index bd484d0..59f96fe 100644 --- a/app/game-api/package.json +++ b/app/game-api/package.json @@ -21,6 +21,7 @@ "@sammo-ts/infra": "workspace:*", "@sammo-ts/logic": "workspace:*", "@trpc/server": "^11.4.4", + "date-fns": "^4.1.0", "fastify": "^5.3.3", "redis": "^5.10.0", "zod": "^4.2.1" diff --git a/app/game-api/src/auth/tokenVerifier.ts b/app/game-api/src/auth/tokenVerifier.ts index f6d223f..832517d 100644 --- a/app/game-api/src/auth/tokenVerifier.ts +++ b/app/game-api/src/auth/tokenVerifier.ts @@ -1,5 +1,6 @@ import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken.js'; import { decryptGameSessionToken } from '@sammo-ts/common/auth/gameToken.js'; +import { isAfter, isValid, parseISO } from 'date-fns'; import type { FlushStore } from './flushStore.js'; @@ -8,11 +9,8 @@ export interface GameTokenVerifier { } const parseDate = (value: string): Date | null => { - const parsed = new Date(value); - if (Number.isNaN(parsed.getTime())) { - return null; - } - return parsed; + const parsed = parseISO(value); + return isValid(parsed) ? parsed : null; }; export const createGameTokenVerifier = (options: { @@ -34,7 +32,7 @@ export const createGameTokenVerifier = (options: { if (!expiresAt || !issuedAt) { return null; } - if (Date.now() > expiresAt.getTime()) { + if (isAfter(new Date(), expiresAt)) { return null; } const flushedAt = options.flushStore.getFlushedAt(payload.user.id); diff --git a/app/gateway-api/package.json b/app/gateway-api/package.json index 3b90b4b..edf831d 100644 --- a/app/gateway-api/package.json +++ b/app/gateway-api/package.json @@ -21,6 +21,7 @@ "@sammo-ts/common": "workspace:*", "@sammo-ts/infra": "workspace:*", "@trpc/server": "^11.4.4", + "date-fns": "^4.1.0", "fastify": "^5.3.3", "redis": "^5.10.0", "zod": "^4.2.1" diff --git a/app/gateway-api/src/router.ts b/app/gateway-api/src/router.ts index e124bf1..29aacfa 100644 --- a/app/gateway-api/src/router.ts +++ b/app/gateway-api/src/router.ts @@ -1,6 +1,7 @@ import { randomBytes } from 'node:crypto'; import { TRPCError } from '@trpc/server'; +import { addHours, addSeconds, isAfter, isValid, parseISO } from 'date-fns'; import { z } from 'zod'; import { @@ -18,11 +19,8 @@ const zProfile = z.string().min(1).max(64); const zOAuthMode = z.enum(['login', 'change_pw']); const parseDate = (value: string): Date | null => { - const parsed = new Date(value); - if (Number.isNaN(parsed.getTime())) { - return null; - } - return parsed; + const parsed = parseISO(value); + return isValid(parsed) ? parsed : null; }; export const appRouter = router({ @@ -67,11 +65,13 @@ export const appRouter = router({ }); } const token = await ctx.kakaoClient.exchangeCode(input.code); - const accessTokenValidUntil = new Date( - Date.now() + token.accessTokenExpiresIn * 1000 + const tokenIssuedAt = new Date(); + const accessTokenValidUntil = addSeconds( + tokenIssuedAt, + token.accessTokenExpiresIn ).toISOString(); const refreshTokenValidUntil = token.refreshTokenExpiresIn - ? new Date(Date.now() + token.refreshTokenExpiresIn * 1000).toISOString() + ? addSeconds(tokenIssuedAt, token.refreshTokenExpiresIn).toISOString() : undefined; const signupResult = await ctx.kakaoClient.signup(token.accessToken); @@ -117,7 +117,8 @@ export const appRouter = router({ const nextPasswordChange = existing.oauthInfo?.nextPasswordChange ? parseDate(existing.oauthInfo.nextPasswordChange) : null; - if (nextPasswordChange && Date.now() < nextPasswordChange.getTime()) { + const now = new Date(); + if (nextPasswordChange && isAfter(nextPasswordChange, now)) { throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: '비밀번호 초기화는 잠시 후 다시 시도해주세요.', @@ -129,7 +130,7 @@ export const appRouter = router({ `임시 비밀번호는 ${tempPassword} 입니다. 로그인 후 바로 다른 비밀번호로 변경해주세요.`, ctx.publicBaseUrl ); - const nextChange = new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(); + const nextChange = addHours(now, 4).toISOString(); await ctx.users.updatePassword(existing.id, tempPassword); await ctx.users.updateOAuthInfo(existing.id, { ...oauthInfo, @@ -321,7 +322,7 @@ export const appRouter = router({ version: 1, profile: gameSession.profile, issuedAt: now.toISOString(), - expiresAt: new Date(now.getTime() + 1000 * ctx.gameSessionTtlSeconds).toISOString(), + expiresAt: addSeconds(now, ctx.gameSessionTtlSeconds).toISOString(), sessionId: gameSession.gameToken, user: { id: gameSession.userId, @@ -366,7 +367,7 @@ export const appRouter = router({ return null; } const expiresAt = parseDate(payload.expiresAt); - if (!expiresAt || Date.now() > expiresAt.getTime()) { + if (!expiresAt || isAfter(new Date(), expiresAt)) { return null; } return { diff --git a/package.json b/package.json index f170859..37e4eac 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,6 @@ "devDependencies": { "@types/node": "^25.0.3", "tsdown": "^0.18.3", - "typescript": "^5.5.4" + "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f092a19..2f16e17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: specifier: ^0.18.3 version: 0.18.3(typescript@5.9.3) typescript: - specifier: ^5.5.4 + specifier: ^5.9.3 version: 5.9.3 app/game-api: @@ -35,6 +35,9 @@ importers: '@trpc/server': specifier: ^11.4.4 version: 11.8.1(typescript@5.9.3) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 fastify: specifier: ^5.3.3 version: 5.6.2 @@ -102,6 +105,9 @@ importers: '@trpc/server': specifier: ^11.4.4 version: 11.8.1(typescript@5.9.3) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 fastify: specifier: ^5.3.3 version: 5.6.2 @@ -878,6 +884,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2173,6 +2182,8 @@ snapshots: csstype@3.2.3: {} + date-fns@4.1.0: {} + debug@4.4.3: dependencies: ms: 2.1.3