Files
Hotel-Booking/server/src/services/authService.js
Iliyan Angelov 824eec6190 Hotel Booking
2025-11-16 14:19:13 +02:00

436 lines
10 KiB
JavaScript

const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const authRepository = require('../repositories/authRepository');
const { sendEmail } = require('../utils/mailer');
/**
* Auth Service - Business logic layer
* Handles authentication business logic
*/
class AuthService {
/**
* Generate JWT tokens
*/
generateTokens(userId) {
const accessToken = jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '1h' }
);
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
);
return { accessToken, refreshToken };
}
/**
* Verify JWT token
*/
verifyAccessToken(token) {
return jwt.verify(token, process.env.JWT_SECRET);
}
/**
* Verify refresh token
*/
verifyRefreshToken(token) {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
}
/**
* Hash password
*/
async hashPassword(password) {
return await bcrypt.hash(password, 10);
}
/**
* Compare password
*/
async comparePassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
/**
* Format user response
*/
formatUserResponse(user) {
return {
id: user.id,
name: user.full_name,
email: user.email,
phone: user.phone,
role: user.role ? user.role.name : 'customer',
createdAt: user.created_at,
updatedAt: user.updated_at
};
}
/**
* Register new user
*/
async register(data) {
const { name, email, password, phone } = data;
// Check if email exists
const emailExists = await authRepository.isEmailExists(email);
if (emailExists) {
throw new Error('Email already registered');
}
// Hash password
const hashedPassword = await this.hashPassword(password);
// Create user (default role_id = 3 for customer)
const user = await authRepository.createUser({
full_name: name,
email,
password: hashedPassword,
phone,
role_id: 3 // Customer role
});
// Generate tokens
const { accessToken, refreshToken } =
this.generateTokens(user.id);
// Save refresh token (expires in 7 days)
const expiresAt = new Date(
Date.now() + 7 * 24 * 60 * 60 * 1000
);
await authRepository.saveRefreshToken(
user.id,
refreshToken,
expiresAt
);
// Remove password from response
const userResponse = user.toJSON();
delete userResponse.password;
// Send welcome email (non-blocking)
try {
await sendEmail({
to: user.email,
subject: 'Welcome to Hotel Booking',
html: `
<div style="font-family: Arial, sans-serif;
max-width: 600px; margin: 0 auto;">
<h2 style="color: #4F46E5;">
Welcome ${user.full_name}!
</h2>
<p>Thank you for registering an account at
<strong>Hotel Booking</strong>.</p>
<p>Your account has been successfully created with
email: <strong>${user.email}</strong></p>
<div style="background-color: #F3F4F6;
padding: 20px; border-radius: 8px;
margin: 20px 0;">
<p style="margin: 0;">
<strong>You can:</strong>
</p>
<ul style="margin-top: 10px;">
<li>Search and book hotel rooms</li>
<li>Manage your bookings</li>
<li>Update your personal information</li>
</ul>
</div>
<p>
<a href="${process.env.CLIENT_URL}/login"
style="background-color: #4F46E5;
color: white; padding: 12px 24px;
text-decoration: none; border-radius: 6px;
display: inline-block;">
Login Now
</a>
</p>
<p style="color: #6B7280; font-size: 14px;
margin-top: 30px;">
If you have any questions, please
contact us.
</p>
</div>
`
});
} catch (err) {
console.error('Failed to send welcome email:', err);
// Don't fail registration if email fails
}
return {
user: userResponse,
token: accessToken,
refreshToken
};
}
/**
* Login user
*/
async login(data) {
const { email, password, rememberMe } = data;
// Find user with role and password (needed for authentication)
const user = await authRepository.findUserByEmail(
email,
true, // includeRole
true // includePassword - needed to verify password
);
if (!user) {
throw new Error('Invalid email or password');
}
// Check password
const isPasswordValid = await this.comparePassword(
password,
user.password
);
if (!isPasswordValid) {
throw new Error('Invalid email or password');
}
// Generate tokens
const { accessToken, refreshToken } =
this.generateTokens(user.id);
// Calculate expiry based on rememberMe
const expiryDays = rememberMe ? 7 : 1;
const expiresAt = new Date(
Date.now() + expiryDays * 24 * 60 * 60 * 1000
);
// Save refresh token
await authRepository.saveRefreshToken(
user.id,
refreshToken,
expiresAt
);
// Format user response
const userResponse = this.formatUserResponse(user);
return {
user: userResponse,
token: accessToken,
refreshToken
};
}
/**
* Refresh access token
*/
async refreshAccessToken(refreshToken) {
if (!refreshToken) {
throw new Error('Refresh token is required');
}
// Verify refresh token
const decoded = this.verifyRefreshToken(refreshToken);
// Check if refresh token exists in database
const storedToken = await authRepository.findRefreshToken(
refreshToken,
decoded.userId
);
if (!storedToken) {
throw new Error('Invalid refresh token');
}
// Check if token is expired
if (new Date() > storedToken.expires_at) {
await authRepository.deleteRefreshToken(refreshToken);
throw new Error('Refresh token expired');
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: decoded.userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '1h' }
);
return { token: accessToken };
}
/**
* Logout user
*/
async logout(refreshToken) {
if (refreshToken) {
await authRepository.deleteRefreshToken(refreshToken);
}
return true;
}
/**
* Get user profile
*/
async getProfile(userId) {
const user = await authRepository.findUserById(userId, true);
if (!user) {
throw new Error('User not found');
}
return this.formatUserResponse(user);
}
/**
* Generate reset token
*/
generateResetToken() {
const resetToken = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
return { resetToken, hashedToken };
}
/**
* Forgot Password - Send reset link
*/
async forgotPassword(email) {
// Find user by email
const user = await authRepository.findUserByEmail(email);
// Always return success to prevent email enumeration
if (!user) {
return {
success: true,
message: 'If email exists, reset link has been sent'
};
}
// Generate reset token
const { resetToken, hashedToken } =
this.generateResetToken();
// Save token (expires in 1 hour)
const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
await authRepository.savePasswordResetToken(
user.id,
hashedToken,
expiresAt
);
// Build reset URL
const resetUrl =
`${process.env.CLIENT_URL}/reset-password/${resetToken}`;
// Try to send email
try {
await sendEmail({
to: user.email,
subject: 'Reset password - Hotel Booking',
html: `
<p>You (or someone) has requested to reset your password.</p>
<p>Click the link below to reset your password
(expires in 1 hour):</p>
<p><a href="${resetUrl}">${resetUrl}</a></p>
`
});
} catch (err) {
console.error('Failed to send reset email:', err);
// Do NOT log the raw reset token or URL in production.
// Errors are logged above; token must remain secret.
}
return {
success: true,
message: 'Password reset link has been sent to your email'
};
}
/**
* Reset Password - Update password with token
*/
async resetPassword(data) {
const { token, password } = data;
if (!token || !password) {
throw new Error('Token and password are required');
}
// Hash the token to compare
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
// Find valid token
const resetToken =
await authRepository.findValidPasswordResetToken(
hashedToken
);
if (!resetToken) {
throw new Error('Invalid or expired reset token');
}
// Find user (include password so we can compare)
const user = await authRepository.findUserById(
resetToken.user_id,
false, // includeRole
true // includePassword
);
if (!user) {
throw new Error('User not found');
}
// Check if new password matches old password
const isSamePassword = await this.comparePassword(
password,
user.password
);
if (isSamePassword) {
// Return error message to the client
throw new Error('New password must be different from the old password');
}
// Hash new password
const hashedPassword = await this.hashPassword(password);
// Update password
await authRepository.updateUserPassword(
user.id,
hashedPassword
);
// Delete used token
await authRepository.deletePasswordResetToken(resetToken.id);
// Send confirmation email (non-blocking)
try {
await sendEmail({
to: user.email,
subject: 'Password Changed',
html: `
<p>The password for account ${user.email} has been changed
successfully.</p>
`
});
} catch (err) {
console.error('Failed to send confirmation email:', err);
}
return {
success: true,
message: 'Password has been reset successfully'
};
}
}
module.exports = new AuthService();