Source

services/auth.service.js

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;
        }
    }
}