feat: add date-fns library for improved date handling and validation
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"@sammo-ts/infra": "workspace:*",
|
"@sammo-ts/infra": "workspace:*",
|
||||||
"@sammo-ts/logic": "workspace:*",
|
"@sammo-ts/logic": "workspace:*",
|
||||||
"@trpc/server": "^11.4.4",
|
"@trpc/server": "^11.4.4",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"fastify": "^5.3.3",
|
"fastify": "^5.3.3",
|
||||||
"redis": "^5.10.0",
|
"redis": "^5.10.0",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.2.1"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken.js';
|
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken.js';
|
||||||
import { decryptGameSessionToken } 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';
|
import type { FlushStore } from './flushStore.js';
|
||||||
|
|
||||||
@@ -8,11 +9,8 @@ export interface GameTokenVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parseDate = (value: string): Date | null => {
|
const parseDate = (value: string): Date | null => {
|
||||||
const parsed = new Date(value);
|
const parsed = parseISO(value);
|
||||||
if (Number.isNaN(parsed.getTime())) {
|
return isValid(parsed) ? parsed : null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createGameTokenVerifier = (options: {
|
export const createGameTokenVerifier = (options: {
|
||||||
@@ -34,7 +32,7 @@ export const createGameTokenVerifier = (options: {
|
|||||||
if (!expiresAt || !issuedAt) {
|
if (!expiresAt || !issuedAt) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (Date.now() > expiresAt.getTime()) {
|
if (isAfter(new Date(), expiresAt)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const flushedAt = options.flushStore.getFlushedAt(payload.user.id);
|
const flushedAt = options.flushStore.getFlushedAt(payload.user.id);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@sammo-ts/common": "workspace:*",
|
"@sammo-ts/common": "workspace:*",
|
||||||
"@sammo-ts/infra": "workspace:*",
|
"@sammo-ts/infra": "workspace:*",
|
||||||
"@trpc/server": "^11.4.4",
|
"@trpc/server": "^11.4.4",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"fastify": "^5.3.3",
|
"fastify": "^5.3.3",
|
||||||
"redis": "^5.10.0",
|
"redis": "^5.10.0",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.2.1"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
import { addHours, addSeconds, isAfter, isValid, parseISO } from 'date-fns';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,11 +19,8 @@ const zProfile = z.string().min(1).max(64);
|
|||||||
const zOAuthMode = z.enum(['login', 'change_pw']);
|
const zOAuthMode = z.enum(['login', 'change_pw']);
|
||||||
|
|
||||||
const parseDate = (value: string): Date | null => {
|
const parseDate = (value: string): Date | null => {
|
||||||
const parsed = new Date(value);
|
const parsed = parseISO(value);
|
||||||
if (Number.isNaN(parsed.getTime())) {
|
return isValid(parsed) ? parsed : null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
@@ -67,11 +65,13 @@ export const appRouter = router({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const token = await ctx.kakaoClient.exchangeCode(input.code);
|
const token = await ctx.kakaoClient.exchangeCode(input.code);
|
||||||
const accessTokenValidUntil = new Date(
|
const tokenIssuedAt = new Date();
|
||||||
Date.now() + token.accessTokenExpiresIn * 1000
|
const accessTokenValidUntil = addSeconds(
|
||||||
|
tokenIssuedAt,
|
||||||
|
token.accessTokenExpiresIn
|
||||||
).toISOString();
|
).toISOString();
|
||||||
const refreshTokenValidUntil = token.refreshTokenExpiresIn
|
const refreshTokenValidUntil = token.refreshTokenExpiresIn
|
||||||
? new Date(Date.now() + token.refreshTokenExpiresIn * 1000).toISOString()
|
? addSeconds(tokenIssuedAt, token.refreshTokenExpiresIn).toISOString()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const signupResult = await ctx.kakaoClient.signup(token.accessToken);
|
const signupResult = await ctx.kakaoClient.signup(token.accessToken);
|
||||||
@@ -117,7 +117,8 @@ export const appRouter = router({
|
|||||||
const nextPasswordChange = existing.oauthInfo?.nextPasswordChange
|
const nextPasswordChange = existing.oauthInfo?.nextPasswordChange
|
||||||
? parseDate(existing.oauthInfo.nextPasswordChange)
|
? parseDate(existing.oauthInfo.nextPasswordChange)
|
||||||
: null;
|
: null;
|
||||||
if (nextPasswordChange && Date.now() < nextPasswordChange.getTime()) {
|
const now = new Date();
|
||||||
|
if (nextPasswordChange && isAfter(nextPasswordChange, now)) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'TOO_MANY_REQUESTS',
|
code: 'TOO_MANY_REQUESTS',
|
||||||
message: '비밀번호 초기화는 잠시 후 다시 시도해주세요.',
|
message: '비밀번호 초기화는 잠시 후 다시 시도해주세요.',
|
||||||
@@ -129,7 +130,7 @@ export const appRouter = router({
|
|||||||
`임시 비밀번호는 ${tempPassword} 입니다. 로그인 후 바로 다른 비밀번호로 변경해주세요.`,
|
`임시 비밀번호는 ${tempPassword} 입니다. 로그인 후 바로 다른 비밀번호로 변경해주세요.`,
|
||||||
ctx.publicBaseUrl
|
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.updatePassword(existing.id, tempPassword);
|
||||||
await ctx.users.updateOAuthInfo(existing.id, {
|
await ctx.users.updateOAuthInfo(existing.id, {
|
||||||
...oauthInfo,
|
...oauthInfo,
|
||||||
@@ -321,7 +322,7 @@ export const appRouter = router({
|
|||||||
version: 1,
|
version: 1,
|
||||||
profile: gameSession.profile,
|
profile: gameSession.profile,
|
||||||
issuedAt: now.toISOString(),
|
issuedAt: now.toISOString(),
|
||||||
expiresAt: new Date(now.getTime() + 1000 * ctx.gameSessionTtlSeconds).toISOString(),
|
expiresAt: addSeconds(now, ctx.gameSessionTtlSeconds).toISOString(),
|
||||||
sessionId: gameSession.gameToken,
|
sessionId: gameSession.gameToken,
|
||||||
user: {
|
user: {
|
||||||
id: gameSession.userId,
|
id: gameSession.userId,
|
||||||
@@ -366,7 +367,7 @@ export const appRouter = router({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const expiresAt = parseDate(payload.expiresAt);
|
const expiresAt = parseDate(payload.expiresAt);
|
||||||
if (!expiresAt || Date.now() > expiresAt.getTime()) {
|
if (!expiresAt || isAfter(new Date(), expiresAt)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -14,6 +14,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"tsdown": "^0.18.3",
|
"tsdown": "^0.18.3",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
-1
@@ -15,7 +15,7 @@ importers:
|
|||||||
specifier: ^0.18.3
|
specifier: ^0.18.3
|
||||||
version: 0.18.3(typescript@5.9.3)
|
version: 0.18.3(typescript@5.9.3)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.4
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
app/game-api:
|
app/game-api:
|
||||||
@@ -35,6 +35,9 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.4.4
|
specifier: ^11.4.4
|
||||||
version: 11.8.1(typescript@5.9.3)
|
version: 11.8.1(typescript@5.9.3)
|
||||||
|
date-fns:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
fastify:
|
fastify:
|
||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.6.2
|
version: 5.6.2
|
||||||
@@ -102,6 +105,9 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.4.4
|
specifier: ^11.4.4
|
||||||
version: 11.8.1(typescript@5.9.3)
|
version: 11.8.1(typescript@5.9.3)
|
||||||
|
date-fns:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
fastify:
|
fastify:
|
||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.6.2
|
version: 5.6.2
|
||||||
@@ -878,6 +884,9 @@ packages:
|
|||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
|
date-fns@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -2173,6 +2182,8 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|||||||
Reference in New Issue
Block a user