436 lines
10 KiB
JavaScript
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();
|