230 lines
9.1 KiB
JavaScript
230 lines
9.1 KiB
JavaScript
const UserRepository = require('../../repositories/user/UserRepository');
|
|
const LoginRepository = require('../../repositories/login/LoginRepository');
|
|
const UnitOfWork = require('../../database/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; |