import { randomUUID } from 'node:crypto'; import { parseJson } from '@sammo-ts/common'; export type OAuthMode = 'login' | 'change_pw'; export interface OAuthPendingState { state: string; mode: OAuthMode; scopes: string[]; createdAt: string; } export interface OAuthSession { id: string; mode: OAuthMode; kakaoId: string; email: string; accessToken: string; refreshToken?: string; accessTokenValidUntil: string; refreshTokenValidUntil?: string; createdAt: string; } export interface OAuthSessionStore { createPendingState(mode: OAuthMode, scopes: string[]): Promise; consumePendingState(state: string): Promise; createSession(session: Omit): Promise; consumeSession(sessionId: string): Promise; } interface RedisPipeline { set(key: string, value: string, options?: { EX?: number }): RedisPipeline; del(key: string): RedisPipeline; exec(): Promise; } interface RedisClientLike { get(key: string): Promise; set(key: string, value: string, options?: { EX?: number }): Promise; del(key: string): Promise; multi(): RedisPipeline; } export class RedisOAuthSessionStore implements OAuthSessionStore { private readonly client: RedisClientLike; private readonly prefix: string; private readonly ttlSeconds: number; constructor(client: RedisClientLike, prefix: string, ttlSeconds: number) { this.client = client; this.prefix = prefix; this.ttlSeconds = ttlSeconds; } private stateKey(state: string): string { return `${this.prefix}:oauth-state:${state}`; } private sessionKey(sessionId: string): string { return `${this.prefix}:oauth-session:${sessionId}`; } async createPendingState(mode: OAuthMode, scopes: string[]): Promise { const state: OAuthPendingState = { state: randomUUID(), mode, scopes, createdAt: new Date().toISOString(), }; await this.client.set(this.stateKey(state.state), JSON.stringify(state), { EX: this.ttlSeconds, }); return state; } async consumePendingState(state: string): Promise { const key = this.stateKey(state); const raw = await this.client.get(key); if (!raw) { return null; } await this.client.del(key); return parseJson(raw); } async createSession(session: Omit): Promise { const stored: OAuthSession = { ...session, id: randomUUID(), }; await this.client.set(this.sessionKey(stored.id), JSON.stringify(stored), { EX: this.ttlSeconds, }); return stored; } async consumeSession(sessionId: string): Promise { const key = this.sessionKey(sessionId); const raw = await this.client.get(key); if (!raw) { return null; } await this.client.del(key); return parseJson(raw); } } // 테스트용 인메모리 OAuth 세션 저장소. export class InMemoryOAuthSessionStore implements OAuthSessionStore { private readonly pendingStates = new Map(); private readonly sessions = new Map(); async createPendingState(mode: OAuthMode, scopes: string[]): Promise { const pending: OAuthPendingState = { state: randomUUID(), mode, scopes, createdAt: new Date().toISOString(), }; this.pendingStates.set(pending.state, pending); return pending; } async consumePendingState(state: string): Promise { const pending = this.pendingStates.get(state) ?? null; if (pending) { this.pendingStates.delete(state); } return pending; } async createSession(session: Omit): Promise { const stored: OAuthSession = { ...session, id: randomUUID(), }; this.sessions.set(stored.id, stored); return stored; } async consumeSession(sessionId: string): Promise { const session = this.sessions.get(sessionId) ?? null; if (session) { this.sessions.delete(sessionId); } return session; } }