All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 4m24s
221 lines
7.0 KiB
TypeScript
221 lines
7.0 KiB
TypeScript
// src/services/authService.ts
|
|
import * as bcrypt from 'bcrypt';
|
|
import jwt from 'jsonwebtoken';
|
|
import crypto from 'crypto';
|
|
import { userRepo, adminRepo } from './db/index.db';
|
|
import { UniqueConstraintError } from './db/errors.db';
|
|
import { getPool } from './db/connection.db';
|
|
import { logger } from './logger.server';
|
|
import { sendPasswordResetEmail } from './emailService.server';
|
|
import type { UserProfile } from '../types';
|
|
import { validatePasswordStrength } from '../utils/authUtils';
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET!;
|
|
|
|
class AuthService {
|
|
async registerUser(
|
|
email: string,
|
|
password: string,
|
|
fullName: string | undefined,
|
|
avatarUrl: string | undefined,
|
|
reqLog: any,
|
|
) {
|
|
try {
|
|
const saltRounds = 10;
|
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
|
logger.info(`Hashing password for new user: ${email}`);
|
|
|
|
// The createUser method in UserRepository now handles its own transaction.
|
|
const newUser = await userRepo.createUser(
|
|
email,
|
|
hashedPassword,
|
|
{ full_name: fullName, avatar_url: avatarUrl },
|
|
reqLog,
|
|
);
|
|
|
|
const userEmail = newUser.user.email;
|
|
const userId = newUser.user.user_id;
|
|
logger.info(`Successfully created new user in DB: ${userEmail} (ID: ${userId})`);
|
|
|
|
// Use the new standardized logging function
|
|
await adminRepo.logActivity(
|
|
{
|
|
userId: newUser.user.user_id,
|
|
action: 'user_registered',
|
|
displayText: `${userEmail} has registered.`,
|
|
icon: 'user-plus',
|
|
},
|
|
reqLog,
|
|
);
|
|
|
|
return newUser;
|
|
} catch (error: unknown) {
|
|
if (error instanceof UniqueConstraintError) {
|
|
// If the email is a duplicate, return a 409 Conflict status.
|
|
throw error;
|
|
}
|
|
logger.error({ error }, `User registration route failed for email: ${email}.`);
|
|
// Pass the error to the centralized handler
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async registerAndLoginUser(
|
|
email: string,
|
|
password: string,
|
|
fullName: string | undefined,
|
|
avatarUrl: string | undefined,
|
|
reqLog: any,
|
|
): Promise<{ newUserProfile: UserProfile; accessToken: string; refreshToken: string }> {
|
|
const newUserProfile = await this.registerUser(
|
|
email,
|
|
password,
|
|
fullName,
|
|
avatarUrl,
|
|
reqLog,
|
|
);
|
|
const { accessToken, refreshToken } = await this.handleSuccessfulLogin(newUserProfile, reqLog);
|
|
return { newUserProfile, accessToken, refreshToken };
|
|
}
|
|
|
|
generateAuthTokens(userProfile: UserProfile) {
|
|
const payload = {
|
|
user_id: userProfile.user.user_id,
|
|
email: userProfile.user.email,
|
|
role: userProfile.role,
|
|
};
|
|
const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
|
|
const refreshToken = crypto.randomBytes(64).toString('hex');
|
|
|
|
return { accessToken, refreshToken };
|
|
}
|
|
|
|
async saveRefreshToken(userId: string, refreshToken: string, reqLog: any) {
|
|
try {
|
|
await userRepo.saveRefreshToken(userId, refreshToken, reqLog);
|
|
} catch (tokenErr) {
|
|
logger.error(
|
|
{ error: tokenErr },
|
|
`Failed to save refresh token during login for user: ${userId}`,
|
|
);
|
|
throw tokenErr;
|
|
}
|
|
}
|
|
|
|
async handleSuccessfulLogin(userProfile: UserProfile, reqLog: any) {
|
|
const { accessToken, refreshToken } = this.generateAuthTokens(userProfile);
|
|
await this.saveRefreshToken(userProfile.user.user_id, refreshToken, reqLog);
|
|
return { accessToken, refreshToken };
|
|
}
|
|
|
|
async resetPassword(email: string, reqLog: any) {
|
|
try {
|
|
logger.debug(`[API /forgot-password] Received request for email: ${email}`);
|
|
const user = await userRepo.findUserByEmail(email, reqLog);
|
|
let token: string | undefined;
|
|
logger.debug(
|
|
{ user: user ? { user_id: user.user_id, email: user.email } : 'NOT FOUND' },
|
|
`[API /forgot-password] Database search result for ${email}:`,
|
|
);
|
|
|
|
if (user) {
|
|
token = crypto.randomBytes(32).toString('hex');
|
|
const saltRounds = 10;
|
|
const tokenHash = await bcrypt.hash(token, saltRounds);
|
|
const expiresAt = new Date(Date.now() + 3600000); // 1 hour
|
|
|
|
await userRepo.createPasswordResetToken(user.user_id, tokenHash, expiresAt, reqLog);
|
|
|
|
const resetLink = `${process.env.FRONTEND_URL}/reset-password/${token}`;
|
|
|
|
try {
|
|
await sendPasswordResetEmail(email, resetLink, reqLog);
|
|
} catch (emailError) {
|
|
logger.error({ emailError }, `Email send failure during password reset for user`);
|
|
}
|
|
} else {
|
|
logger.warn(`Password reset requested for non-existent email: ${email}`);
|
|
}
|
|
|
|
return token;
|
|
} catch (error) {
|
|
logger.error({ error }, `An error occurred during /forgot-password for email: ${email}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updatePassword(token: string, newPassword: string, reqLog: any) {
|
|
try {
|
|
const validTokens = await userRepo.getValidResetTokens(reqLog);
|
|
let tokenRecord;
|
|
for (const record of validTokens) {
|
|
const isMatch = await bcrypt.compare(token, record.token_hash);
|
|
if (isMatch) {
|
|
tokenRecord = record;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!tokenRecord) {
|
|
return null;
|
|
}
|
|
|
|
const saltRounds = 10;
|
|
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
|
|
|
await userRepo.updateUserPassword(tokenRecord.user_id, hashedPassword, reqLog);
|
|
await userRepo.deleteResetToken(tokenRecord.token_hash, reqLog);
|
|
|
|
// Log this security event after a successful password reset.
|
|
await adminRepo.logActivity(
|
|
{
|
|
userId: tokenRecord.user_id,
|
|
action: 'password_reset',
|
|
displayText: `User ID ${tokenRecord.user_id} has reset their password.`,
|
|
icon: 'key',
|
|
details: { source_ip: null },
|
|
},
|
|
reqLog,
|
|
);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error({ error }, `An error occurred during password reset.`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getUserByRefreshToken(refreshToken: string, reqLog: any) {
|
|
try {
|
|
const basicUser = await userRepo.findUserByRefreshToken(refreshToken, reqLog);
|
|
if (!basicUser) {
|
|
return null;
|
|
}
|
|
const userProfile = await userRepo.findUserProfileById(basicUser.user_id, reqLog);
|
|
return userProfile;
|
|
} catch (error) {
|
|
logger.error({ error }, 'An error occurred during /refresh-token.');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async logout(refreshToken: string, reqLog: any) {
|
|
try {
|
|
await userRepo.deleteRefreshToken(refreshToken, reqLog);
|
|
} catch (err: any) {
|
|
logger.error({ error: err }, 'Failed to delete refresh token from DB during logout.');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async refreshAccessToken(refreshToken: string, reqLog: any): Promise<{ accessToken: string } | null> {
|
|
const user = await this.getUserByRefreshToken(refreshToken, reqLog);
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
const { accessToken } = this.generateAuthTokens(user);
|
|
return { accessToken };
|
|
}
|
|
}
|
|
|
|
export const authService = new AuthService(); |