76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
|
|
// JWT 토큰 무효화 서비스
|
||
|
|
// user_info.token_version 기반으로 기존 JWT 토큰을 무효화
|
||
|
|
|
||
|
|
import { query } from "../database/db";
|
||
|
|
import { cache } from "../utils/cache";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
const TOKEN_VERSION_CACHE_TTL = 2 * 60 * 1000; // 2분 캐시
|
||
|
|
|
||
|
|
export class TokenInvalidationService {
|
||
|
|
/**
|
||
|
|
* 캐시 키 생성
|
||
|
|
*/
|
||
|
|
static cacheKey(userId: string): string {
|
||
|
|
return `token_version:${userId}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 단일 사용자의 토큰 무효화 (token_version +1)
|
||
|
|
*/
|
||
|
|
static async invalidateUserTokens(userId: string): Promise<void> {
|
||
|
|
try {
|
||
|
|
await query(
|
||
|
|
`UPDATE user_info SET token_version = COALESCE(token_version, 0) + 1 WHERE user_id = $1`,
|
||
|
|
[userId]
|
||
|
|
);
|
||
|
|
cache.delete(this.cacheKey(userId));
|
||
|
|
logger.info(`토큰 무효화: ${userId}`);
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`토큰 무효화 실패: ${userId}`, { error });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 여러 사용자의 토큰 일괄 무효화
|
||
|
|
*/
|
||
|
|
static async invalidateMultipleUserTokens(userIds: string[]): Promise<void> {
|
||
|
|
if (userIds.length === 0) return;
|
||
|
|
try {
|
||
|
|
const placeholders = userIds.map((_, i) => `$${i + 1}`).join(", ");
|
||
|
|
await query(
|
||
|
|
`UPDATE user_info SET token_version = COALESCE(token_version, 0) + 1 WHERE user_id IN (${placeholders})`,
|
||
|
|
userIds
|
||
|
|
);
|
||
|
|
userIds.forEach((id) => cache.delete(this.cacheKey(id)));
|
||
|
|
logger.info(`토큰 일괄 무효화: ${userIds.length}명`);
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`토큰 일괄 무효화 실패`, { error, userIds });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 현재 token_version 조회 (캐시 사용)
|
||
|
|
*/
|
||
|
|
static async getUserTokenVersion(userId: string): Promise<number> {
|
||
|
|
const cacheKey = this.cacheKey(userId);
|
||
|
|
const cached = cache.get<number>(cacheKey);
|
||
|
|
if (cached !== null) {
|
||
|
|
return cached;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await query<{ token_version: number | null }>(
|
||
|
|
`SELECT token_version FROM user_info WHERE user_id = $1`,
|
||
|
|
[userId]
|
||
|
|
);
|
||
|
|
const version = result.length > 0 ? (result[0].token_version ?? 0) : 0;
|
||
|
|
cache.set(cacheKey, version, TOKEN_VERSION_CACHE_TTL);
|
||
|
|
return version;
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`token_version 조회 실패: ${userId}`, { error });
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|