"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NestAuthAuthGuard = void 0;
const common_1 = require("@nestjs/common");
const auth_constants_1 = require("../../auth.constants");
const core_1 = require("@nestjs/core");
const jwt_service_1 = require("../../core/services/jwt.service");
const session_manager_service_1 = require("../../session/services/session-manager.service");
const access_key_service_1 = require("../../user/services/access-key.service");
const skip_mfa_decorator_1 = require("../../core/decorators/skip-mfa.decorator");
const public_decorator_1 = require("../../core/decorators/public.decorator");
const must_change_password_decorator_1 = require("../../core/decorators/must-change-password.decorator");
const permissions_decorator_1 = require("../../core/decorators/permissions.decorator");
const role_decorator_1 = require("../../core/decorators/role.decorator");
const auth_config_service_1 = require("../../core/services/auth-config.service");
const cookie_helper_1 = require("../../utils/cookie.helper");
const lodash_1 = require("lodash");
const debug_logger_service_1 = require("../../core/services/debug-logger.service");
const user_entity_1 = require("../../user/entities/user.entity");
const role_mapper_util_1 = require("../../role/utils/role-mapper.util");
let NestAuthAuthGuard = class NestAuthAuthGuard {
    reflector;
    jwtService;
    sessionManager;
    accessKeyService;
    authConfigService;
    debugLogger;
    constructor(reflector, jwtService, sessionManager, accessKeyService, authConfigService, debugLogger) {
        this.reflector = reflector;
        this.jwtService = jwtService;
        this.sessionManager = sessionManager;
        this.accessKeyService = accessKeyService;
        this.authConfigService = authConfigService;
        this.debugLogger = debugLogger;
    }
    async canActivate(context) {
        const request = context.switchToHttp().getRequest();
        const isPublic = this.reflector.getAllAndOverride(public_decorator_1.IS_PUBLIC_KEY, [
            context.getHandler(),
            context.getClass(),
        ]);
        if (isPublic) {
            return true;
        }
        const isOptional = this.reflector.getAllAndOverride(auth_constants_1.OPTIONAL_AUTH_KEY, [
            context.getHandler(),
            context.getClass(),
        ]);
        request.user = null;
        request.session = null;
        request.accessKey = null;
        request.authType = null;
        const { token, authType } = this.extractToken(request);
        if (!token) {
            if (isOptional) {
                return true;
            }
            else {
                this.debugLogger.warn('Required auth - no token provided', 'AuthGuard');
                throw new common_1.UnauthorizedException({
                    message: 'No authentication provided',
                    code: auth_constants_1.ERROR_CODES.NO_AUTH_PROVIDED
                });
            }
        }
        let isAuthenticated = false;
        try {
            switch (authType) {
                case 'bearer':
                    this.debugLogger.debug('Handling JWT authentication', 'AuthGuard');
                    isAuthenticated = await this.handleJwtAuth(context, request, token, isOptional);
                    break;
                case 'apikey':
                    this.debugLogger.debug('Handling API key authentication', 'AuthGuard');
                    isAuthenticated = await this.handleApiKeyAuth(request, token, isOptional);
                    break;
                default:
                    this.debugLogger.warn(`Invalid auth type: ${authType}`, 'AuthGuard');
                    if (isOptional) {
                        return true;
                    }
                    else {
                        throw new common_1.UnauthorizedException({
                            message: 'Invalid authentication type',
                            code: auth_constants_1.ERROR_CODES.INVALID_AUTH_TYPE
                        });
                    }
            }
        }
        catch (error) {
            if (isOptional) {
                return true;
            }
            else {
                throw error;
            }
        }
        if (!isAuthenticated && !isOptional) {
            this.debugLogger.warn('Authentication failed (required)', 'AuthGuard');
            return false;
        }
        if (isAuthenticated) {
            this.checkMustChangePassword(context, request);
            await this.checkAuthorization(context, request);
        }
        return true;
    }
    checkMustChangePassword(context, request) {
        if (this.authConfigService.getConfig().mustChangePassword?.enforce !== true)
            return;
        if (request.mustChangePassword !== true)
            return;
        const exempt = this.reflector.getAllAndOverride(must_change_password_decorator_1.SKIP_MUST_CHANGE_PASSWORD_KEY, [
            context.getHandler(),
            context.getClass(),
        ]);
        if (exempt)
            return;
        this.debugLogger.warn('Blocked: password change required', 'AuthGuard');
        throw new common_1.ForbiddenException({
            message: 'You must change your password before continuing',
            code: auth_constants_1.ERROR_CODES.MUST_CHANGE_PASSWORD,
        });
    }
    extractToken(request) {
        const config = this.authConfigService.getConfig();
        const accessTokenType = config.session?.accessTokenType ?? null;
        const checkHeader = accessTokenType !== 'cookie';
        const checkCookie = accessTokenType !== 'header';
        if (checkHeader) {
            const authHeader = request.headers.authorization;
            if (authHeader) {
                const [type, token] = authHeader.split(' ');
                if (type && token) {
                    const authType = type.toLowerCase();
                    if (authType === 'bearer' || authType === 'apikey') {
                        return { token, authType };
                    }
                }
            }
        }
        if (checkCookie) {
            if (config.session?.allowMultipleAccounts === true) {
                const activeAccount = cookie_helper_1.CookieHelper.get(request, auth_constants_1.ACTIVE_ACCOUNT_COOKIE_NAME);
                if (activeAccount) {
                    const perAccountToken = cookie_helper_1.CookieHelper.get(request, (0, auth_constants_1.accountAccessCookieName)(activeAccount));
                    if (perAccountToken) {
                        return { token: perAccountToken, authType: 'bearer' };
                    }
                }
            }
            const cookieToken = cookie_helper_1.CookieHelper.get(request, auth_constants_1.ACCESS_TOKEN_COOKIE_NAME);
            if (cookieToken) {
                return { token: cookieToken, authType: 'bearer' };
            }
        }
        this.debugLogger.verbose('No token found in request', 'AuthGuard');
        return { token: null, authType: null };
    }
    resetAuth(request) {
        request.user = null;
        request.session = null;
        request.accessKey = null;
        request.authType = null;
    }
    async handleJwtAuth(context, request, token, isOptional = false) {
        try {
            const payload = await this.jwtService.verifyToken(token);
            const config = this.authConfigService.getConfig();
            if (config.guards?.beforeAuth) {
                const result = await config.guards.beforeAuth(request, payload);
                if (result && result.reject) {
                    throw new common_1.UnauthorizedException({
                        message: result.reason || 'Authentication rejected by custom guard',
                        code: auth_constants_1.ERROR_CODES.ACCESS_DENIED,
                    });
                }
            }
            request.user = payload;
            request.authType = 'jwt';
            const session = await this.sessionManager.getSession(payload.sessionId);
            if (!session) {
                if (isOptional) {
                    this.resetAuth(request);
                    return false;
                }
                throw new common_1.UnauthorizedException({
                    message: 'Session not found',
                    code: auth_constants_1.ERROR_CODES.SESSION_NOT_FOUND,
                });
            }
            if (config.session?.jwt?.validateToken) {
                const isValid = await config.session.jwt.validateToken(payload, session);
                if (!isValid) {
                    throw new common_1.UnauthorizedException({
                        message: 'Token validation failed',
                        code: auth_constants_1.ERROR_CODES.INVALID_TOKEN,
                    });
                }
            }
            request.session = session;
            const user = await user_entity_1.NestAuthUser.findOne({
                where: { id: session.userId },
                select: ['id', 'isActive', 'mustChangePassword'],
            });
            request.mustChangePassword = user?.mustChangePassword === true;
            if (!user || user.isActive === false) {
                if (isOptional) {
                    this.resetAuth(request);
                    return false;
                }
                throw new common_1.UnauthorizedException({
                    message: !user ? 'User not found' : 'User is not active',
                    code: auth_constants_1.ERROR_CODES.ACCOUNT_INACTIVE,
                });
            }
            await this.checkMfa(context, payload, isOptional);
            if (config.guards?.afterAuth && session.data?.user) {
                await config.guards.afterAuth(request, session.data.user, session);
            }
            return true;
        }
        catch (error) {
            if (isOptional) {
                this.resetAuth(request);
                return false;
            }
            if (error instanceof common_1.UnauthorizedException || error.status)
                throw error;
            throw new common_1.UnauthorizedException({
                message: 'Invalid or expired token',
                code: auth_constants_1.ERROR_CODES.INVALID_TOKEN,
            });
        }
    }
    async handleApiKeyAuth(request, token, isOptional = false) {
        const [publicKey, privateKey] = token.split('.');
        if (!publicKey || !privateKey) {
            this.debugLogger.warn('Invalid API key format', 'AuthGuard');
            if (isOptional) {
                return true;
            }
            else {
                throw new common_1.UnauthorizedException({
                    message: 'Invalid API key format',
                    code: auth_constants_1.ERROR_CODES.INVALID_API_KEY_FORMAT
                });
            }
        }
        try {
            const isValid = await this.accessKeyService.validateAccessKey(publicKey, privateKey);
            if (!isValid) {
                this.debugLogger.warn('Invalid API key', 'AuthGuard');
                if (isOptional) {
                    return true;
                }
                else {
                    throw new common_1.UnauthorizedException({
                        message: 'Invalid API key',
                        code: auth_constants_1.ERROR_CODES.INVALID_API_KEY
                    });
                }
            }
            const accessKey = await this.accessKeyService.getAccessKey(publicKey);
            await this.accessKeyService.updateAccessKeyLastUsed(publicKey);
            request.user = accessKey.user;
            request.accessKey = accessKey;
            request.authType = 'api-key';
            return true;
        }
        catch (error) {
            if (isOptional) {
                return true;
            }
            else {
                throw error;
            }
        }
    }
    async checkMfa(context, payload, isOptional = false) {
        const skipMfa = this.reflector.getAllAndOverride(skip_mfa_decorator_1.SKIP_MFA_KEY, [
            context.getHandler(),
            context.getClass(),
        ]);
        const isMfaEnabled = payload.isMfaEnabled;
        const isMfaVerified = payload.isMfaVerified;
        if (isMfaEnabled && !isMfaVerified && !skipMfa) {
            this.debugLogger.warn('MFA verification required but not verified', 'AuthGuard');
            if (isOptional) {
                return;
            }
            else {
                throw new common_1.UnauthorizedException({
                    message: 'Multi-factor authentication is required',
                    code: auth_constants_1.ERROR_CODES.MFA_REQUIRED
                });
            }
        }
    }
    async checkAuthorization(context, request) {
        const requiredPermissions = this.getRequiredPermissions(context);
        const requiredRoles = this.getRequiredRoles(context);
        const requiredGuard = this.getRequiredGuard(context);
        if (!requiredPermissions.length && !requiredRoles.length) {
            this.debugLogger.debug('No authorization requirements - allowing access', 'AuthGuard');
            return;
        }
        const user = request.user;
        const session = request.session;
        if (!user) {
            this.debugLogger.warn('Authorization check failed - no user', 'AuthGuard');
            throw new common_1.ForbiddenException({
                message: 'Access denied: User not authenticated',
                code: auth_constants_1.ERROR_CODES.UNAUTHORIZED,
            });
        }
        const rolesForAuth = session?.data?.roles || user.userAccesses?.map(access => access.roles).flat() || [];
        const permissionsForAuth = session?.data?.permissions;
        if (requiredRoles.length > 0) {
            await this.checkRoles(user, rolesForAuth, requiredRoles, requiredGuard);
        }
        if (requiredPermissions.length > 0) {
            const requireAll = this.getPermissionsRequireAll(context);
            await this.checkPermissions(user, rolesForAuth, permissionsForAuth, requiredPermissions, requireAll);
        }
    }
    getRequiredPermissions(context) {
        const permissions = this.reflector.getAllAndOverride(permissions_decorator_1.PERMISSIONS_KEY, [context.getHandler(), context.getClass()]);
        if (!permissions || permissions === undefined || permissions === null) {
            return [];
        }
        if (typeof permissions === 'string') {
            return permissions.trim() ? [permissions] : [];
        }
        if (Array.isArray(permissions)) {
            return permissions.filter(p => p && typeof p === 'string' && p.trim().length > 0);
        }
        return [];
    }
    getPermissionsRequireAll(context) {
        const requireAll = this.reflector.getAllAndOverride(permissions_decorator_1.PERMISSIONS_REQUIRE_ALL_KEY, [context.getHandler(), context.getClass()]);
        return requireAll !== false;
    }
    getRequiredRoles(context) {
        const roles = this.reflector.getAllAndOverride(role_decorator_1.ROLES_KEY, [context.getHandler(), context.getClass()]);
        if (!roles || roles === undefined || roles === null) {
            return [];
        }
        if (typeof roles === 'string') {
            return roles.trim() ? [roles] : [];
        }
        if (Array.isArray(roles)) {
            return roles.filter(r => r && typeof r === 'string' && r.trim().length > 0);
        }
        return [];
    }
    getRequiredGuard(context) {
        return this.reflector.getAllAndOverride(role_decorator_1.GUARD_KEY, [context.getHandler(), context.getClass()]);
    }
    async resolveUserRoles(user, roles) {
        const config = this.authConfigService.getConfig();
        if (config.authorization?.resolveRoles) {
            return await config.authorization.resolveRoles(user);
        }
        if (!roles || !Array.isArray(roles)) {
            return [];
        }
        return roles
            .filter((role) => role?.isActive !== false)
            .map((role) => role?.name)
            .filter((name) => name);
    }
    async checkRoles(user, rolesForAuth, requiredRoles, requiredGuard) {
        if (requiredGuard) {
            const userGuards = (0, lodash_1.uniq)(rolesForAuth
                .map((role) => role?.guard)
                .filter((guard) => guard));
            if (userGuards.length === 0 || !userGuards.includes(requiredGuard)) {
                throw new common_1.ForbiddenException({
                    message: `Access denied: Guard mismatch. Required: ${requiredGuard}, Found: ${userGuards.length > 0 ? userGuards.join(', ') : 'none'}`,
                    code: auth_constants_1.ERROR_CODES.GUARD_MISMATCH,
                });
            }
        }
        const userRoleNames = await this.resolveUserRoles(user, rolesForAuth);
        if (userRoleNames.length === 0) {
            throw new common_1.ForbiddenException({
                message: 'Access denied: No roles assigned',
                code: auth_constants_1.ERROR_CODES.NO_ROLES_ASSIGNED,
            });
        }
        const hasAllRoles = requiredRoles.every(role => userRoleNames.includes(role));
        if (!hasAllRoles) {
            const missingRoles = requiredRoles.filter(role => !userRoleNames.includes(role));
            throw new common_1.ForbiddenException({
                message: `Access denied: Missing required roles: ${missingRoles.join(', ')}`,
                code: auth_constants_1.ERROR_CODES.MISSING_REQUIRED_ROLES,
            });
        }
    }
    async checkPermissions(user, rolesForAuth, permissionsForAuth, requiredPermissions, requireAll = true) {
        const config = this.authConfigService.getConfig();
        let userPermissions = [];
        if (config.authorization?.resolvePermissions) {
            const roles = await this.resolveUserRoles(user, rolesForAuth);
            userPermissions = await config.authorization.resolvePermissions(user, roles);
        }
        else {
            if (permissionsForAuth && Array.isArray(permissionsForAuth)) {
                userPermissions = permissionsForAuth;
            }
            else {
                if (!rolesForAuth || !Array.isArray(rolesForAuth) || rolesForAuth.length === 0) {
                    throw new common_1.ForbiddenException({
                        message: 'Access denied: No roles assigned for permission check',
                        code: auth_constants_1.ERROR_CODES.NO_ROLES_ASSIGNED,
                    });
                }
                userPermissions = this.getUserPermissions(rolesForAuth);
            }
        }
        const hasRequired = requireAll
            ? requiredPermissions.every((p) => userPermissions.includes(p))
            : requiredPermissions.some((p) => userPermissions.includes(p));
        if (!hasRequired) {
            const missing = requiredPermissions.filter((p) => !userPermissions.includes(p));
            const message = requireAll
                ? `Access denied: Missing required permissions: ${missing.join(', ')}`
                : `Access denied: Requires at least one of: ${requiredPermissions.join(', ')}`;
            throw new common_1.ForbiddenException({
                message,
                code: auth_constants_1.ERROR_CODES.MISSING_REQUIRED_PERMISSIONS,
            });
        }
    }
    getUserPermissions(roles) {
        const permissions = new Set();
        roles.forEach(role => {
            if (role?.isActive === false)
                return;
            (0, role_mapper_util_1.getRolePermissionNames)(role).forEach((permission) => permissions.add(permission));
        });
        return Array.from(permissions);
    }
};
exports.NestAuthAuthGuard = NestAuthAuthGuard;
exports.NestAuthAuthGuard = NestAuthAuthGuard = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [core_1.Reflector,
        jwt_service_1.JwtService,
        session_manager_service_1.SessionManagerService,
        access_key_service_1.AccessKeyService,
        auth_config_service_1.AuthConfigService,
        debug_logger_service_1.DebugLoggerService])
], NestAuthAuthGuard);
//# sourceMappingURL=auth.guard.js.map