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) => {