Files
cim_summary/backend/src/services/sessionService.ts
Jon c67dab22b4 Add comprehensive CIM processing features and UI improvements
- Add new database migrations for analysis data and job tracking
- Implement enhanced document processing service with LLM integration
- Add processing progress and queue status components
- Create testing guides and utility scripts for CIM processing
- Update frontend components for better user experience
- Add environment configuration and backup files
- Implement job queue service and upload progress tracking
2025-07-27 20:25:46 -04:00

327 lines
8.3 KiB
TypeScript

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<void> {
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<void> {
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<SessionData, 'lastActivity'>): Promise<void> {
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<SessionData | null> {
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<void> {
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<void> {
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<boolean> {
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<void> {
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<boolean> {
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<number> {
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();