From 74c375b50fd7c984218ef0751d871390a2fece55 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:11:28 +0000 Subject: [PATCH] refactor: implement PrismaClient singleton and Socket.IO authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements critical production improvements identified through autonomous code review: ## Changes Made ### 1. PrismaClient Singleton Pattern (High Priority) - Created `backend/src/config/prisma.ts` with singleton implementation - Prevents connection pool exhaustion from multiple PrismaClient instances - Added proper logging integration with Prisma query/error/warn events - Implemented graceful shutdown handlers for clean disconnects - Updated 7 files to use the singleton: auth.ts, taskController.ts, projectController.ts, authController.ts, userController.ts, notificationController.ts, and server.ts **Impact**: This fixes a critical anti-pattern that could cause database connection issues under load. Previously, each controller created its own PrismaClient instance, leading to connection pool exhaustion. ### 2. Socket.IO Authentication (Security Fix) - Implemented JWT validation in Socket.IO middleware (server.ts line 105) - Resolved TODO comment at line 110 (token validation) - Added proper user verification with database lookup - Validates user exists and is active before allowing connection - Attaches user data to socket for authorized connections - Comprehensive error handling with detailed error messages **Impact**: This closes a security gap where WebSocket connections were not properly authenticated, potentially allowing unauthorized real-time access to project updates. ## Technical Details ### PrismaClient Singleton Benefits - Single connection pool shared across application - Development hot-reload resilience (cached on global object) - Automatic cleanup on process termination - Centralized query logging and error handling - Follows Prisma best practices for production deployments ### Socket.IO Auth Flow 1. Extract JWT token from socket handshake 2. Verify token signature and claims (issuer, audience) 3. Query database to ensure user exists and is active 4. Attach user object to socket.data for downstream use 5. Proper error handling with descriptive messages ## Testing Recommendations - Verify Socket.IO connections require valid JWT tokens - Test database connection pool doesn't exhaust under load - Confirm all existing functionality continues to work - Check WebSocket real-time updates still function correctly ## Related Issues - Addresses connection pool management (production readiness) - Fixes Socket.IO authentication gap (security) - Improves code maintainability and follows best practices --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/src/config/prisma.ts | 82 +++++++++++++++++++ backend/src/controllers/authController.ts | 4 +- .../src/controllers/notificationController.ts | 4 +- backend/src/controllers/projectController.ts | 4 +- backend/src/controllers/taskController.ts | 4 +- backend/src/controllers/userController.ts | 4 +- backend/src/middleware/auth.ts | 4 +- backend/src/server.ts | 41 ++++++++-- 8 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 backend/src/config/prisma.ts diff --git a/backend/src/config/prisma.ts b/backend/src/config/prisma.ts new file mode 100644 index 0000000..d8da635 --- /dev/null +++ b/backend/src/config/prisma.ts @@ -0,0 +1,82 @@ +/** + * Prisma Client Singleton + * + * This module ensures only one instance of PrismaClient is created + * and reused across the application, preventing connection pool exhaustion. + * + * @see https://www.prisma.io/docs/guides/performance-and-optimization/connection-management + */ + +import { PrismaClient } from '@prisma/client'; +import { logger } from './logger'; + +// PrismaClient is attached to the global object in development +// to prevent hot-reloading from creating new instances +const globalForPrisma = global as unknown as { prisma: PrismaClient }; + +/** + * Create PrismaClient with logging configuration + */ +const createPrismaClient = (): PrismaClient => { + const client = new PrismaClient({ + log: [ + { + emit: 'event', + level: 'query', + }, + { + emit: 'event', + level: 'error', + }, + { + emit: 'event', + level: 'warn', + }, + ], + }); + + // Hook into Prisma query logging + client.$on('query' as never, ((event: any) => { + logger.debug('Prisma Query:', { + query: event.query, + params: event.params, + duration: `${event.duration}ms`, + }); + }) as never); + + client.$on('error' as never, ((event: any) => { + logger.error('Prisma Error:', event); + }) as never); + + client.$on('warn' as never, ((event: any) => { + logger.warn('Prisma Warning:', event); + }) as never); + + return client; +}; + +/** + * Singleton PrismaClient instance + * In development, the instance is cached on the global object + * to survive hot-reloads + */ +export const prisma = globalForPrisma.prisma || createPrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma; +} + +/** + * Graceful disconnect on process termination + */ +const gracefulShutdown = async () => { + logger.info('Disconnecting Prisma Client...'); + await prisma.$disconnect(); + logger.info('Prisma Client disconnected'); +}; + +process.on('beforeExit', gracefulShutdown); +process.on('SIGINT', gracefulShutdown); +process.on('SIGTERM', gracefulShutdown); + +export default prisma; diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 1c49445..79d45a4 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,6 +1,5 @@ import { Request, Response } from 'express'; import bcrypt from 'bcryptjs'; -import { PrismaClient } from '@prisma/client'; import { generateAccessToken, generateRefreshToken, @@ -10,8 +9,7 @@ import { AppError, asyncHandler } from '../middleware/errorHandler'; import { logger } from '../config/logger'; import { config } from '../config/environment'; import { AuthenticatedRequest } from '../types'; - -const prisma = new PrismaClient(); +import { prisma } from '../config/prisma'; /** * @desc Register a new user diff --git a/backend/src/controllers/notificationController.ts b/backend/src/controllers/notificationController.ts index 64237a2..4db81ae 100644 --- a/backend/src/controllers/notificationController.ts +++ b/backend/src/controllers/notificationController.ts @@ -1,10 +1,8 @@ import { Request, Response } from 'express'; -import { PrismaClient } from '@prisma/client'; import { AppError, asyncHandler } from '../middleware/errorHandler'; import { logger } from '../config/logger'; import { AuthenticatedRequest } from '../types'; - -const prisma = new PrismaClient(); +import { prisma } from '../config/prisma'; /** * @desc Get user notifications diff --git a/backend/src/controllers/projectController.ts b/backend/src/controllers/projectController.ts index 5955225..c3e882f 100644 --- a/backend/src/controllers/projectController.ts +++ b/backend/src/controllers/projectController.ts @@ -1,10 +1,8 @@ import { Request, Response } from 'express'; -import { PrismaClient } from '@prisma/client'; import { AppError, asyncHandler } from '../middleware/errorHandler'; import { logger } from '../config/logger'; import { AuthenticatedRequest } from '../types'; - -const prisma = new PrismaClient(); +import { prisma } from '../config/prisma'; /** * Check if user has access to project diff --git a/backend/src/controllers/taskController.ts b/backend/src/controllers/taskController.ts index 0e3b9e6..fbba7c2 100644 --- a/backend/src/controllers/taskController.ts +++ b/backend/src/controllers/taskController.ts @@ -1,10 +1,8 @@ import { Request, Response } from 'express'; -import { PrismaClient } from '@prisma/client'; import { AppError, asyncHandler } from '../middleware/errorHandler'; import { logger } from '../config/logger'; import { AuthenticatedRequest } from '../types'; - -const prisma = new PrismaClient(); +import { prisma } from '../config/prisma'; /** * Check if user has access to task diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/userController.ts index 2b476f1..75e1213 100644 --- a/backend/src/controllers/userController.ts +++ b/backend/src/controllers/userController.ts @@ -1,12 +1,10 @@ import { Request, Response } from 'express'; import bcrypt from 'bcryptjs'; -import { PrismaClient } from '@prisma/client'; import { AppError, asyncHandler } from '../middleware/errorHandler'; import { logger } from '../config/logger'; import { config } from '../config/environment'; import { AuthenticatedRequest } from '../types'; - -const prisma = new PrismaClient(); +import { prisma } from '../config/prisma'; /** * @desc Get current user profile diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 2e39684..d62d7e3 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -1,11 +1,9 @@ import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; -import { PrismaClient } from '@prisma/client'; import { config, jwtConfig } from '../config/environment'; import { logger } from '../config/logger'; import { AuthenticatedRequest, JWTPayload } from '../types'; - -const prisma = new PrismaClient(); +import { prisma } from '../config/prisma'; /** * JWT Authentication Middleware diff --git a/backend/src/server.ts b/backend/src/server.ts index 447eea2..a5c9139 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -102,13 +102,44 @@ app.use('/api/tasks', authMiddleware, taskRoutes); app.use('/api/notifications', authMiddleware, notificationRoutes); // Socket.IO for real-time features -io.use((socket, next) => { - const token = socket.handshake.auth.token; - if (!token) { +io.use(async (socket, next) => { + try { + const token = socket.handshake.auth.token; + if (!token) { + return next(new Error('Authentication error: Token required')); + } + + // Validate JWT token + const jwt = await import('jsonwebtoken'); + const { jwtConfig } = await import('./config/environment'); + const { prisma } = await import('./config/prisma'); + + try { + const decoded = jwt.verify(token, jwtConfig.secret, { + issuer: jwtConfig.issuer, + audience: jwtConfig.audience, + }) as any; + + // Verify user exists and is active + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { id: true, email: true, isActive: true }, + }); + + if (!user || !user.isActive) { + return next(new Error('Authentication error: Invalid user')); + } + + // Attach user to socket + socket.data.user = user; + next(); + } catch (error) { + return next(new Error('Authentication error: Invalid token')); + } + } catch (error) { + logger.error('Socket.IO auth error:', error); return next(new Error('Authentication error')); } - // Add token validation here - next(); }); io.on('connection', (socket) => {