import { createClient } from 'redis'; import { config } from '../config/env'; import logger from '../utils/logger'; export interface SessionData { userId: string; email: string; role: string; refreshToken: string; lastActivity: number; } class SessionService { private client: any; private isConnected: boolean = false; constructor() { this.client = createClient({ url: config.redis.url, socket: { host: config.redis.host, port: config.redis.port, reconnectStrategy: (retries) => { if (retries > 10) { logger.error('Redis connection failed after 10 retries'); return new Error('Redis connection failed'); } return Math.min(retries * 100, 3000); } } }); this.setupEventHandlers(); } private setupEventHandlers(): void { this.client.on('connect', () => { logger.info('Connected to Redis'); this.isConnected = true; }); this.client.on('ready', () => { logger.info('Redis client ready'); }); this.client.on('error', (error: Error) => { logger.error('Redis client error:', error); this.isConnected = false; }); this.client.on('end', () => { logger.info('Redis connection ended'); this.isConnected = false; }); this.client.on('reconnecting', () => { logger.info('Reconnecting to Redis...'); }); } /** * Connect to Redis */ async connect(): Promise { if (this.isConnected) { return; } try { // Check if client is already connecting or connected if (this.client.isOpen) { this.isConnected = true; return; } await this.client.connect(); this.isConnected = true; logger.info('Successfully connected to Redis'); } catch (error) { // If it's a "Socket already opened" error, mark as connected if (error instanceof Error && error.message.includes('Socket already opened')) { this.isConnected = true; logger.info('Redis connection already established'); return; } logger.error('Failed to connect to Redis:', error); throw error; } } /** * Disconnect from Redis */ async disconnect(): Promise { if (!this.isConnected) { return; } try { await this.client.quit(); logger.info('Disconnected from Redis'); } catch (error) { logger.error('Error disconnecting from Redis:', error); } } /** * Store user session */ async storeSession(userId: string, sessionData: Omit): Promise { try { await this.connect(); const session: SessionData = { ...sessionData, lastActivity: Date.now() }; const key = `session:${userId}`; const sessionTTL = parseInt(config.jwt.refreshExpiresIn.replace(/[^0-9]/g, '')) * (config.jwt.refreshExpiresIn.includes('h') ? 3600 : config.jwt.refreshExpiresIn.includes('d') ? 86400 : 60); await this.client.setEx(key, sessionTTL, JSON.stringify(session)); logger.info(`Stored session for user: ${userId}`); } catch (error) { logger.error('Error storing session:', error); throw new Error('Failed to store session'); } } /** * Get user session */ async getSession(userId: string): Promise { try { await this.connect(); const key = `session:${userId}`; const sessionData = await this.client.get(key); if (!sessionData) { return null; } const session: SessionData = JSON.parse(sessionData); // Update last activity session.lastActivity = Date.now(); await this.updateSessionActivity(userId, session.lastActivity); logger.info(`Retrieved session for user: ${userId}`); return session; } catch (error) { logger.error('Error getting session:', error); return null; } } /** * Update session activity timestamp */ async updateSessionActivity(userId: string, lastActivity: number): Promise { try { await this.connect(); const key = `session:${userId}`; const sessionData = await this.client.get(key); if (sessionData) { const session: SessionData = JSON.parse(sessionData); session.lastActivity = lastActivity; const sessionTTL = parseInt(config.jwt.refreshExpiresIn.replace(/[^0-9]/g, '')) * (config.jwt.refreshExpiresIn.includes('h') ? 3600 : config.jwt.refreshExpiresIn.includes('d') ? 86400 : 60); await this.client.setEx(key, sessionTTL, JSON.stringify(session)); } } catch (error) { logger.error('Error updating session activity:', error); } } /** * Remove user session */ async removeSession(userId: string): Promise { try { await this.connect(); const key = `session:${userId}`; await this.client.del(key); logger.info(`Removed session for user: ${userId}`); } catch (error) { logger.error('Error removing session:', error); throw new Error('Failed to remove session'); } } /** * Check if session exists */ async sessionExists(userId: string): Promise { try { await this.connect(); const key = `session:${userId}`; const exists = await this.client.exists(key); return exists === 1; } catch (error) { logger.error('Error checking session existence:', error); return false; } } /** * Store refresh token for blacklisting */ async blacklistToken(token: string, expiresIn: number): Promise { try { await this.connect(); const key = `blacklist:${token}`; await this.client.setEx(key, expiresIn, '1'); logger.info('Token blacklisted successfully'); } catch (error) { logger.error('Error blacklisting token:', error); throw new Error('Failed to blacklist token'); } } /** * Check if token is blacklisted */ async isTokenBlacklisted(token: string): Promise { try { await this.connect(); const key = `blacklist:${token}`; const exists = await this.client.exists(key); return exists === 1; } catch (error) { logger.error('Error checking token blacklist:', error); return false; } } /** * Get all active sessions (for admin) */ async getAllSessions(): Promise<{ userId: string; session: SessionData }[]> { try { await this.connect(); const keys = await this.client.keys('session:*'); const sessions: { userId: string; session: SessionData }[] = []; for (const key of keys) { const userId = key.replace('session:', ''); const sessionData = await this.client.get(key); if (sessionData) { sessions.push({ userId, session: JSON.parse(sessionData) }); } } return sessions; } catch (error) { logger.error('Error getting all sessions:', error); return []; } } /** * Clean up expired sessions */ async cleanupExpiredSessions(): Promise { try { await this.connect(); const keys = await this.client.keys('session:*'); let cleanedCount = 0; for (const key of keys) { const sessionData = await this.client.get(key); if (sessionData) { const session: SessionData = JSON.parse(sessionData); const now = Date.now(); const sessionTTL = parseInt(config.jwt.refreshExpiresIn.replace(/[^0-9]/g, '')) * (config.jwt.refreshExpiresIn.includes('h') ? 3600 : config.jwt.refreshExpiresIn.includes('d') ? 86400 : 60) * 1000; if (now - session.lastActivity > sessionTTL) { await this.client.del(key); cleanedCount++; } } } logger.info(`Cleaned up ${cleanedCount} expired sessions`); return cleanedCount; } catch (error) { logger.error('Error cleaning up expired sessions:', error); return 0; } } /** * Get Redis connection status */ getConnectionStatus(): boolean { return this.isConnected; } } // Export singleton instance export const sessionService = new SessionService();