Files
cim_summary/backend/src/services/sessionService.ts
Jon 5a3c961bfc feat: Complete implementation of Tasks 1-5 - CIM Document Processor
Backend Infrastructure:
- Complete Express server setup with security middleware (helmet, CORS, rate limiting)
- Comprehensive error handling and logging with Winston
- Authentication system with JWT tokens and session management
- Database models and migrations for Users, Documents, Feedback, and Processing Jobs
- API routes structure for authentication and document management
- Integration tests for all server components (86 tests passing)

Frontend Infrastructure:
- React application with TypeScript and Vite
- Authentication UI with login form, protected routes, and logout functionality
- Authentication context with proper async state management
- Component tests with proper async handling (25 tests passing)
- Tailwind CSS styling and responsive design

Key Features:
- User registration, login, and authentication
- Protected routes with role-based access control
- Comprehensive error handling and user feedback
- Database schema with proper relationships
- Security middleware and validation
- Production-ready build configuration

Test Coverage: 111/111 tests passing
Tasks Completed: 1-5 (Project setup, Database, Auth system, Frontend UI, Backend infrastructure)

Ready for Task 6: File upload backend infrastructure
2025-07-27 13:29:26 -04:00

313 lines
7.8 KiB
TypeScript

import Redis 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: Redis.RedisClientType;
private isConnected: boolean = false;
constructor() {
this.client = Redis.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) => {
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 {
await this.client.connect();
logger.info('Successfully connected to Redis');
} catch (error) {
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();