const UserRepository = require('../repositories/UserRepository'); const LoginRepository = require('../repositories/LoginRepository'); const UnitOfWork = require('../repositories/UnitOfWork'); const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const { logger } = require('../middleware/logger'); class LoginService { static async login(email, password) { logger.info('LoginService.login:start', { email }); const unitOfWork = new UnitOfWork(); await unitOfWork.start(); unitOfWork.registerRepository('user', new UserRepository(unitOfWork)); unitOfWork.registerRepository('login', new LoginRepository(unitOfWork)); try { // Find user by email const userRepo = unitOfWork.getRepository('user'); const loginRepo = unitOfWork.getRepository('login'); const user = await userRepo.findUserByEmail(email); if (!user) { logger.warn('LoginService.login:user_not_found', { email }); await unitOfWork.rollback(); const error = new Error('Invalid credentials'); error.status = 401; throw error; } // Validate password const valid = await user.comparePassword(password); if (!valid) { logger.warn('LoginService.login:invalid_password', { email }); await unitOfWork.rollback(); const error = new Error('Invalid credentials'); error.status = 401; throw error; } // Generate access token (JWT) const accessToken = jwt.sign( { userId: user.id, email: user.email, userType: user.userType, role: user.role }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '15m' } ); logger.info('LoginService.login:access_token_issued', { userId: user.id, email: user.email }); // Generate refresh token (random string) const refreshToken = crypto.randomBytes(64).toString('hex'); // Use REFRESH_TOKEN_EXPIRES_IN from env, fallback to 7d const refreshExpiresMs = (() => { const envVal = process.env.REFRESH_TOKEN_EXPIRES_IN || '7d'; if (envVal.endsWith('d')) return parseInt(envVal) * 24 * 60 * 60 * 1000; if (envVal.endsWith('h')) return parseInt(envVal) * 60 * 60 * 1000; if (envVal.endsWith('m')) return parseInt(envVal) * 60 * 1000; return 7 * 24 * 60 * 60 * 1000; })(); const expiresAt = new Date(Date.now() + refreshExpiresMs); // Store refresh token in DB (atomic) via LoginRepository await loginRepo.insertRefreshToken(user.id, refreshToken, expiresAt); logger.info('LoginService.login:refresh_token_issued', { userId: user.id, email: user.email }); // Update last_login_at in users table via LoginRepository await loginRepo.updateLastLogin(user.id); logger.info('LoginService.login:last_login_updated', { userId: user.id }); // Fetch user permissions via LoginRepository const permissions = await loginRepo.getUserPermissions(user.id); logger.info('LoginService.login:permissions_fetched', { userId: user.id, count: permissions.length }); await unitOfWork.commit(); logger.info('LoginService.login:success', { userId: user.id }); // Send access token in response return { accessToken, refreshToken, refreshTokenExpires: expiresAt, user: { ...user.getPublicData(), role: user.role, permissions // Add permissions array to response } }; } catch (error) { logger.error('LoginService.login:error', { email, error: error.message }); await unitOfWork.rollback(error); throw error; } } static async refresh(refreshToken) { logger.info('LoginService.refresh:start'); const unitOfWork = new UnitOfWork(); await unitOfWork.start(); unitOfWork.registerRepository('login', new LoginRepository(unitOfWork)); unitOfWork.registerRepository('user', new UserRepository(unitOfWork)); try { const loginRepo = unitOfWork.getRepository('login'); // Find refresh token in DB via LoginRepository const tokenRecord = await loginRepo.findRefreshToken(refreshToken); if (!tokenRecord) { logger.warn('LoginService.refresh:token_not_found'); await unitOfWork.rollback(); const error = new Error('Invalid or expired refresh token'); error.status = 401; throw error; } const { user_id, expires_at } = tokenRecord; if (new Date(expires_at) < new Date()) { logger.warn('LoginService.refresh:token_expired'); await unitOfWork.rollback(); const error = new Error('Refresh token expired'); error.status = 401; throw error; } // Rotate refresh token: revoke old, issue new via LoginRepository await loginRepo.revokeRefreshToken(refreshToken); const newRefreshToken = crypto.randomBytes(64).toString('hex'); const refreshExpiresMs = (() => { const envVal = process.env.REFRESH_TOKEN_EXPIRES_IN || '7d'; if (envVal.endsWith('d')) return parseInt(envVal) * 24 * 60 * 60 * 1000; if (envVal.endsWith('h')) return parseInt(envVal) * 60 * 60 * 1000; if (envVal.endsWith('m')) return parseInt(envVal) * 60 * 1000; return 7 * 24 * 60 * 60 * 1000; })(); const newExpiresAt = new Date(Date.now() + refreshExpiresMs); await loginRepo.insertRefreshToken(user_id, newRefreshToken, newExpiresAt); logger.info('LoginService.refresh:refresh_token_rotated', { userId: user_id }); // Get user via UserRepository const userRepo = unitOfWork.getRepository('user'); const user = await userRepo.findUserByEmailOrId(user_id); // Fetch user role directly from DB via LoginRepository const role = await loginRepo.getUserRole(user.id); // Fetch user permissions via LoginRepository const permissions = await loginRepo.getUserPermissions(user.id); logger.info('LoginService.refresh:user_fetched', { userId: user.id }); logger.info('LoginService.refresh:role_fetched', { userId: user.id, role }); logger.info('LoginService.refresh:permissions_fetched', { userId: user.id, count: permissions.length }); // Generate new access token const accessToken = jwt.sign( { userId: user.id, email: user.email, userType: user.userType, role }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '15m' } ); logger.info('LoginService.refresh:access_token_issued', { userId: user.id, email: user.email }); await unitOfWork.commit(); logger.info('LoginService.refresh:success', { userId: user.id }); return { accessToken, refreshToken: newRefreshToken, refreshTokenExpires: newExpiresAt, user: { ...user.getPublicData(), role, permissions } }; } catch (error) { logger.error('LoginService.refresh:error', { error: error.message }); // Improved error logging for refresh token rotation failures console.error('💥 Error during refresh token rotation:', error); if (error && error.stack) { console.error('💥 Error stack:', error.stack); } await unitOfWork.rollback(error); throw error; } } static async logout(refreshToken) { logger.info('LoginService.logout:start'); const unitOfWork = new UnitOfWork(); await unitOfWork.start(); unitOfWork.registerRepository('login', new LoginRepository(unitOfWork)); try { const loginRepo = unitOfWork.getRepository('login'); // Mark refresh token as revoked via LoginRepository await loginRepo.revokeRefreshToken(refreshToken); logger.info('LoginService.logout:refresh_token_revoked'); await unitOfWork.commit(); logger.info('LoginService.logout:success'); return true; } catch (error) { logger.error('LoginService.logout:error', { error: error.message }); await unitOfWork.rollback(error); throw error; } } } // Helper for finding user by id or email LoginService.findUserByEmailOrId = async function(userIdOrEmail, unitOfWork) { logger.info('LoginService.findUserByEmailOrId:start', { userIdOrEmail }); const userRepo = new UserRepository(unitOfWork); try { if (typeof userIdOrEmail === 'number') { // Find by id const query = `SELECT * FROM users WHERE id = ?`; const [rows] = await unitOfWork.connection.query(query, [userIdOrEmail]); if (!rows.length) { logger.warn('LoginService.findUserByEmailOrId:not_found', { userIdOrEmail }); return null; } const row = rows[0]; // You may want to instantiate the correct user type here logger.info('LoginService.findUserByEmailOrId:found', { userIdOrEmail }); return userRepo.findUserByEmail(row.email); } else { // Find by email const user = await userRepo.findUserByEmail(userIdOrEmail); logger.info('LoginService.findUserByEmailOrId:found', { userIdOrEmail, found: !!user }); return user; } } catch (error) { logger.error('LoginService.findUserByEmailOrId:error', { userIdOrEmail, error: error.message }); throw error; } }; module.exports = LoginService;