import { randomUUID } from 'node:crypto'; import { parseJson } from '@sammo-ts/common'; import { createGatewayRedisKeyBuilder } from './redisKeys.js'; import type { GameSessionInfo, GatewaySessionConfig, GatewaySessionInfo, GatewaySessionService, SessionRevocationOptions, } from './sessionService.js'; import type { UserRecord } from './userRepository.js'; interface RedisGatewaySessionOptions extends GatewaySessionConfig { keyPrefix: string; } interface RedisPipeline { set(key: string, value: string, options?: { EX?: number }): RedisPipeline; sAdd(key: string, member: string): RedisPipeline; expire(key: string, seconds: number): RedisPipeline; del(key: string): RedisPipeline; exec(): Promise; } interface RedisClientLike { get(key: string): Promise; set(key: string, value: string, options?: { EX?: number }): Promise; sMembers(key: string): Promise; multi(): RedisPipeline; del(key: string): Promise; } // Redis 세션 저장소는 게이트웨이와 게임 서버 간 SSO 토큰을 관리한다. export class RedisGatewaySessionService implements GatewaySessionService { private readonly client: RedisClientLike; private readonly keys: ReturnType; private readonly sessionTtlSeconds: number; private readonly gameSessionTtlSeconds: number; constructor(client: RedisClientLike, options: RedisGatewaySessionOptions) { this.client = client; this.keys = createGatewayRedisKeyBuilder(options.keyPrefix); this.sessionTtlSeconds = options.sessionTtlSeconds; this.gameSessionTtlSeconds = options.gameSessionTtlSeconds; } async createSession(user: UserRecord): Promise { const sessionToken = randomUUID(); const info: GatewaySessionInfo = { sessionToken, userId: user.id, username: user.username, displayName: user.displayName, roles: user.roles, sanctions: user.sanctions, createdAt: user.createdAt, issuedAt: new Date().toISOString(), legacyMemberNo: user.legacyMemberNo, }; await this.client.set(this.keys.sessionKey(sessionToken), JSON.stringify(info), { EX: this.sessionTtlSeconds, }); return info; } async getSession(sessionToken: string): Promise { const raw = await this.client.get(this.keys.sessionKey(sessionToken)); return parseJson(raw); } async revokeSession( sessionToken: string, options: SessionRevocationOptions = { revokeGames: true } ): Promise { const key = this.keys.sessionKey(sessionToken); if (options.revokeGames ?? true) { const gameSetKey = this.keys.sessionGameSetKey(sessionToken); const games = await this.client.sMembers(gameSetKey); if (games.length > 0) { const pipeline = this.client.multi(); for (const entry of games) { pipeline.del(entry); } pipeline.del(gameSetKey); pipeline.del(key); await pipeline.exec(); return; } await this.client.del(gameSetKey); } await this.client.del(key); } async createGameSession(sessionToken: string, profile: string): Promise { const session = await this.getSession(sessionToken); if (!session) { return null; } const gameToken = randomUUID(); const info: GameSessionInfo = { profile, gameToken, sessionToken, userId: session.userId, username: session.username, displayName: session.displayName, roles: session.roles, sanctions: session.sanctions, createdAt: session.createdAt, issuedAt: new Date().toISOString(), legacyMemberNo: session.legacyMemberNo, }; const gameKey = this.keys.gameSessionKey(profile, gameToken); const gameSetKey = this.keys.sessionGameSetKey(sessionToken); await this.client .multi() .set(gameKey, JSON.stringify(info), { EX: this.gameSessionTtlSeconds }) .sAdd(gameSetKey, gameKey) .expire(gameSetKey, this.sessionTtlSeconds) .exec(); return info; } async getGameSession(profile: string, gameToken: string): Promise { const raw = await this.client.get(this.keys.gameSessionKey(profile, gameToken)); return parseJson(raw); } }