130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
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<unknown>;
|
|
}
|
|
|
|
interface RedisClientLike {
|
|
get(key: string): Promise<string | null>;
|
|
set(key: string, value: string, options?: { EX?: number }): Promise<unknown>;
|
|
sMembers(key: string): Promise<string[]>;
|
|
multi(): RedisPipeline;
|
|
del(key: string): Promise<number>;
|
|
}
|
|
|
|
// Redis 세션 저장소는 게이트웨이와 게임 서버 간 SSO 토큰을 관리한다.
|
|
export class RedisGatewaySessionService implements GatewaySessionService {
|
|
private readonly client: RedisClientLike;
|
|
private readonly keys: ReturnType<typeof createGatewayRedisKeyBuilder>;
|
|
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<GatewaySessionInfo> {
|
|
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<GatewaySessionInfo | null> {
|
|
const raw = await this.client.get(this.keys.sessionKey(sessionToken));
|
|
return parseJson<GatewaySessionInfo>(raw);
|
|
}
|
|
|
|
async revokeSession(
|
|
sessionToken: string,
|
|
options: SessionRevocationOptions = { revokeGames: true }
|
|
): Promise<void> {
|
|
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<GameSessionInfo | null> {
|
|
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<GameSessionInfo | null> {
|
|
const raw = await this.client.get(this.keys.gameSessionKey(profile, gameToken));
|
|
return parseJson<GameSessionInfo>(raw);
|
|
}
|
|
}
|