UNPKG

@gftdcojp/gftd-orm

Version:

Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture

565 lines (559 loc) 18.3 kB
/** * Auth0 Custom Session Store * * セッションストアの抽象化とカスタム実装 * Database、Redis、Memory、File system対応 */ import { log } from './utils/logger'; import { AuditLogManager, AuditLogLevel, AuditEventType } from './types'; /** * 🗄️ Memory Session Store * 開発・テスト用のインメモリストア */ export class MemorySessionStore { constructor(options = {}) { this.options = options; this.sessions = new Map(); this.userSessions = new Map(); this.cleanupInterval = null; // 自動クリーンアップを開始 if (this.options.cleanupIntervalMs) { this.startCleanup(); } } async save(sessionId, session) { this.sessions.set(sessionId, { ...session, sessionId }); // ユーザーセッションマップを更新 if (!this.userSessions.has(session.userId)) { this.userSessions.set(session.userId, new Set()); } this.userSessions.get(session.userId).add(sessionId); log.debug(`Session saved: ${sessionId}`); } async get(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return null; } // 有効期限チェック if (Date.now() > session.expiresAt * 1000) { await this.delete(sessionId); return null; } // 最終アクセス時刻を更新 session.lastAccessedAt = Math.floor(Date.now() / 1000); return session; } async delete(sessionId) { const session = this.sessions.get(sessionId); if (session) { this.sessions.delete(sessionId); // ユーザーセッションマップからも削除 const userSessions = this.userSessions.get(session.userId); if (userSessions) { userSessions.delete(sessionId); if (userSessions.size === 0) { this.userSessions.delete(session.userId); } } } } async update(sessionId, updates) { const session = this.sessions.get(sessionId); if (session) { Object.assign(session, updates); log.debug(`Session updated: ${sessionId}`); } } async getByUserId(userId) { const sessionIds = this.userSessions.get(userId); if (!sessionIds) { return []; } const sessions = []; for (const sessionId of sessionIds) { const session = await this.get(sessionId); if (session) { sessions.push(session); } } return sessions; } async deleteByUserId(userId) { const sessionIds = this.userSessions.get(userId); if (sessionIds) { for (const sessionId of sessionIds) { await this.delete(sessionId); } } } async cleanup() { const now = Date.now(); let cleanedCount = 0; for (const [sessionId, session] of this.sessions) { if (now > session.expiresAt * 1000) { await this.delete(sessionId); cleanedCount++; } } if (cleanedCount > 0) { log.info(`Cleaned up ${cleanedCount} expired sessions`); } } async health() { return { status: 'ok', message: `${this.sessions.size} sessions in memory`, }; } startCleanup() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.cleanupInterval = setInterval(async () => { await this.cleanup(); }, this.options.cleanupIntervalMs); } /** * ストアを終了 */ destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.sessions.clear(); this.userSessions.clear(); } } /** * 🗃️ Database Session Store * PostgreSQL、MySQL、SQLite対応 */ export class DatabaseSessionStore { constructor(db, // Database connection options = {}) { this.db = db; this.options = options; this.options = { tableName: 'auth0_sessions', schemaName: 'public', ...options, }; } get tableName() { return this.options.schemaName ? `${this.options.schemaName}.${this.options.tableName}` : this.options.tableName; } async save(sessionId, session) { const query = ` INSERT INTO ${this.tableName} ( session_id, user_id, access_token, id_token, refresh_token, expires_at, created_at, last_accessed_at, user_data, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (session_id) DO UPDATE SET access_token = EXCLUDED.access_token, id_token = EXCLUDED.id_token, refresh_token = EXCLUDED.refresh_token, expires_at = EXCLUDED.expires_at, last_accessed_at = EXCLUDED.last_accessed_at, user_data = EXCLUDED.user_data, metadata = EXCLUDED.metadata `; await this.db.query(query, [ sessionId, session.userId, session.accessToken, session.idToken, session.refreshToken, new Date(session.expiresAt * 1000), new Date(session.createdAt * 1000), new Date(session.lastAccessedAt * 1000), JSON.stringify(session.user), JSON.stringify(session.metadata || {}), ]); log.debug(`Session saved to database: ${sessionId}`); } async get(sessionId) { const query = ` SELECT * FROM ${this.tableName} WHERE session_id = ? AND expires_at > NOW() `; const results = await this.db.query(query, [sessionId]); if (results.length === 0) { return null; } const row = results[0]; // 最終アクセス時刻を更新 await this.updateLastAccessed(sessionId); return { sessionId: row.session_id, userId: row.user_id, accessToken: row.access_token, idToken: row.id_token, refreshToken: row.refresh_token, expiresAt: Math.floor(row.expires_at.getTime() / 1000), createdAt: Math.floor(row.created_at.getTime() / 1000), lastAccessedAt: Math.floor(Date.now() / 1000), user: JSON.parse(row.user_data), metadata: JSON.parse(row.metadata || '{}'), }; } async delete(sessionId) { const query = `DELETE FROM ${this.tableName} WHERE session_id = ?`; await this.db.query(query, [sessionId]); } async update(sessionId, updates) { const setParts = []; const values = []; if (updates.accessToken) { setParts.push('access_token = ?'); values.push(updates.accessToken); } if (updates.idToken) { setParts.push('id_token = ?'); values.push(updates.idToken); } if (updates.refreshToken) { setParts.push('refresh_token = ?'); values.push(updates.refreshToken); } if (updates.expiresAt) { setParts.push('expires_at = ?'); values.push(new Date(updates.expiresAt * 1000)); } if (updates.user) { setParts.push('user_data = ?'); values.push(JSON.stringify(updates.user)); } if (updates.metadata) { setParts.push('metadata = ?'); values.push(JSON.stringify(updates.metadata)); } if (setParts.length === 0) { return; } setParts.push('last_accessed_at = NOW()'); values.push(sessionId); const query = ` UPDATE ${this.tableName} SET ${setParts.join(', ')} WHERE session_id = ? `; await this.db.query(query, values); } async getByUserId(userId) { const query = ` SELECT * FROM ${this.tableName} WHERE user_id = ? AND expires_at > NOW() `; const results = await this.db.query(query, [userId]); return results.map((row) => ({ sessionId: row.session_id, userId: row.user_id, accessToken: row.access_token, idToken: row.id_token, refreshToken: row.refresh_token, expiresAt: Math.floor(row.expires_at.getTime() / 1000), createdAt: Math.floor(row.created_at.getTime() / 1000), lastAccessedAt: Math.floor(row.last_accessed_at.getTime() / 1000), user: JSON.parse(row.user_data), metadata: JSON.parse(row.metadata || '{}'), })); } async deleteByUserId(userId) { const query = `DELETE FROM ${this.tableName} WHERE user_id = ?`; const result = await this.db.query(query, [userId]); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.AUTH_LOGOUT, userId, result: 'SUCCESS', message: `All sessions deleted for user`, details: { deletedCount: result.affectedRows }, }); } async cleanup() { const query = `DELETE FROM ${this.tableName} WHERE expires_at <= NOW()`; const result = await this.db.query(query); if (result.affectedRows > 0) { log.info(`Cleaned up ${result.affectedRows} expired sessions from database`); } } async health() { try { const query = `SELECT COUNT(*) as count FROM ${this.tableName}`; const results = await this.db.query(query); return { status: 'ok', message: `${results[0].count} sessions in database`, }; } catch (error) { return { status: 'error', message: `Database health check failed: ${error}`, }; } } async updateLastAccessed(sessionId) { const query = ` UPDATE ${this.tableName} SET last_accessed_at = NOW() WHERE session_id = ? `; await this.db.query(query, [sessionId]); } /** * Database schema creation */ async createSchema() { const query = ` CREATE TABLE IF NOT EXISTS ${this.tableName} ( session_id VARCHAR(255) PRIMARY KEY, user_id VARCHAR(255) NOT NULL, access_token TEXT NOT NULL, id_token TEXT NOT NULL, refresh_token TEXT, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL, last_accessed_at TIMESTAMP NOT NULL, user_data TEXT NOT NULL, metadata TEXT DEFAULT '{}', INDEX idx_user_id (user_id), INDEX idx_expires_at (expires_at), INDEX idx_last_accessed_at (last_accessed_at) ) `; await this.db.query(query); log.info(`Session table created: ${this.tableName}`); } } /** * 🔴 Redis Session Store * Redis対応のセッションストア */ export class RedisSessionStore { constructor(redis, // Redis client options = {}) { this.redis = redis; this.options = options; this.options = { keyPrefix: 'auth0:session:', defaultTtl: 7 * 24 * 60 * 60, // 7日 ...options, }; } sessionKey(sessionId) { return `${this.options.keyPrefix}${sessionId}`; } userSessionsKey(userId) { return `${this.options.keyPrefix}user:${userId}`; } async save(sessionId, session) { const key = this.sessionKey(sessionId); const userKey = this.userSessionsKey(session.userId); const ttl = session.expiresAt - Math.floor(Date.now() / 1000); // Pipeline for atomic operations const pipeline = this.redis.pipeline(); // Save session data pipeline.setex(key, ttl, JSON.stringify(session)); // Add session to user's session set pipeline.sadd(userKey, sessionId); pipeline.expire(userKey, ttl); await pipeline.exec(); log.debug(`Session saved to Redis: ${sessionId}`); } async get(sessionId) { const key = this.sessionKey(sessionId); const data = await this.redis.get(key); if (!data) { return null; } try { const session = JSON.parse(data); // 最終アクセス時刻を更新 session.lastAccessedAt = Math.floor(Date.now() / 1000); // セッションを再保存(TTL更新) const ttl = session.expiresAt - Math.floor(Date.now() / 1000); if (ttl > 0) { await this.redis.setex(key, ttl, JSON.stringify(session)); } return session; } catch (error) { log.error(`Failed to parse session data: ${error}`); await this.redis.del(key); return null; } } async delete(sessionId) { const key = this.sessionKey(sessionId); // セッションデータを取得してユーザーIDを確認 const data = await this.redis.get(key); if (data) { try { const session = JSON.parse(data); const userKey = this.userSessionsKey(session.userId); // Pipeline for atomic operations const pipeline = this.redis.pipeline(); pipeline.del(key); pipeline.srem(userKey, sessionId); await pipeline.exec(); } catch (error) { log.error(`Failed to parse session data during deletion: ${error}`); await this.redis.del(key); } } } async update(sessionId, updates) { const session = await this.get(sessionId); if (!session) { return; } const updatedSession = { ...session, ...updates }; await this.save(sessionId, updatedSession); } async getByUserId(userId) { const userKey = this.userSessionsKey(userId); const sessionIds = await this.redis.smembers(userKey); if (sessionIds.length === 0) { return []; } const sessions = []; for (const sessionId of sessionIds) { const session = await this.get(sessionId); if (session) { sessions.push(session); } } return sessions; } async deleteByUserId(userId) { const userKey = this.userSessionsKey(userId); const sessionIds = await this.redis.smembers(userKey); if (sessionIds.length === 0) { return; } // Pipeline for atomic operations const pipeline = this.redis.pipeline(); for (const sessionId of sessionIds) { pipeline.del(this.sessionKey(sessionId)); } pipeline.del(userKey); await pipeline.exec(); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.AUTH_LOGOUT, userId, result: 'SUCCESS', message: `All sessions deleted for user`, details: { deletedCount: sessionIds.length }, }); } async cleanup() { // Redis TTLによる自動削除に依存 log.debug('Redis sessions cleanup - relying on TTL expiration'); } async health() { try { await this.redis.ping(); // セッション数をカウント const pattern = `${this.options.keyPrefix}*`; const keys = await this.redis.keys(pattern); return { status: 'ok', message: `${keys.length} sessions in Redis`, }; } catch (error) { return { status: 'error', message: `Redis health check failed: ${error}`, }; } } } /** * 🔧 Session Store Factory */ export class SessionStoreFactory { /** * Memory store を作成 */ static createMemoryStore(options) { return new MemorySessionStore(options); } /** * Database store を作成 */ static createDatabaseStore(db, options) { return new DatabaseSessionStore(db, options); } /** * Redis store を作成 */ static createRedisStore(redis, options) { return new RedisSessionStore(redis, options); } /** * 設定から適切なストアを作成 */ static createFromConfig(config) { switch (config.type) { case 'memory': return this.createMemoryStore(config.options); case 'database': return this.createDatabaseStore(config.connection, config.options); case 'redis': return this.createRedisStore(config.connection, config.options); default: throw new Error(`Unknown session store type: ${config.type}`); } } } /** * 🎯 Session Store 使用例 */ export const sessionStoreExamples = { memory: ` // Memory Store (開発用) const memoryStore = SessionStoreFactory.createMemoryStore({ cleanupIntervalMs: 5 * 60 * 1000, // 5分毎にクリーンアップ }); `, database: ` // Database Store (本番用) import mysql from 'mysql2/promise'; const db = await mysql.createConnection({ host: 'localhost', user: 'auth0', password: 'password', database: 'sessions', }); const dbStore = SessionStoreFactory.createDatabaseStore(db, { tableName: 'auth0_sessions', schemaName: 'auth', }); // テーブル作成 await dbStore.createSchema(); `, redis: ` // Redis Store (本番用) import Redis from 'ioredis'; const redis = new Redis({ host: 'localhost', port: 6379, password: 'password', }); const redisStore = SessionStoreFactory.createRedisStore(redis, { keyPrefix: 'app:auth0:session:', defaultTtl: 7 * 24 * 60 * 60, // 7日 }); `, }; //# sourceMappingURL=auth0-session-store.js.map