"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 otp_entity_1 = require("../../auth/entities/otp.entity");
const nest_auth_contracts_2 = require("@ackplus/nest-auth-contracts");
const otp_1 = require("../../utils/otp");
const ms_1 = __importDefault(require("ms"));
const auth_config_service_1 = require("../../core/services/auth-config.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 user_2fa_enabled_event_1 = require("../events/user-2fa-enabled.event");
const user_2fa_disabled_event_1 = require("../events/user-2fa-disabled.event");
let MfaService = class MfaService {
    mfaSecretRepository;
    userRepository;
    otpRepository;
    trustedDeviceRepository;
    eventEmitter;
    constructor(mfaSecretRepository, userRepository, otpRepository, trustedDeviceRepository, eventEmitter) {
        this.mfaSecretRepository = mfaSecretRepository;
        this.userRepository = userRepository;
        this.otpRepository = otpRepository;
        this.trustedDeviceRepository = trustedDeviceRepository;
        this.eventEmitter = eventEmitter;
    }
    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 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)) {
            verifiedMethods.push(nest_auth_contracts_1.NestAuthMFAMethodEnum.EMAIL);
        }
        if (this.mfaConfig.methods?.includes(nest_auth_contracts_1.NestAuthMFAMethodEnum.SMS)) {
            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 options = auth_config_service_1.AuthConfigService.getOptions();
        let code;
        if (options.otp?.generate) {
            code = await options.otp.generate(this.mfaConfig.otpLength);
        }
        else {
            code = (0, otp_1.generateOtp)(this.mfaConfig.otpLength);
        }
        let expiresAtMs;
        if (typeof this.mfaConfig.otpExpiresIn === 'string') {
            expiresAtMs = (0, ms_1.default)(this.mfaConfig.otpExpiresIn);
        }
        else {
            expiresAtMs = this.mfaConfig.otpExpiresIn || 900000;
        }
        if (!expiresAtMs || isNaN(expiresAtMs) || expiresAtMs <= 0) {
            throw new Error(`Invalid MFA configuration: otpExpiresIn '${this.mfaConfig.otpExpiresIn}' results in invalid duration`);
        }
        await this.otpRepository.delete({
            userId,
            type: nest_auth_contracts_2.NestAuthOTPTypeEnum.MFA
        });
        const otp = await this.otpRepository.create({
            userId,
            type: nest_auth_contracts_2.NestAuthOTPTypeEnum.MFA,
            expiresAt: new Date(Date.now() + expiresAtMs),
            code,
        });
        await this.otpRepository.save(otp);
        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: user.tenantId,
                    method,
                    code,
                }));
            }
        }
        return true;
    }
    async verifyMfa(userId, inputOtp, method) {
        this.requireMfaEnabledForApp(true);
        if (this.mfaConfig.defaultOtp && this.mfaConfig.defaultOtp === inputOtp) {
            return 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) {
            const otp = await this.otpRepository.findOne({
                where: {
                    userId,
                    type: nest_auth_contracts_2.NestAuthOTPTypeEnum.MFA,
                    used: false,
                    expiresAt: (0, typeorm_2.MoreThan)(new Date()),
                    code: inputOtp
                }
            });
            if (!otp) {
                return false;
            }
            await this.otpRepository.delete(otp.id);
            return true;
        }
        return false;
    }
    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.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 });
    }
    async generateRecoveryCode(userId) {
        this.checkIsMfaEnabledForApp(true);
        const secret = speakeasy_1.default.generateSecret({
            name: `NestAuth:${userId}`,
        });
        await this.userRepository.update(userId, { mfaRecoveryCode: secret.base32 });
        return secret.base32;
    }
    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
            });
        }
        if (user.mfaRecoveryCode === code) {
            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)];
    }
    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);
    }
    async createTrustedDevice(userId, userAgent, ipAddress) {
        this.requireMfaEnabledForApp(true);
        const token = (0, crypto_1.randomBytes)(32).toString('hex');
        const duration = this.mfaConfig.trustedDeviceDuration || '30d';
        const expiresAtMs = typeof duration === 'string' ? (0, ms_1.default)(duration) : duration;
        await this.trustedDeviceRepository.save({
            userId,
            token,
            userAgent,
            ipAddress,
            expiresAt: new Date(Date.now() + expiresAtMs),
        });
        return token;
    }
    async validateTrustedDevice(userId, token) {
        if (!token)
            return false;
        const device = await this.trustedDeviceRepository.findOne({
            where: { userId, token },
        });
        if (!device)
            return false;
        if (device.expiresAt < new Date()) {
            await this.trustedDeviceRepository.remove(device);
            return false;
        }
        await this.trustedDeviceRepository.update(device.id, { lastUsedAt: new Date() });
        return true;
    }
};
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)(otp_entity_1.NestAuthOTP)),
    __param(3, (0, typeorm_1.InjectRepository)(trusted_device_entity_1.NestAuthTrustedDevice)),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        event_emitter_1.EventEmitter2])
], MfaService);
//# sourceMappingURL=mfa.service.js.map