Source

middlewares/auth.js

import { JwtUtils } from "@utils/jwt.utils.js";
import jwt from "jsonwebtoken";
import { logger } from "../config/logger.js";
import { User } from "../models/User.model.js";
/**
 * Middleware verifying bearer tokens and populating `req.user`.
 * @category Middlewares
 * @param {IAuthRequest} req Express request containing auth headers.
 * @param {Response} res Express response instance.
 * @param {NextFunction} next Express next handler.
 * @returns {Promise<void>} Resolves when the request is forwarded.
 */
export async function authenticateToken(req, res, next) {
    try {
        const authHeader = req.headers.authorization;
        const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
        if (!token) {
            res.status(401).json({
                success: false,
                message: "Access token required",
            });
            return;
        }
        // Verify the token using JWT utils
        const decoded = JwtUtils.verifyAccessToken(token);
        // Check if user still exists
        const user = await User.findById(decoded.userId).select("-passwordHash");
        if (!user) {
            res.status(401).json({
                success: false,
                message: "User not found",
            });
            return;
        }
        // Attach user info to request
        req.user = {
            userId: user._id.toString(),
            email: user.email,
            name: user.name,
            role: user.role,
        };
        next();
    }
    catch (error) {
        logger.error({
            error,
            message: "Authentication error",
        });
        if (error instanceof jwt.JsonWebTokenError) {
            res.status(401).json({
                success: false,
                message: "Invalid token",
            });
        }
        else if (error instanceof jwt.TokenExpiredError) {
            res.status(401).json({
                success: false,
                message: "Token expired",
            });
        }
        else {
            res.status(500).json({
                success: false,
                message: "Authentication failed",
            });
        }
    }
}
/**
 * Middleware ensuring the requester is authenticated as an admin.
 * @category Middlewares
 * @param {IAuthRequest} req Express request with authenticated user context.
 * @param {Response} res Express response instance.
 * @param {NextFunction} next Express next handler.
 * @returns {void}
 */
export const requireAdmin = (req, res, next) => {
    if (!req.user) {
        res.status(401).json({
            success: false,
            message: "Authentication required",
        });
        return;
    }
    if (req.user.role !== "admin") {
        res.status(403).json({
            success: false,
            message: "Admin access required",
        });
        return;
    }
    next();
};
/**
 * Middleware ensuring the requester is authenticated.
 * @category Middlewares
 * @param {IAuthRequest} req Express request with authenticated user context.
 * @param {Response} res Express response instance.
 * @param {NextFunction} next Express next handler.
 * @returns {void}
 */
export const requireAuth = (req, res, next) => {
    if (!req.user) {
        res.status(401).json({
            success: false,
            message: "Authentication required",
        });
        return;
    }
    next();
};
/**
 * Optional authentication middleware
 *
 * Attempts to authenticate the user if an access token is provided in the Authorization header,
 * but never fails the request. This is useful for endpoints that should work both with and without
 * authentication (e.g., logout endpoint that should clear cache even with expired tokens).
 * @category Middlewares
 * @param {IAuthRequest} req Express request containing auth headers.
 * @param {Response} _res Express response instance.
 * @param {NextFunction} next Express next handler.
 * @returns {Promise<void>} Resolves when the request is forwarded.
 */
export async function optionalAuthenticateToken(req, _res, next) {
    try {
        const authHeader = req.headers.authorization;
        const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
        if (!token) {
            // No token provided, continue without authentication
            next();
            return;
        }
        try {
            // Try to verify the token first
            const decoded = JwtUtils.verifyAccessToken(token);
            // Check if user still exists
            const user = await User.findById(decoded.userId).select("-passwordHash");
            if (user) {
                // Attach user info to request
                req.user = {
                    userId: user._id.toString(),
                    email: user.email,
                    name: user.name,
                    role: user.role,
                };
            }
        }
        catch (error) {
            // If verification fails (e.g., token expired), try to decode without verification
            // This is useful for logout where we want to clear cache even with expired tokens
            if (error instanceof jwt.TokenExpiredError || error instanceof jwt.JsonWebTokenError) {
                try {
                    const decoded = jwt.decode(token);
                    if (decoded && typeof decoded === "object" && "userId" in decoded) {
                        // Attach minimal user info with just userId for cache operations
                        req.user = {
                            userId: decoded.userId,
                            email: "",
                            name: "",
                            role: "sme",
                        };
                    }
                }
                catch (decodeError) {
                    // If decode also fails, just continue without user info
                    logger.debug({ error: decodeError }, "Could not decode token in optional auth");
                }
            }
        }
        next();
    }
    catch (error) {
        // On any unexpected error, just continue without authentication
        logger.debug({ error }, "Optional authentication error, continuing without auth");
        next();
    }
}