Source

middlewares/auth.js

import { vaultSecrets } from "@config/vault-secrets";
import { AuthGrpcService } from "@services/auth.service";
import { UserRole } from "@typez/auth";
import { getLogger } from "@utils/asyncLocalStorage";
import { SchemaUtils } from "@utils/schema";
import jwt from "jsonwebtoken";
/**
 * Create authentication middleware for Express routes
 *
 * This middleware validates JWT tokens and optionally checks with the Auth Service
 * for additional security. It attaches user information to the request object.
 * @param options - Configuration options for the middleware
 * @param options.jwtSecret - JWT secret for token validation
 * @param options.validateWithAuthService - Whether to validate with Auth Service (optional)
 * @param options.requiredRoles - Required user roles for access (optional)
 * @returns Express middleware function
 */
/**
 * Create authentication middleware for Express routes
 * @category Middlewares
 * @param {IAuthMiddlewareOptions} options Configuration options for the middleware
 * @returns {Function} Express middleware function
 */
export function createAuthMiddleware(options) {
    const logger = getLogger();
    logger.info({ options }, "createAuthMiddleware");
    return (req, res, next) => {
        try {
            // Extract Authorization header
            const authHeader = req.headers.authorization;
            const jwtSecret = vaultSecrets.get("JWT_SECRET");
            if (!authHeader) {
                res.status(401).json({
                    success: false,
                    message: "Authorization header is required",
                    error: "MISSING_AUTH_HEADER",
                });
                return;
            }
            // Validate header format and extract token
            let token;
            try {
                SchemaUtils.validateAuthHeader(authHeader);
                token = SchemaUtils.extractTokenFromHeader(authHeader);
            }
            catch {
                res.status(401).json({
                    success: false,
                    message: "Invalid Authorization header format",
                    error: "INVALID_AUTH_HEADER",
                });
                return;
            }
            try {
                const decoded = jwt.verify(token, jwtSecret);
                const payload = SchemaUtils.validateJWTPayload(decoded);
                const user = {
                    userId: payload.userId,
                    name: payload.name,
                    role: payload.role,
                    avatar: payload.avatar,
                    expiresAt: payload.exp ? new Date(payload.exp * 1000) : new Date(),
                };
                logger.info({ user }, "User information");
                // Check required roles
                if (options.requiredRoles && options.requiredRoles.length > 0) {
                    if (!options.requiredRoles.includes(user.role)) {
                        res.status(403).json({
                            success: false,
                            message: "Insufficient permissions",
                            error: "INSUFFICIENT_PERMISSIONS",
                        });
                        return;
                    }
                }
                const grpcClient = new AuthGrpcService(token);
                req.grpcClient = grpcClient;
                // Attach user and token to request
                req.user = user;
                next();
            }
            catch (error) {
                logger.error({ error }, "Authentication error");
                res.status(401).json({
                    success: false,
                    message: "Invalid token",
                });
                return;
            }
        }
        catch (error) {
            if (error instanceof AuthenticationError) {
                res.status(error.status).json({
                    success: false,
                    message: error.message,
                    error: error.code,
                });
            }
        }
    };
}
// Custom error class for authentication
class AuthenticationError extends Error {
    message;
    status;
    code;
    constructor(message, status, code) {
        super(message);
        this.message = message;
        this.status = status;
        this.code = code;
        this.name = "AuthenticationError";
    }
}
// // Hybrid validation: Local first, fallback to Auth Service
// async function validateTokenHybrid(
//   token: string,
//   options: IAuthMiddlewareOptions,
//   cacheDuration: number,
// ): Promise<IAuthUser> {
//   // Try local validation first (fast)
//   if (options.jwtSecret) {
//     try {
//       const decoded = jwt.verify(token, options.jwtSecret);
//       const payload = SchemaUtils.validateJWTPayload(decoded);
//       return {
//         userId: payload.userId,
//         name: payload.name,
//         role: payload.role,
//         avatar: payload.avatar,
//         expiresAt: payload.exp ? new Date(payload.exp * 1000) : new Date(),
//       };
//     } catch (error) {
//       logger.error(
//         {
//           error: error instanceof Error ? error.message : "Unknown error",
//         },
//         "Local validation failed, trying Auth Service",
//       );
//       // Local validation failed, try Auth Service
//       // Silent fallback to Auth Service validation
//     }
//   }
//   // Fallback to Auth Service validation
//   // Check cache first
//   const cached = validationCache.get(token);
//   if (cached && Date.now() - cached.timestamp < cacheDuration) {
//     return cached.result;
//   }
//   try {
//     const authClient = AuthServiceClient.getInstance();
//     const validationResult = await authClient.validateToken(token);
//     if (!validationResult || !validationResult.isValid) {
//       throw new AuthenticationError("Token validation failed", 401, "TOKEN_VALIDATION_FAILED");
//     }
//     const user: IAuthUser = {
//       userId: validationResult.userId ?? "",
//       name: validationResult.name ?? "",
//       avatar: validationResult.avatar,
//       role: validationResult.role
//         ? UserReferenceUtils.protoValueToRole(validationResult.role as unknown as ProtoUserRole)
//         : UserRole.SME,
//       expiresAt: validationResult.expiresAt ?? new Date(),
//     };
//     // Cache the result
//     validationCache.set(token, { result: user, timestamp: Date.now() });
//     return user;
//   } catch (error) {
//     if (error instanceof AuthenticationError) {
//       throw error;
//     }
//     throw new AuthenticationError("Auth Service unavailable", 503, "AUTH_SERVICE_ERROR");
//   }
// }
/**
 * Create middleware that requires admin role
 * @category Middlewares
 * @param {Omit<IAuthMiddlewareOptions, "requiredRoles">} options Configuration options (excluding requiredRoles)
 * @returns {Function} Express middleware function that only allows admin users
 */
export function requireAdmin(options = {}) {
    return createAuthMiddleware({
        ...options,
        requiredRoles: [UserRole.ADMIN],
    });
}
/**
 * Create middleware that requires authentication (admin or SME role)
 * @category Middlewares
 * @param {IAuthMiddlewareOptions} options Configuration options (excluding requiredRoles)
 * @returns {Function} Express middleware function that allows authenticated users
 */
export const requireAuthenticated = createAuthMiddleware({
    requiredRoles: [UserRole.ADMIN, UserRole.SME],
});
/**
 * Create optional authentication middleware
 * @category Middlewares
 * @param {IAuthMiddlewareOptions} options Configuration options for the middleware
 * @returns {Function} Express middleware function that optionally authenticates
 */
export function optionalAuth(options) {
    return (req, res, next) => {
        const authHeader = req.headers.authorization;
        if (!authHeader) {
            // No token provided, continue without authentication
            next();
            return;
        }
        // If token is provided, validate it
        createAuthMiddleware(options)(req, res, next);
    };
}
/**
 * Export the authentication middleware functions
 * @category Middlewares
 * @returns {Record<string, Function>} Authentication middleware functions
 */
export default {
    createAuthMiddleware,
    requireAdmin,
    requireAuthenticated,
    optionalAuth,
};