- 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
327 lines
8.3 KiB
TypeScript
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();
|