"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);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MfaService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const mfa_secret_entity_1 = require("../../auth/entities/mfa-secret.entity");
const speakeasy_1 = __importDefault(require("speakeasy"));
const qrcode_1 = __importDefault(require("qrcode"));
const nest_auth_contracts_1 = require("@ackplus/nest-auth-contracts");
const auth_constants_1 = require("../../auth.constants");
const user_entity_1 = require("../../user/entities/user.entity");
const nest_auth_contracts_2 = require("@ackplus/nest-auth-contracts");
const auth_config_service_1 = require("../../core/services/auth-config.service");
const otp_flow_service_1 = require("./otp-flow.service");
const event_emitter_1 = require("@nestjs/event-emitter");
const two_factor_code_sent_event_1 = require("../events/two-factor-code-sent.event");
const trusted_device_entity_1 = require("../entities/trusted-device.entity");
const crypto_1 = require("crypto");
const has_token_1 = require("../../utils/has-token");
const typeorm_3 = require("typeorm");
const user_2fa_enabled_event_1 = require("../events/user-2fa-enabled.event");
const user_2fa_disabled_event_1 = require("../events/user-2fa-disabled.event");
const request_context_1 = require("../../request-context/request-context");
const ms_1 = __importDefault(require("ms"));
let MfaService = class MfaService {
    mfaSecretRepository;
    userRepository;
    trustedDeviceRepository;
    eventEmitter;
    otpFlow;
    constructor(mfaSecretRepository, userRepository, trustedDeviceRepository, eventEmitter, otpFlow) {
        this.mfaSecretRepository = mfaSecretRepository;
        this.userRepository = userRepository;
        this.trustedDeviceRepository = trustedDeviceRepository;
        this.eventEmitter = eventEmitter;
        this.otpFlow = otpFlow;
    }
    get mfaConfig() {
        return auth_config_service_1.AuthConfigService.getOptions().mfa || {};
    }
    requireMfaEnabledForApp(throwError = true) {
        if (!this.mfaConfig.enabled) {
            if (throwError) {
                throw new common_1.ForbiddenException({
                    message: 'MFA is not enabled for the application',
                    code: auth_constants_1.ERROR_CODES.MFA_NOT_ENABLED,
                });
            }
            return false;
        }
        return true;
    }
    checkIsMfaEnabledForApp(throwError = true) {
        return this.requireMfaEnabledForApp(throwError);
    }
    async getVerifiedMethods(userId) {
        if (!this.requireMfaEnabledForApp(false)) {
            return [];
        }
        const user = await this.userRepository.findOne({ where: { id: userId } });
        const verifiedMethods = [];
        const verifiedTotpDevice = await this.mfaSecretRepository.findOne({
            where: {
                userId,
                verified: true,
            },
        });
        if (verifiedTotpDevice && this.mfaConfig.methods?.includes(nest_auth_contracts_1.NestAuthMFAMethodEnum.TOTP)) {
            verifiedMethods.push(nest_auth_contracts_1.NestAuthMFAMethodEnum.TOTP);
        }
        if (this.mfaConfig.methods?.includes(nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL) && user?.email) {
            verifiedMethods.push(nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL);
        }
        if (this.mfaConfig.methods?.includes(nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS) && user?.phone) {
            verifiedMethods.push(nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS);
        }
        return verifiedMethods;
    }
    async getEnabledMethods(userId) {
        if (!this.requireMfaEnabledForApp(false)) {
            return [];
        }
        const isEnabled = await this.isMfaEnabled(userId);
        if (!isEnabled) {
            return [];
        }
        const enableMethod = [];
        if (this.mfaConfig.methods?.includes(nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL)) {
            enableMethod.push(nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL);
        }
        if (this.mfaConfig.methods?.includes(nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS)) {
            enableMethod.push(nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS);
        }
        const verifiedTotpDevice = await this.mfaSecretRepository.findOne({
            where: {
                userId,
                verified: true,
            },
        });
        if (verifiedTotpDevice) {
            enableMethod.push(nest_auth_contracts_1.NestAuthMFAMethodEnum.TOTP);
        }
        return enableMethod;
    }
    async sendMfaCode(userId, method) {
        this.requireMfaEnabledForApp(true);
        const { plainCode } = await this.otpFlow.createOtp({
            userId,
            type: nest_auth_contracts_2.NestAuthOTPTypeEnum.MFA,
            replaceExisting: true,
        });
        if (method === nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL || method === nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS) {
            const user = await this.userRepository.findOne({ where: { id: userId } });
            if (user) {
                await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.TWO_FACTOR_CODE_SENT, new two_factor_code_sent_event_1.TwoFactorCodeSentEvent({
                    user,
                    tenantId: request_context_1.RequestContext.currentTenantId(),
                    method,
                    code: plainCode,
                }));
            }
        }
        return true;
    }
    async verifyMfa(userId, inputOtp, method) {
        this.requireMfaEnabledForApp(true);
        if (method === nest_auth_contracts_1.NestAuthMFAMethodEnum.TOTP) {
            const devices = await this.mfaSecretRepository.find({
                where: { userId, verified: true }
            });
            for (const device of devices) {
                const isValid = speakeasy_1.default.totp.verify({
                    secret: device.secret,
                    encoding: 'base32',
                    token: inputOtp,
                    window: 1
                });
                if (isValid) {
                    await this.mfaSecretRepository.update({ id: device.id }, { lastUsedAt: new Date() });
                    return true;
                }
            }
            return false;
        }
        if (method === nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL || method === nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS) {
            try {
                await this.otpFlow.validateAndConsume({
                    userId,
                    type: nest_auth_contracts_2.NestAuthOTPTypeEnum.MFA,
                    code: inputOtp,
                });
                await this.markChannelVerified(userId, method);
                return true;
            }
            catch {
                return false;
            }
        }
        return false;
    }
    async markChannelVerified(userId, method) {
        try {
            if (method === nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL) {
                await this.userRepository.update({ id: userId, emailVerifiedAt: (0, typeorm_3.IsNull)() }, { emailVerifiedAt: new Date() });
            }
            else if (method === nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS) {
                await this.userRepository.update({ id: userId, phoneVerifiedAt: (0, typeorm_3.IsNull)() }, { phoneVerifiedAt: new Date() });
            }
        }
        catch {
        }
    }
    async setupTotpDevice(userId, deviceName) {
        this.requireMfaEnabledForApp(true);
        const user = await this.userRepository.findOne({ where: { id: userId } });
        if (!user) {
            throw new Error('User not found');
        }
        const secret = speakeasy_1.default.generateSecret();
        await this.mfaSecretRepository.save({
            userId,
            secret: secret.base32,
            deviceName: deviceName || 'Authenticator',
            verified: false
        });
        const qrCode = await qrcode_1.default.toDataURL(secret.otpauth_url || '');
        return { secret: secret.base32, qrCode };
    }
    async verifyTotpSetup(userId, secret, inputOtp) {
        this.requireMfaEnabledForApp(true);
        const device = await this.mfaSecretRepository.findOne({
            where: { userId, secret }
        });
        if (device) {
            if (device.verified) {
                return true;
            }
            const isValid = speakeasy_1.default.totp.verify({
                secret: device.secret,
                encoding: 'base32',
                token: inputOtp,
                window: 1
            });
            if (isValid) {
                await this.mfaSecretRepository.update({ id: device.id }, { verified: true });
                return true;
            }
        }
        return false;
    }
    async getTotpDevices(userId) {
        this.requireMfaEnabledForApp(true);
        const devices = await this.mfaSecretRepository.find({
            select: ['id', 'deviceName', 'lastUsedAt', 'verified', 'createdAt'],
            where: { userId },
            order: { lastUsedAt: 'DESC', createdAt: 'DESC' }
        });
        return devices.map(device => ({
            id: device.id,
            deviceName: device.deviceName,
            method: nest_auth_contracts_1.NestAuthMFAMethodEnum.TOTP,
            lastUsedAt: device.lastUsedAt,
            createdAt: device.createdAt,
            verified: device.verified,
        }));
    }
    async removeDevice(deviceId) {
        this.requireMfaEnabledForApp(true);
        await this.mfaSecretRepository.delete({ id: deviceId });
    }
    async isRequiresMfa(userId) {
        if (!this.mfaConfig.enabled) {
            return false;
        }
        if (this.mfaConfig.required) {
            return true;
        }
        const user = await this.userRepository.findOne({
            select: ['id', 'isMfaEnabled'],
            where: { id: userId },
        });
        return !!user?.isMfaEnabled;
    }
    async isMfaEnabled(userId) {
        if (this.mfaConfig.enabled) {
            const user = await this.userRepository.findOne({
                select: ['id', 'isMfaEnabled'],
                where: { id: userId },
            });
            return !!user?.isMfaEnabled;
        }
        return false;
    }
    async markAsVerified(userId, deviceId) {
        this.requireMfaEnabledForApp(true);
        await this.mfaSecretRepository.update({ id: deviceId, userId }, { verified: true });
    }
    async enableMFA(userId) {
        this.requireMfaEnabledForApp(true);
        if (!this.mfaConfig.allowUserToggle) {
            throw new common_1.ForbiddenException({
                message: 'MFA toggling is not allowed',
                code: auth_constants_1.ERROR_CODES.MFA_TOGGLING_NOT_ALLOWED,
            });
        }
        const verifiedMethods = await this.getVerifiedMethods(userId);
        if (verifiedMethods.length === 0) {
            throw new common_1.ForbiddenException({
                message: 'Cannot enable MFA without at least one verified method',
                code: auth_constants_1.ERROR_CODES.MFA_CANNOT_ENABLE_WITHOUT_METHOD,
            });
        }
        await this.userRepository.update(userId, { isMfaEnabled: true });
        const user = await this.userRepository.findOne({ where: { id: userId } });
        if (user) {
            await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.TWO_FACTOR_ENABLED, new user_2fa_enabled_event_1.User2faEnabledEvent({
                user,
                method: verifiedMethods[0]
            }));
        }
    }
    async disableMFA(userId) {
        this.checkIsMfaEnabledForApp(true);
        if (this.mfaConfig.required) {
            throw new common_1.ForbiddenException({
                message: 'MFA is required and cannot be disabled',
                code: auth_constants_1.ERROR_CODES.MFA_TOGGLING_NOT_ALLOWED,
            });
        }
        if (!this.mfaConfig.allowUserToggle) {
            throw new common_1.ForbiddenException({
                message: 'MFA toggling is not allowed',
                code: auth_constants_1.ERROR_CODES.MFA_TOGGLING_NOT_ALLOWED,
            });
        }
        await this.userRepository.update(userId, { isMfaEnabled: false });
        const user = await this.userRepository.findOne({ where: { id: userId } });
        if (user) {
            await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.TWO_FACTOR_DISABLED, new user_2fa_disabled_event_1.User2faDisabledEvent({
                user
            }));
        }
    }
    async removeTotpDevice(deviceId) {
        this.checkIsMfaEnabledForApp(true);
        await this.mfaSecretRepository.delete({ id: deviceId });
    }
    getRecoveryCodeSecret() {
        const opts = auth_config_service_1.AuthConfigService.getOptions();
        const secret = opts.mfa?.trustedDeviceSecret || opts.session?.jwt?.secret;
        if (!secret) {
            throw new Error('Recovery code HMAC secret is not configured. Set mfa.trustedDeviceSecret or session.jwt.secret.');
        }
        return secret;
    }
    async generateRecoveryCode(userId) {
        this.checkIsMfaEnabledForApp(true);
        const plainCode = (0, crypto_1.randomBytes)(20).toString('base64url');
        const hashed = (0, has_token_1.hmacSha256Hex)(this.getRecoveryCodeSecret(), plainCode);
        await this.userRepository.update(userId, { mfaRecoveryCode: hashed });
        return plainCode;
    }
    async resetMfa(userId, code) {
        this.checkIsMfaEnabledForApp(true);
        const user = await this.userRepository.findOne({ where: { id: userId } });
        if (!user) {
            throw new common_1.UnauthorizedException({
                message: 'User not found',
                code: auth_constants_1.ERROR_CODES.USER_NOT_FOUND
            });
        }
        const stored = user.mfaRecoveryCode || '';
        const computed = (0, has_token_1.hmacSha256Hex)(this.getRecoveryCodeSecret(), code);
        if (stored && (0, has_token_1.timingSafeEqualHex)(stored, computed)) {
            await this.userRepository.update(userId, { mfaRecoveryCode: null });
            await this.mfaSecretRepository.delete({ userId });
            return {
                message: 'Recovery code verified',
            };
        }
        throw new common_1.UnauthorizedException({
            message: 'Invalid recovery code',
            code: auth_constants_1.ERROR_CODES.MFA_RECOVERY_CODE_INVALID
        });
    }
    getAvailableMethods() {
        if (!this.requireMfaEnabledForApp(false)) {
            return [];
        }
        const methods = this.mfaConfig.methods ?? [];
        return [...new Set(methods)];
    }
    isMfaRequired() {
        return this.mfaConfig.required ?? false;
    }
    canUserToggleMfa() {
        const allowUserToggle = this.mfaConfig.allowUserToggle ?? false;
        const required = this.mfaConfig.required ?? false;
        return allowUserToggle && !required;
    }
    getMfaConfig() {
        return this.mfaConfig;
    }
    async hasRecoveryCode(userId) {
        if (!this.checkIsMfaEnabledForApp(false)) {
            return false;
        }
        const user = await this.userRepository.findOne({
            select: ['id', 'mfaRecoveryCode'],
            where: { id: userId },
        });
        return Boolean(user?.mfaRecoveryCode);
    }
    getTrustedDeviceSecret() {
        const secret = this.mfaConfig.trustedDeviceSecret;
        if (!secret) {
            throw new Error('Trusted device HMAC secret is not configured. Set mfa.trustedDeviceSecret or session.jwt.secret.');
        }
        return secret;
    }
    async createTrustedDevice(userId, userAgent, ipAddress) {
        this.requireMfaEnabledForApp(true);
        const plainToken = (0, crypto_1.randomBytes)(32).toString('base64url');
        const duration = this.mfaConfig.trustedDeviceDuration;
        const expiresAtMs = (0, ms_1.default)(duration);
        const secret = this.getTrustedDeviceSecret();
        const device = this.trustedDeviceRepository.create({
            userId,
            userAgent,
            ipAddress,
            expiresAt: new Date(Date.now() + expiresAtMs),
            revokedAt: null,
        });
        await device.setTrustToken(secret, plainToken);
        await this.trustedDeviceRepository.save(device);
        return plainToken;
    }
    async validateTrustedDevice(userId, token) {
        if (!token) {
            return false;
        }
        const secret = this.getTrustedDeviceSecret();
        const candidates = await this.trustedDeviceRepository.find({
            where: {
                userId,
                revokedAt: (0, typeorm_3.IsNull)(),
                expiresAt: (0, typeorm_3.MoreThan)(new Date()),
            },
            select: ['id', 'tokenHash'],
        });
        const now = new Date();
        for (const device of candidates) {
            if (!(await device.validateTrustToken(secret, token))) {
                continue;
            }
            device.lastUsedAt = now;
            await this.trustedDeviceRepository.save(device);
            return true;
        }
        return false;
    }
};
exports.MfaService = MfaService;
exports.MfaService = MfaService = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, typeorm_1.InjectRepository)(mfa_secret_entity_1.NestAuthMFASecret)),
    __param(1, (0, typeorm_1.InjectRepository)(user_entity_1.NestAuthUser)),
    __param(2, (0, typeorm_1.InjectRepository)(trusted_device_entity_1.NestAuthTrustedDevice)),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        event_emitter_1.EventEmitter2,
        otp_flow_service_1.OtpFlowService])
], MfaService);
//# sourceMappingURL=mfa.service.js.map