import { CacheService } from "@utils/cache.utils.js";
import { JwtUtils } from "@utils/jwt.utils.js";
import { PasswordUtils } from "@utils/password.utils.js";
import { logger } from "../config/logger.js";
import { User } from "../models/User.model.js";
/**
* Service encapsulating authentication, session, and profile workflows.
* @category Services
*/
export class AuthService {
/**
* Authenticate a user and generate tokens.
* @param {ILoginDto} credentials Login credentials payload.
* @returns {Promise<TokenResponse>} Access/refresh token pair.
*/
async login(credentials) {
try {
const { email, password } = credentials;
// Find user by email
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
throw new Error("Invalid credentials");
}
// Verify password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
throw new Error("Invalid credentials");
}
// Generate tokens
const tokens = JwtUtils.generateTokens(user);
logger.info({ tokens }, "Tokens generated");
return {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
};
}
catch (error) {
logger.error({
error,
message: "Login failed",
});
throw error;
}
}
/**
* Change a user's password after validating the current one.
* @param {string} userId Target user identifier.
* @param {ChangePasswordRequest} passwordData Contains current and new passwords.
* @returns {Promise<void>} Resolves when the password is updated.
*/
async changePassword(userId, passwordData) {
try {
const { currentPassword, newPassword } = passwordData;
// Find user
const user = await User.findById(userId);
if (!user) {
throw new Error("User not found");
}
// Verify current password
const isCurrentPasswordValid = await user.comparePassword(currentPassword);
if (!isCurrentPasswordValid) {
throw new Error("Current password is incorrect");
}
// Hash new password
const newPasswordHash = await PasswordUtils.hashPassword(newPassword);
// Update password
user.passwordHash = newPasswordHash;
await user.save();
logger.info(`Password changed for user ${userId}`);
}
catch (error) {
logger.error({
error,
message: "Password change failed",
});
throw error;
}
}
/**
* Retrieve a sanitized user profile.
* @param {string} userId Target user identifier.
* @returns {Promise<ICurrentUserResponse>} User response DTO.
*/
async getUser(userId) {
try {
const user = await User.findById(userId).select("-passwordHash");
if (!user) {
throw new Error("User not found");
}
return CacheService.toUserResponse(user);
}
catch (error) {
logger.error({
error,
message: "Get user failed",
});
throw error;
}
}
/**
* Replace a user's avatar with a new image.
* @param {string} userId Target user identifier.
* @param {string} avatar Base64 encoded avatar payload.
* @returns {Promise<string>} URL of the uploaded avatar.
*/
async changeAvatar(userId, avatar) {
try {
// Find user
const user = await User.findById(userId);
if (!user) {
throw new Error("User not found");
}
// Upload avatar to GCS
const { gcsService } = await import("./gcs.service.js");
const avatarUrl = await gcsService.uploadAvatar(userId, avatar);
// Delete old avatar if exists
if (user.avatar) {
await gcsService.deleteAvatar(user.avatar);
}
// Update user avatar
user.avatar = avatarUrl;
const updatedUser = await user.save();
await CacheService.updateUser(CacheService.toUserResponse(updatedUser));
logger.info({ userId, avatarUrl }, "Avatar changed successfully");
return avatarUrl;
}
catch (error) {
logger.error({
error,
userId,
message: "Avatar change failed",
});
throw error;
}
}
/**
* Exchange a refresh token for a new access token.
* @param {string} refreshToken Refresh token string.
* @returns {Promise<IRefreshTokenResponseDto>} Response containing a new access token.
*/
async refreshToken(refreshToken) {
try {
// Verify refresh token
const decoded = JwtUtils.verifyRefreshToken(refreshToken);
if (typeof decoded === "string") {
throw new Error("Invalid refresh token");
}
// Find user
const user = await User.findById(decoded.userId);
if (!user) {
throw new Error("User not found");
}
const accessToken = JwtUtils.generateAccessToken(user);
return { accessToken };
}
catch (error) {
logger.error({
error,
message: "Token refresh failed",
});
throw error;
}
}
/**
* Update the authenticated user's name and refresh cache entries.
* @param {string} userId Target user identifier.
* @param {string} name New display name.
* @returns {Promise<void>} Resolves when persistence is complete.
*/
async updateOwnName(userId, name) {
try {
const user = await User.findById(userId);
if (!user) {
throw new Error("User not found");
}
user.name = name;
const updatedUser = await user.save();
await CacheService.updateUser(CacheService.toUserResponse(updatedUser));
logger.info({ userId, name }, "User updated their own name");
}
catch (error) {
logger.error({
error,
userId,
message: "Update own name failed",
});
throw error;
}
}
/**
* Update a user's record by email (admin only).
* @param {string} email User email lookup key.
* @param {string} name New display name.
* @param {TUserRole} [role] Optional role override.
* @returns {Promise<void>} Resolves when the user has been updated.
*/
async updateUserByEmail(email, name, role) {
try {
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
throw new Error("User not found");
}
user.name = name;
if (role !== undefined) {
user.role = role;
}
const updatedUser = await user.save();
await CacheService.updateUser(CacheService.toUserResponse(updatedUser));
logger.info({ email, name, role }, "Admin updated user");
}
catch (error) {
logger.error({
error,
email,
message: "Update user by email failed",
});
throw error;
}
}
}
Source