CentralBackend/services/LoginService.js
2025-09-07 12:44:01 +02:00

230 lines
9.1 KiB
JavaScript

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;