"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 AuthenticationService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthenticationService = void 0;
const axios_1 = require("@nestjs/axios");
const common_1 = require("@nestjs/common");
const event_emitter_1 = require("@nestjs/event-emitter");
const jwt_1 = require("@nestjs/jwt");
const typeorm_1 = require("@nestjs/typeorm");
const class_validator_1 = require("class-validator");
const crypto_1 = require("crypto");
const error_messages_1 = require("../constants/error-messages");
const success_messages_1 = require("../constants/success-messages");
const mail_factory_1 = require("../factories/mail.factory");
const user_repository_1 = require("../repository/user.repository");
const typeorm_2 = require("typeorm");
const uuid_1 = require("uuid");
const constants_1 = require("../constants");
const user_entity_1 = require("../entities/user.entity");
const interfaces_1 = require("../interfaces");
const hashing_service_1 = require("./hashing.service");
const refresh_token_ids_storage_service_1 = require("./refresh-token-ids-storage.service");
const sso_code_storage_service_1 = require("./sso-code-storage.service");
const role_metadata_service_1 = require("./role-metadata.service");
const setting_service_1 = require("./setting.service");
const user_activity_history_service_1 = require("./user-activity-history.service");
const user_service_1 = require("./user.service");
const sms_factory_1 = require("../factories/sms.factory");
const whatsapp_factory_1 = require("../factories/whatsapp.factory");
const solid_registry_1 = require("../helpers/solid-registry");
var LoginProvider;
(function (LoginProvider) {
    LoginProvider["LOCAL"] = "local";
    LoginProvider["GOOGLE"] = "google";
    LoginProvider["OTP"] = "otp";
})(LoginProvider || (LoginProvider = {}));
let AuthenticationService = AuthenticationService_1 = class AuthenticationService {
    constructor(userService, userRepository, hashingService, jwtService, refreshTokenIdsStorage, httpService, mailServiceFactory, smsFactory, whatsAppFactory, eventEmitter, settingService, roleMetadataService, userActivityHistoryService, ssoCodeStorage, dataSource, solidRegistry) {
        this.userService = userService;
        this.userRepository = userRepository;
        this.hashingService = hashingService;
        this.jwtService = jwtService;
        this.refreshTokenIdsStorage = refreshTokenIdsStorage;
        this.httpService = httpService;
        this.mailServiceFactory = mailServiceFactory;
        this.smsFactory = smsFactory;
        this.whatsAppFactory = whatsAppFactory;
        this.eventEmitter = eventEmitter;
        this.settingService = settingService;
        this.roleMetadataService = roleMetadataService;
        this.userActivityHistoryService = userActivityHistoryService;
        this.ssoCodeStorage = ssoCodeStorage;
        this.dataSource = dataSource;
        this.solidRegistry = solidRegistry;
        this.logger = new common_1.Logger(AuthenticationService_1.name);
    }
    async getCompanyLogo() {
        return this.settingService.getConfigValue("companylogo");
    }
    async resolveUser(username, email) {
        return await this.userRepository.findOne({
            where: [{ username: username }, { email: email }],
            relations: {
                roles: true,
            },
        });
    }
    async updatePasswordDetails(user, newPassword) {
        user.password = await this.hashingService.hash(newPassword);
        user.passwordScheme = this.hashingService.name();
        user.passwordSchemeVersion = this.hashingService.currentVersion();
        user.rehashedAt = new Date();
        await this.userRepository.update(user.id, {
            password: user.password,
            passwordScheme: user.passwordScheme,
            passwordSchemeVersion: user.passwordSchemeVersion,
            rehashedAt: user.rehashedAt,
        });
        return user;
    }
    async resolveUserByVerificationToken(token) {
        return await this.userRepository.findOne({
            where: { verificationTokenOnForgotPassword: token },
            relations: { roles: true },
        });
    }
    async validateUserForPasswordLogin(user, password) {
        if (!user.active) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_ACTIVE);
        }
        this.checkAccountBlocked(user);
        const isEqual = await this.hashingService.compare(password, user.password, user.passwordSchemeVersion);
        if (!isEqual) {
            await this.incrementFailedAttempts(user);
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_CREDENTIALS);
        }
    }
    async rehashPasswordIfRequired(user, password) {
        if (this.hashingService.needsRehash(user.password, user.passwordSchemeVersion)) {
            await this.updatePasswordDetails(user, password);
        }
    }
    async signUp(signUpDto, activeUser = null) {
        const hasExtensionFields = Object.keys(signUpDto).some((k) => !AuthenticationService_1.SIGNUP_DTO_KEYS.has(k));
        if (hasExtensionFields) {
            const provider = this.solidRegistry.getExtensionUserCreationProvider();
            if (!provider) {
                throw new common_1.InternalServerErrorException("No ExtensionUserCreationProvider registered. Register one to handle extension user creation.");
            }
            const entity = await provider.buildExtensionEntity(signUpDto);
            const effectiveDto = { ...signUpDto, roles: provider.roles(signUpDto) };
            return this.performSignUp(effectiveDto, entity, provider.repo);
        }
        return this.performSignUp(signUpDto, new user_entity_1.User(), this.userRepository);
    }
    async performSignUp(signUpDto, entity, repo) {
        try {
            const onForcePasswordChange = this.settingService.getConfigValue("forceChangePasswordOnFirstLogin");
            const activateUserOnRegistration = this.settingService.getConfigValue("activateUserOnRegistration");
            const defaultRole = this.settingService.getConfigValue("defaultRole");
            var { user, pwd, autoGeneratedPwd } = await this.populateForSignup(entity, signUpDto, activateUserOnRegistration, onForcePasswordChange);
            const privateDto = signUpDto;
            if (privateDto.isAllowedToGenerateApiKeys !== undefined) {
                user.isAllowedToGenerateApiKeys = privateDto.isAllowedToGenerateApiKeys;
            }
            const savedUser = await repo.save(user);
            const userRoles = signUpDto.roles ?? [];
            if ((signUpDto.roles?.length ?? 0) === 0 &&
                signUpDto.username !== "sa" &&
                defaultRole) {
                userRoles.push(defaultRole);
            }
            await this.handlePostSignup(savedUser, userRoles, pwd, autoGeneratedPwd);
            await this.handlePasswordlessSignupOtp(savedUser, signUpDto, autoGeneratedPwd, repo);
            this.triggerRegistrationEvent(savedUser);
            return savedUser;
        }
        catch (err) {
            const pgUniqueViolationErrorCode = "23505";
            if (err.code === pgUniqueViolationErrorCode) {
                throw new common_1.ConflictException(parseUniqueConstraintError(err.detail || error_messages_1.ERROR_MESSAGES.UNIQUE_CONSTRAINT_VIOLATION));
            }
            throw err;
        }
    }
    async handlePasswordlessSignupOtp(user, signUpDto, autoGeneratedPwd, repo) {
        const isPasswordProvided = !!signUpDto.password;
        const isAutoGeneratedPassword = !!autoGeneratedPwd;
        if (isPasswordProvided || isAutoGeneratedPassword) {
            return;
        }
        if (!user.mobile) {
            this.logger.warn(`Skipping passwordless signup OTP WhatsApp notification for user ${user.username}: mobile is missing.`);
            return;
        }
        const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
        if (!isPasswordlessRegistrationEnabled) {
            return;
        }
        await this.assignRegistrationOtp(constants_1.PasswordlessRegistrationValidateWhatSources.MOBILE, user);
        await repo.save(user);
        await this.notifyUserOnOtpInitiateRegistration(user, constants_1.PasswordlessRegistrationValidateWhatSources.MOBILE);
    }
    async signupForExtensionUser(signUpDto, extensionUserDto, extensionUserRepo) {
        const entity = extensionUserRepo.create(extensionUserDto);
        return this.performSignUp(signUpDto, entity, extensionUserRepo);
    }
    async populateForSignup(user, signUpDto, isUserActive = true, onForcePasswordChange) {
        let autoGeneratedPwdPermission = this.settingService.getConfigValue("iamAutoGeneratedPassword");
        if (signUpDto.roles && signUpDto.roles.length > 0) {
            for (let i = 0; i < signUpDto.roles.length; i++) {
                const roleName = signUpDto.roles[i];
                await this.roleMetadataService.findRoleByName(roleName);
            }
        }
        user.username = signUpDto.username;
        user.email = signUpDto.email;
        user.fullName = signUpDto.fullName;
        user.forcePasswordChange = onForcePasswordChange;
        if (signUpDto.mobile) {
            user.mobile = signUpDto.mobile;
        }
        let pwd = "";
        let autoGeneratedPwd = "";
        if (signUpDto.password) {
            pwd = await this.hashingService.hash(signUpDto.password);
        }
        else {
            if (autoGeneratedPwdPermission?.toString().toLowerCase() === "true") {
                autoGeneratedPwd = this.generatePassword();
                pwd = await this.hashingService.hash(autoGeneratedPwd);
                user.forcePasswordChange = true;
            }
            else {
                if (!(await this.isPasswordlessRegistrationEnabled())) {
                    this.logger.error("User being created without password, and password less login is also not enabled in the system. Is this intentional?");
                    throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
                }
                pwd = await this.hashingService.hash(pwd);
            }
        }
        user.password = pwd;
        user.passwordScheme = this.hashingService.name();
        user.passwordSchemeVersion = this.hashingService.currentVersion();
        user.active = isUserActive;
        return { user, pwd, autoGeneratedPwd };
    }
    async handlePostSignup(user, roles = [], pwd, autoGeneratedPwd) {
        await this.userService.initializeRolesForNewUser(roles, user);
        if (user.forcePasswordChange && autoGeneratedPwd) {
            await this.notifyUserOnForcePasswordChange(user, autoGeneratedPwd);
        }
        await this.notifyUserOnSignup(user);
    }
    generatePassword(length = 8) {
        const upperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        const lowerCase = "abcdefghijklmnopqrstuvwxyz";
        const numbers = "0123456789";
        const specialChars = "@$#";
        const allChars = upperCase + lowerCase + numbers + specialChars;
        let password = "";
        for (let i = 0; i < length; i++) {
            const randomIndex = Math.floor(Math.random() * allChars.length);
            password += allChars[randomIndex];
        }
        return password;
    }
    async notifyUserOnForcePasswordChange(user, autoGeneratedPwd) {
        const companyLogo = await this.getCompanyLogo();
        const mailService = this.mailServiceFactory.getMailService();
        mailService.sendEmailUsingTemplate(user.email, "on-force-password-change", {
            solidAppName: this.settingService.getConfigValue("appTitle"),
            solidAppWebsiteUrl: this.settingService.getConfigValue("solidAppWebsiteUrl"),
            frontendLoginPageUrl: this.settingService.getConfigValue("frontendLoginPageUrl"),
            email: user.email,
            fullName: user.fullName,
            userName: user.username,
            password: autoGeneratedPwd,
            companyLogoUrl: companyLogo,
        }, this.settingService.getConfigValue("shouldQueueEmails"), null, null, "user", user.id);
    }
    async isWelcomeEmailEnabled() {
        const sendWelcomeEmailOnSignup = this.settingService.getConfigValue("sendWelcomeEmailOnSignup");
        return sendWelcomeEmailOnSignup;
    }
    async isWelcomeSmsEnabled() {
        const sendWelcomeSmsOnSignup = this.settingService.getConfigValue("sendWelcomeSmsOnSignup");
        return sendWelcomeSmsOnSignup;
    }
    async notifyUserOnSignup(user) {
        const companyLogo = await this.getCompanyLogo();
        if (await this.isWelcomeEmailEnabled()) {
            const mailService = this.mailServiceFactory.getMailService();
            mailService.sendEmailUsingTemplate(user.email, "email-on-signup", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                solidAppWebsiteUrl: this.settingService.getConfigValue("solidAppWebsiteUrl"),
                frontendLoginPageUrl: this.settingService.getConfigValue("frontendLoginPageUrl"),
                email: user.email,
                fullName: user.fullName,
                userName: user.username,
                companyLogoUrl: companyLogo,
            }, this.settingService.getConfigValue("shouldQueueEmails"), null, null, "user", user.id);
        }
        const isWelcomeSmsEnabled = await this.isWelcomeSmsEnabled();
        if (isWelcomeSmsEnabled && user.mobile) {
            const smsService = this.smsFactory.getSmsService();
            smsService.sendSMSUsingTemplate(user.mobile, "text-on-signup", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                frontendLoginPageUrl: this.settingService.getConfigValue("frontendLoginPageUrl"),
                firstName: user.username,
                fullName: user.fullName ? user.fullName : user.username,
            }, this.settingService.getConfigValue("shouldQueueSms"));
        }
    }
    async otpInitiateRegistration(signUpDto) {
        const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
        if (!isPasswordlessRegistrationEnabled) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
        }
        const validationSource = this.resolvePasswordlessValidationSource();
        this.validateOtpRegistrationInput(signUpDto, validationSource);
        const existingUser = await this.findExistingRegistrationUser(signUpDto);
        if ((0, class_validator_1.isNotEmpty)(existingUser) && existingUser.active) {
            throw new common_1.ConflictException(error_messages_1.ERROR_MESSAGES.USER_ALREADY_EXISTS);
        }
        try {
            const user = await this.upsertUserWithRegistrationVerificationTokens(existingUser, signUpDto, validationSource);
            await this.notifyUserOnOtpInitiateRegistration(user, validationSource);
        }
        catch (err) {
            if (err.code === "23505") {
                throw new common_1.ConflictException(error_messages_1.ERROR_MESSAGES.USER_ALREADY_EXISTS);
            }
            throw err;
        }
        return { message: success_messages_1.SUCCESS_MESSAGES.OTP_SENT_SUCCESS_REGISTRATION };
    }
    validateOtpRegistrationInput(signUpDto, validationSource) {
        if (validationSource === constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL &&
            (0, class_validator_1.isEmpty)(signUpDto.email)) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.EMAIL_REQUIRED_FOR_VALIDATION);
        }
        if (validationSource === constants_1.PasswordlessRegistrationValidateWhatSources.MOBILE &&
            (0, class_validator_1.isEmpty)(signUpDto.mobile)) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.MOBILE_REQUIRED_FOR_VALIDATION);
        }
    }
    async findExistingRegistrationUser(signUpDto) {
        return this.userRepository.findOne({
            where: [
                { email: signUpDto.email },
                { mobile: signUpDto.mobile },
                { username: signUpDto.username },
            ],
        });
    }
    resolvePasswordlessValidationSource() {
        return this.settingService.getConfigValue("passwordlessRegistrationValidateWhat");
    }
    async upsertUserWithRegistrationVerificationTokens(existingUser, signUpDto, validationSource) {
        let user = existingUser;
        if ((0, class_validator_1.isEmpty)(user)) {
            user = this.createUser(signUpDto);
            user.active = false;
            await this.assignRegistrationOtp(validationSource, user);
            await this.userRepository.save(user);
            await this.userService.addRoleToUser(user.username, this.settingService.getConfigValue("defaultRole"));
        }
        else {
            await this.assignRegistrationOtp(validationSource, user);
            await this.userRepository.save(user);
        }
        return user;
    }
    createUser(signUpDto) {
        const user = new user_entity_1.User();
        user.username = signUpDto.username;
        user.email = signUpDto.email;
        user.mobile = signUpDto.mobile;
        user.customPayload = signUpDto.customPayload;
        user.lastLoginProvider = LoginProvider.OTP;
        return user;
    }
    async assignRegistrationOtp(passwordlessRegistrationValidateWhat, user) {
        if (!passwordlessRegistrationValidateWhat) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.VALIDATION_SOURCE_REQUIRED);
        }
        const autoLoginUserOnRegistration = this.settingService.getConfigValue("autoLoginUserOnRegistration");
        if (passwordlessRegistrationValidateWhat ===
            constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL) {
            const { token, expiresAt } = await this.otp();
            user.emailVerificationTokenOnRegistration = token;
            user.emailVerificationTokenOnRegistrationExpiresAt = expiresAt;
            if (autoLoginUserOnRegistration) {
                user.emailVerificationTokenOnLogin = token;
                user.emailVerificationTokenOnLoginExpiresAt = expiresAt;
            }
        }
        if (passwordlessRegistrationValidateWhat ===
            constants_1.PasswordlessRegistrationValidateWhatSources.MOBILE) {
            const { token, expiresAt } = await this.otp();
            user.mobileVerificationTokenOnRegistration = token;
            user.mobileVerificationTokenOnRegistrationExpiresAt = expiresAt;
            if (autoLoginUserOnRegistration) {
                user.mobileVerificationTokenOnLogin = token;
                user.mobileVerificationTokenOnLoginExpiresAt = expiresAt;
            }
        }
    }
    async notifyUserOnOtpInitiateRegistration(user, registrationValidationSource) {
        const companyLogo = await this.getCompanyLogo();
        if (registrationValidationSource ===
            constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL) {
            const mailService = this.mailServiceFactory.getMailService();
            mailService.sendEmailUsingTemplate(user.email, "otp-on-register", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                solidAppWebsiteUrl: this.settingService.getConfigValue("solidAppWebsiteUrl"),
                firstName: user.username,
                fullName: user.fullName ? user.fullName : user.username,
                emailVerificationTokenOnRegistration: user.emailVerificationTokenOnRegistration,
                companyLogoUrl: companyLogo,
            }, this.settingService.getConfigValue("shouldQueueEmails"), null, null, "user", user.id);
        }
        if (registrationValidationSource ===
            constants_1.PasswordlessRegistrationValidateWhatSources.MOBILE) {
            const templateParams = {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                otp: user.mobileVerificationTokenOnRegistration,
                mobileVerificationTokenOnRegistration: user.mobileVerificationTokenOnRegistration,
                firstName: user.username,
                fullName: user.fullName ? user.fullName : user.username,
                companyLogoUrl: companyLogo,
            };
            const whatsappDestination = this.normalizeWhatsAppDestination(user.mobile);
            const gupshupTemplateAppName = process.env.COMMON_GUPSHUP_APP_NAME || "Gupshup";
            const whatsappTemplateId = this.settingService.getConfigValue("otpWhatsappTemplateId") || "common_otp";
            const whatsappIndependentEnabled = this.settingService.getConfigValue("otpWhatsappIndependentEnabled") !== false;
            let smsSent = false;
            let whatsappSent = false;
            let smsErrorMessage;
            let whatsappErrorMessage;
            this.logger.debug(`OTP SMS send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}`);
            try {
                const smsService = this.smsFactory.getSmsService();
                await smsService.sendSMSUsingTemplate(user.mobile, "otp-on-register", templateParams, false);
                smsSent = true;
            }
            catch (smsError) {
                smsErrorMessage = smsError?.message;
                this.logger.warn(`OTP SMS failed: destination=${user.mobile}, message=${smsErrorMessage}`);
            }
            if (whatsappIndependentEnabled) {
                if (!whatsappDestination) {
                    whatsappErrorMessage = "Normalized WhatsApp destination is empty";
                    this.logger.error(`Independent OTP WhatsApp skipped: destination=${user.mobile}, message=${whatsappErrorMessage}`);
                }
                else {
                    this.logger.debug(`Independent OTP WhatsApp send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`);
                    try {
                        await this.sendOtpToWhatsappProvider(whatsappDestination, String(whatsappTemplateId), String(gupshupTemplateAppName), String(templateParams.otp || ""));
                        whatsappSent = true;
                        this.logger.log(`Independent OTP WhatsApp success: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`);
                    }
                    catch (waError) {
                        whatsappErrorMessage = waError?.message;
                        this.logger.error(`Independent OTP WhatsApp failed: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}, message=${whatsappErrorMessage}`);
                    }
                }
            }
            if (!smsSent && !whatsappSent) {
                throw new Error(`OTP delivery failed on both channels. smsError=${smsErrorMessage || "unknown"}, whatsappError=${whatsappErrorMessage || "disabled/unknown"}`);
            }
        }
    }
    normalizeWhatsAppDestination(rawMobile) {
        const raw = (rawMobile || "").trim();
        let sanitized = raw.replace(/[^\d+]/g, "");
        if (sanitized.startsWith("00")) {
            sanitized = `+${sanitized.slice(2)}`;
        }
        const defaultDialCode = String(this.settingService.getConfigValue("otpDefaultCountryDialCode") || "").replace(/\D/g, "");
        if (sanitized.startsWith("+")) {
            const e164Digits = sanitized.slice(1).replace(/\D/g, "");
            if (e164Digits.length >= 8 && e164Digits.length <= 15) {
                return e164Digits;
            }
        }
        const digits = sanitized.replace(/\D/g, "");
        if (digits.length >= 11 && digits.length <= 15) {
            return digits;
        }
        if (digits.length === 10 && defaultDialCode) {
            return `${defaultDialCode}${digits}`;
        }
        return digits;
    }
    async sendOtpToWhatsappProvider(destination, templateId, appName, otp) {
        const whatsappService = this.whatsAppFactory.getWhatsappService();
        await whatsappService.sendWhatsAppMessage(destination, null, {
            type: "text",
            text: `${appName} OTP is ${otp}. It is valid for 10 mins.`,
        });
    }
    async otpConfirmRegistration(confirmSignUpDto) {
        const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
        if (!isPasswordlessRegistrationEnabled) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
        }
        const { type, identifier, otp } = confirmSignUpDto;
        if (type !== constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL &&
            type !== constants_1.PasswordlessRegistrationValidateWhatSources.MOBILE) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
        }
        const user = await this.findUserByRegistrationIdentifier(type, identifier);
        this.validateRegistrationOtp(user, otp, type);
        this.clearRegistrationOtp(user, type);
        user.active =
            this.settingService.getConfigValue("activateUserOnRegistration") &&
                (await this.areAllPasswordlessRegistrationValidationSourcesVerified(user));
        const savedUser = await this.userRepository.save(user);
        this.triggerRegistrationEvent(savedUser);
        return {
            active: savedUser.active,
            message: `User registration verified for ${type}`,
        };
    }
    async findUserByRegistrationIdentifier(type, identifier) {
        const where = type === constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL
            ? { email: identifier }
            : { mobile: identifier };
        const user = await this.userRepository.findOne({ where });
        if (!user) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        return user;
    }
    validateRegistrationOtp(user, otp, type) {
        const isEmail = type === constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL;
        const token = isEmail
            ? user.emailVerificationTokenOnRegistration
            : user.mobileVerificationTokenOnRegistration;
        const expiresAt = isEmail
            ? user.emailVerificationTokenOnRegistrationExpiresAt
            : user.mobileVerificationTokenOnRegistrationExpiresAt;
        if (token !== otp) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_OTP);
        }
        if (expiresAt < new Date()) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.OTP_EXPIRED);
        }
    }
    clearRegistrationOtp(user, type) {
        if (type === constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL) {
            user.emailVerifiedOnRegistrationAt = new Date();
            user.emailVerificationTokenOnRegistration = null;
            user.emailVerificationTokenOnRegistrationExpiresAt = null;
        }
        else {
            user.mobileVerifiedOnRegistrationAt = new Date();
            user.mobileVerificationTokenOnRegistration = null;
            user.mobileVerificationTokenOnRegistrationExpiresAt = null;
        }
    }
    triggerRegistrationEvent(savedUser) {
        const event = new interfaces_1.EventDetails(interfaces_1.EventType.USER_REGISTERED, savedUser);
        this.eventEmitter.emit(interfaces_1.EventType.USER_REGISTERED, event);
    }
    async areAllPasswordlessRegistrationValidationSourcesVerified(user) {
        const registrationValidationSource = this.resolvePasswordlessValidationSource();
        if (registrationValidationSource ===
            constants_1.PasswordlessRegistrationValidateWhatSources.EMAIL) {
            if (!user.emailVerifiedOnRegistrationAt) {
                return false;
            }
        }
        if (registrationValidationSource ===
            constants_1.PasswordlessRegistrationValidateWhatSources.MOBILE) {
            if (!user.mobileVerifiedOnRegistrationAt) {
                return false;
            }
        }
        return true;
    }
    async otp() {
        const now = new Date();
        const otpExpiry = this.settingService.getConfigValue("otpExpiry");
        now.setMinutes(now.getMinutes() + otpExpiry);
        return {
            token: (0, crypto_1.randomInt)(100000, 999999).toString(),
            expiresAt: now,
        };
    }
    getDummyOtpForUser(user) {
        const dummyOtp = this.settingService.getConfigValue("dummyOtp");
        if (!dummyOtp || !user?.username) {
            return undefined;
        }
        const allowedUsers = this.getDummyOtpUsers();
        if (!allowedUsers.size) {
            return undefined;
        }
        const username = user.username.trim().toLowerCase();
        if (!username) {
            return undefined;
        }
        return allowedUsers.has(username) ? dummyOtp : undefined;
    }
    getDummyOtpUsers() {
        const rawUsers = this.settingService.getConfigValue("dummyOtpUsers");
        if (!rawUsers || typeof rawUsers !== "string") {
            return new Set();
        }
        return new Set(rawUsers
            .split(",")
            .map((value) => value.trim().toLowerCase())
            .filter(Boolean));
    }
    async signIn(signInDto) {
        const user = await this.resolveUser(signInDto.username, signInDto.email);
        if (!user) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_CREDENTIALS);
        }
        await this.validateUserForPasswordLogin(user, signInDto.password);
        await this.rehashPasswordIfRequired(user, signInDto.password);
        await this.resetFailedAttempts(user);
        const tokens = await this.generateTokens(user);
        await this.userActivityHistoryService.logEvent("login", user);
        return {
            user: {
                email: user.email,
                mobile: user.mobile,
                username: user.username,
                forcePasswordChange: user.forcePasswordChange,
                id: user.id,
                roles: user.roles.map((role) => role.name),
            },
            ...tokens,
        };
    }
    maskEmail(email) {
        if (!email)
            return null;
        const [localPart, domain] = email.split("@");
        if (localPart.length <= 2) {
            return `${localPart[0]}***@${domain}`;
        }
        const visibleStart = localPart.slice(0, 2);
        const visibleEnd = localPart.slice(-1);
        return `${visibleStart}***${visibleEnd}@${domain}`;
    }
    maskMobile(mobile) {
        if (!mobile)
            return null;
        if (mobile.length <= 4) {
            return mobile;
        }
        const visibleEnd = mobile.slice(-4);
        return `***${visibleEnd}`;
    }
    async otpInitiateLogin(signInDto) {
        const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
        if (!isPasswordlessRegistrationEnabled) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
        }
        const type = this.resolveLoginType(signInDto);
        const user = await this.findUserForLogin(type, signInDto.identifier);
        const dummyOtp = this.getDummyOtpForUser(user);
        if (!dummyOtp) {
            await this.assignLoginOtp(user, type);
            await this.notifyUserOnOtpInititateLogin(user, type);
        }
        return this.buildLoginOtpResponse(user, type);
    }
    resolveLoginType(signInDto) {
        const setting = this.settingService.getConfigValue("passwordlessLoginValidateWhat");
        if (setting === constants_1.PasswordlessLoginValidateWhatSources.SELECTABLE) {
            if (signInDto.type !== constants_1.PasswordlessLoginValidateWhatSources.EMAIL &&
                signInDto.type !== constants_1.PasswordlessLoginValidateWhatSources.MOBILE) {
                throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
            }
            return signInDto.type;
        }
        if (setting === constants_1.PasswordlessLoginValidateWhatSources.EMAIL ||
            setting === constants_1.PasswordlessLoginValidateWhatSources.MOBILE) {
            return setting;
        }
        throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
    }
    async findUserForLogin(type, identifier, options = {}) {
        const typeWhere = type === constants_1.PasswordlessLoginValidateWhatSources.EMAIL
            ? { email: identifier }
            : { mobile: identifier };
        const user = await this.userRepository.findOne({
            where: [{ username: identifier }, typeWhere],
            ...(options.withRoles ? { relations: { roles: true } } : {}),
        });
        if (!user) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        if (!user.active) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_INACTIVE);
        }
        return user;
    }
    async assignLoginOtp(user, type) {
        const { token, expiresAt } = await this.otp();
        if (type === constants_1.PasswordlessLoginValidateWhatSources.EMAIL) {
            user.emailVerificationTokenOnLogin = token;
            user.emailVerificationTokenOnLoginExpiresAt = expiresAt;
            await this.userRepository.update(user.id, {
                emailVerificationTokenOnLogin: token,
                emailVerificationTokenOnLoginExpiresAt: expiresAt,
            });
        }
        else {
            user.mobileVerificationTokenOnLogin = token;
            user.mobileVerificationTokenOnLoginExpiresAt = expiresAt;
            await this.userRepository.update(user.id, {
                mobileVerificationTokenOnLogin: token,
                mobileVerificationTokenOnLoginExpiresAt: expiresAt,
            });
        }
    }
    buildLoginOtpResponse(user, type) {
        const maskedIdentifier = type === constants_1.PasswordlessLoginValidateWhatSources.EMAIL
            ? { email: this.maskEmail(user.email) }
            : { mobile: this.maskMobile(user.mobile) };
        return {
            message: success_messages_1.SUCCESS_MESSAGES.OTP_SENT_SUCCESS_LOGIN,
            user: maskedIdentifier,
        };
    }
    async notifyUserOnOtpInititateLogin(user, loginType) {
        const companyLogo = await this.getCompanyLogo();
        const dummyOtp = this.getDummyOtpForUser(user);
        if (dummyOtp)
            return;
        if (loginType === constants_1.PasswordlessLoginValidateWhatSources.EMAIL) {
            const mailService = this.mailServiceFactory.getMailService();
            mailService.sendEmailUsingTemplate(user.email, "otp-on-login", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                solidAppWebsiteUrl: this.settingService.getConfigValue("solidAppWebsiteUrl"),
                firstName: user.username,
                emailVerificationTokenOnLogin: user.emailVerificationTokenOnLogin,
                fullName: user.fullName ? user.fullName : user.username,
                companyLogoUrl: companyLogo,
            }, this.settingService.getConfigValue("shouldQueueEmails"), null, null, "user", user.id);
        }
        if (loginType === constants_1.PasswordlessLoginValidateWhatSources.MOBILE) {
            const templateParams = {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                otp: user.mobileVerificationTokenOnLogin,
                mobileVerificationTokenOnLogin: user.mobileVerificationTokenOnLogin,
                firstName: user.username,
                fullName: user.fullName ? user.fullName : user.username,
                companyLogoUrl: companyLogo,
            };
            const whatsappDestination = this.normalizeWhatsAppDestination(user.mobile);
            const gupshupTemplateAppName = process.env.COMMON_GUPSHUP_APP_NAME || "Gupshup";
            const whatsappTemplateId = this.settingService.getConfigValue("otpWhatsappTemplateId") || "common_otp";
            const whatsappIndependentEnabled = this.settingService.getConfigValue("otpWhatsappIndependentEnabled") !== false;
            let smsSent = false;
            let whatsappSent = false;
            let smsErrorMessage;
            let whatsappErrorMessage;
            this.logger.debug(`OTP LOGIN SMS send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}`);
            try {
                const smsService = this.smsFactory.getSmsService();
                await smsService.sendSMSUsingTemplate(user.mobile, "otp-on-login", templateParams, false);
                smsSent = true;
            }
            catch (smsError) {
                smsErrorMessage = smsError?.message;
                this.logger.warn(`OTP LOGIN SMS failed: destination=${user.mobile}, message=${smsErrorMessage}`);
            }
            if (whatsappIndependentEnabled) {
                if (!whatsappDestination) {
                    whatsappErrorMessage = "Normalized WhatsApp destination is empty";
                    this.logger.error(`OTP LOGIN WhatsApp skipped: destination=${user.mobile}, message=${whatsappErrorMessage}`);
                }
                else {
                    this.logger.debug(`OTP LOGIN WhatsApp send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`);
                    try {
                        await this.sendOtpToWhatsappProvider(whatsappDestination, String(whatsappTemplateId), String(gupshupTemplateAppName), String(templateParams.otp || ""));
                        whatsappSent = true;
                        this.logger.log(`OTP LOGIN WhatsApp success: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`);
                    }
                    catch (waError) {
                        whatsappErrorMessage = waError?.message;
                        this.logger.error(`OTP LOGIN WhatsApp failed: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}, message=${whatsappErrorMessage}`);
                    }
                }
            }
            if (!smsSent && !whatsappSent) {
                throw new Error(`OTP LOGIN delivery failed on both channels. smsError=${smsErrorMessage || "unknown"}, whatsappError=${whatsappErrorMessage || "disabled/unknown"}`);
            }
        }
    }
    async otpConfirmLogin(confirmSignInDto) {
        const isPasswordlessRegistrationEnabled = await this.isPasswordlessRegistrationEnabled();
        if (!isPasswordlessRegistrationEnabled) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.PASSWORDLESS_REGISTRATION_DISABLED);
        }
        const { type, identifier, otp } = confirmSignInDto;
        if (type !== constants_1.PasswordlessLoginValidateWhatSources.EMAIL &&
            type !== constants_1.PasswordlessLoginValidateWhatSources.MOBILE) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.INVALID_VERIFICATION_TYPE);
        }
        const user = await this.findUserForLogin(type, identifier, {
            withRoles: true,
        });
        this.checkAccountBlocked(user);
        const dummyOtp = this.getDummyOtpForUser(user);
        if (dummyOtp) {
            if (otp !== dummyOtp) {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_OTP);
            }
            return this.buildLoginTokenResponse(user);
        }
        try {
            this.validateLoginOtp(user, otp, type);
        }
        catch (e) {
            await this.incrementFailedAttempts(user);
            throw e;
        }
        await this.clearLoginOtp(user, type);
        await this.userActivityHistoryService.logEvent("login", user);
        await this.resetFailedAttempts(user);
        return this.buildLoginTokenResponse(user);
    }
    validateLoginOtp(user, otp, type) {
        const isEmail = type === constants_1.PasswordlessLoginValidateWhatSources.EMAIL;
        const token = isEmail
            ? user.emailVerificationTokenOnLogin
            : user.mobileVerificationTokenOnLogin;
        const expiresAt = isEmail
            ? user.emailVerificationTokenOnLoginExpiresAt
            : user.mobileVerificationTokenOnLoginExpiresAt;
        if (token !== otp) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_OTP);
        }
        if (expiresAt < new Date()) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.OTP_EXPIRED);
        }
    }
    async clearLoginOtp(user, type) {
        if (type === constants_1.PasswordlessLoginValidateWhatSources.EMAIL) {
            const verifiedAt = new Date();
            user.emailVerifiedOnLoginAt = verifiedAt;
            user.emailVerificationTokenOnLogin = null;
            user.emailVerificationTokenOnLoginExpiresAt = null;
            await this.userRepository.update(user.id, {
                emailVerifiedOnLoginAt: verifiedAt,
                emailVerificationTokenOnLogin: null,
                emailVerificationTokenOnLoginExpiresAt: null,
            });
        }
        else {
            const verifiedAt = new Date();
            user.mobileVerifiedOnLoginAt = verifiedAt;
            user.mobileVerificationTokenOnLogin = null;
            user.mobileVerificationTokenOnLoginExpiresAt = null;
            await this.userRepository.update(user.id, {
                mobileVerifiedOnLoginAt: verifiedAt,
                mobileVerificationTokenOnLogin: null,
                mobileVerificationTokenOnLoginExpiresAt: null,
            });
        }
    }
    buildUserPayload(user) {
        const { id, username, email, mobile, lastLoginProvider } = user;
        const roles = user.roles.map((role) => role.name);
        return { id, username, email, mobile, lastLoginProvider, roles };
    }
    async buildLoginTokenResponse(user) {
        const { accessToken, refreshToken } = await this.generateTokens(user);
        return { accessToken, refreshToken, user: this.buildUserPayload(user) };
    }
    async changePassword(changePasswordDto, activeUser) {
        const user = await this.userRepository.findOne({
            where: { id: changePasswordDto.id },
        });
        if (!user) {
            throw new common_1.NotFoundException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        if (!user.active) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_INACTIVE);
        }
        if (user.lastLoginProvider !== "local") {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.NON_LOCAL_PROVIDER);
        }
        if (!(user.id === activeUser.sub)) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.USER_ID_MISMATCH);
        }
        if (!(user.username === activeUser.username)) {
            throw new common_1.BadRequestException(error_messages_1.ERROR_MESSAGES.USERNAME_MISMATCH);
        }
        const isEqual = await this.hashingService.compare(changePasswordDto.currentPassword, user.password, user.passwordSchemeVersion);
        if (!isEqual) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INCORRECT_CURRENT_PASSWORD);
        }
        const pwdData = await this.userService.hashPassword(changePasswordDto.newPassword);
        user.password = changePasswordDto.newPassword;
        user.password = pwdData.password;
        user.passwordScheme = pwdData.passwordScheme;
        user.passwordSchemeVersion = pwdData.passwordSchemeVersion;
        user.forcePasswordChange = false;
        await this.userRepository.save(user);
        return true;
    }
    async generateForgotPasswordToken(user) {
        const expiryTime = new Date();
        const forgotPasswordVerificationTokenExpiry = this.settingService.getConfigValue("forgotPasswordVerificationTokenExpiry");
        const dummyOtp = this.getDummyOtpForUser(user);
        expiryTime.setMinutes(expiryTime.getMinutes() + forgotPasswordVerificationTokenExpiry);
        return {
            token: dummyOtp ? dummyOtp : (0, uuid_1.v4)(),
            expiresAt: expiryTime,
        };
    }
    async initiateForgotPassword(initiateForgotPasswordDto) {
        const user = await this.resolveUser(initiateForgotPasswordDto.username, initiateForgotPasswordDto.email);
        let isValidUser = true;
        if (!user) {
            isValidUser = false;
        }
        if (isValidUser && !user?.active) {
            isValidUser = false;
        }
        if (isValidUser && user?.lastLoginProvider !== "local") {
            isValidUser = false;
        }
        if (isValidUser) {
            const { token, expiresAt } = await this.generateForgotPasswordToken(user);
            user.verificationTokenOnForgotPassword = token;
            user.verificationTokenOnForgotPasswordExpiresAt = expiresAt;
            await this.userRepository.save(user);
            await this.notifyUserOnForgotPassword(user);
        }
        return {
            status: "success",
            message: success_messages_1.SUCCESS_MESSAGES.FORGOT_PASSWORD_TOKEN_SENT,
            error: "",
            errorCode: "",
            data: {
                user: {
                    email: user?.email,
                },
            },
        };
    }
    async notifyUserOnForgotPassword(user) {
        const companyLogo = await this.getCompanyLogo();
        const forgotPasswordSendVerificationTokenOn = this.settingService.getConfigValue("forgotPasswordSendVerificationTokenOn");
        if (forgotPasswordSendVerificationTokenOn ==
            constants_1.ForgotPasswordSendVerificationTokenOn.EMAIL) {
            const mailService = this.mailServiceFactory.getMailService();
            mailService.sendEmailUsingTemplate(user.email, "forgot-password", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                solidAppWebsiteUrl: this.settingService.getConfigValue("solidAppWebsiteUrl"),
                firstName: user.username,
                fullName: user.fullName,
                passwordResetLink: `${this.settingService.getConfigValue("frontendForgotPasswordPageUrl")}?token=${user.verificationTokenOnForgotPassword}`,
                companyLogoUrl: companyLogo,
            }, this.settingService.getConfigValue("shouldQueueEmails"), null, null, "user", user.id);
        }
        if (forgotPasswordSendVerificationTokenOn ==
            constants_1.ForgotPasswordSendVerificationTokenOn.MOBILE &&
            user.mobile) {
            const smsService = this.smsFactory.getSmsService();
            smsService.sendSMSUsingTemplate(user.mobile, "forgot-password", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                otp: user.verificationTokenOnForgotPassword,
                verificationTokenOnForgotPassword: user.verificationTokenOnForgotPassword,
                firstName: user.username,
                companyLogoUrl: companyLogo,
            }, this.settingService.getConfigValue("shouldQueueSms"));
        }
    }
    async confirmForgotPassword(confirmForgotPasswordDto) {
        return this.dataSource.transaction(async (m) => {
            const user = await this.resolveUserByVerificationToken(confirmForgotPasswordDto.verificationToken);
            if (!user)
                throw new common_1.UnauthorizedException("Invalid verification token");
            if (user.lastLoginProvider !== "local")
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_CREDENTIALS);
            if (!user.active)
                throw new common_1.UnauthorizedException("User is inactive");
            const { affected } = await m
                .createQueryBuilder()
                .update(user_entity_1.User)
                .set({
                forgotPasswordConfirmedAt: () => "NOW()",
                verificationTokenOnForgotPassword: () => "NULL",
                verificationTokenOnForgotPasswordExpiresAt: () => "NULL",
            })
                .where("id = :id", { id: user.id })
                .andWhere("verificationTokenOnForgotPassword = :token", {
                token: confirmForgotPasswordDto.verificationToken,
            })
                .andWhere("verificationTokenOnForgotPasswordExpiresAt > NOW()")
                .execute();
            if (affected !== 1) {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_CREDENTIALS);
            }
            const pwdHash = await this.hashingService.hash(confirmForgotPasswordDto.password);
            const pwdScheme = this.hashingService.name();
            const pwdSchemeVersion = this.hashingService.currentVersion();
            await m.getRepository(user_entity_1.User).update({ id: user.id }, {
                password: pwdHash,
                passwordScheme: pwdScheme,
                passwordSchemeVersion: pwdSchemeVersion,
            });
            await this.notifyUserOnPasswordChanged(user);
            return {
                status: "success",
                message: success_messages_1.SUCCESS_MESSAGES.FORGOT_PASSWORD_CONFIRMED,
                error: "",
                errorCode: "",
                data: {},
            };
        });
    }
    async notifyUserOnPasswordChanged(user) {
        const companyLogo = await this.getCompanyLogo();
        const forgotPasswordSendVerificationTokenOn = this.settingService.getConfigValue("forgotPasswordSendVerificationTokenOn");
        if (forgotPasswordSendVerificationTokenOn ==
            constants_1.ForgotPasswordSendVerificationTokenOn.EMAIL) {
            const mailService = this.mailServiceFactory.getMailService();
            mailService.sendEmailUsingTemplate(user.email, "password-changed", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                solidAppWebsiteUrl: this.settingService.getConfigValue("solidAppWebsiteUrl"),
                email: user.email,
                firstName: user.username,
                fullName: user.fullName,
                passwordResetLink: `${this.settingService.getConfigValue("frontendForgotPasswordPageUrl")}?token=${user.verificationTokenOnForgotPassword}`,
                companyLogoUrl: companyLogo,
            }, this.settingService.getConfigValue("shouldQueueEmails"), null, null, "user", user.id);
        }
        if (forgotPasswordSendVerificationTokenOn ==
            constants_1.ForgotPasswordSendVerificationTokenOn.MOBILE &&
            user.mobile) {
            const smsService = this.smsFactory.getSmsService();
            smsService.sendSMSUsingTemplate(user.mobile, "forgot-password", {
                solidAppName: this.settingService.getConfigValue("appTitle"),
                otp: user.verificationTokenOnForgotPassword,
                verificationTokenOnForgotPassword: user.verificationTokenOnForgotPassword,
                firstName: user.username,
                companyLogoUrl: companyLogo,
            }, this.settingService.getConfigValue("shouldQueueSms"));
        }
    }
    async generateTokens(user) {
        const [accessToken, refreshToken] = await Promise.all([
            await this.generateAccessToken(user),
            await this.generateRefreshToken(user),
        ]);
        return {
            accessToken,
            refreshToken,
        };
    }
    async generateAccessToken(user) {
        const userRoleNames = user.roles.map((role) => role.name);
        const accessTokenTtl = this.settingService.getConfigValue("accessTokenTtl");
        const accessToken = await this.signToken(user.id, accessTokenTtl, { username: user.username, email: user.email, roles: userRoleNames });
        return accessToken;
    }
    async generateRefreshToken(user, previousRefreshToken) {
        const refreshTokenId = (0, crypto_1.randomUUID)();
        const refreshTokenTtl = this.settingService.getConfigValue("refreshTokenTtl");
        const refreshToken = await this.signToken(user.id, refreshTokenTtl, {
            refreshTokenId,
        });
        await this.refreshTokenIdsStorage.insert(user.id, refreshToken, previousRefreshToken);
        return refreshToken;
    }
    async refreshTokens(refreshTokenDto) {
        try {
            const secret = this.settingService.getConfigValue("secret");
            const audience = this.settingService.getConfigValue("audience");
            const issuer = this.settingService.getConfigValue("issuer");
            const { sub } = await this.jwtService.verifyAsync(refreshTokenDto.refreshToken, {
                secret,
                audience,
                issuer,
            });
            const user = await this.userRepository.findOne({
                where: {
                    id: sub,
                },
                relations: {
                    roles: true,
                },
            });
            if (!user) {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.SESSION_INVALID);
            }
            const currentRefreshToken = await this.refreshTokenIdsStorage.validateAndRotate(user, refreshTokenDto.refreshToken);
            await this.userActivityHistoryService.logEvent("tokenRefreshed", user);
            return {
                accessToken: await this.generateAccessToken(user),
                refreshToken: currentRefreshToken,
            };
        }
        catch (err) {
            if (err instanceof refresh_token_ids_storage_service_1.InvalidatedRefreshTokenError) {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.ACCESS_DENIED);
            }
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.SESSION_EXPIRED);
        }
    }
    async signToken(userId, expiresIn, payload) {
        const audience = this.settingService.getConfigValue("audience");
        const issuer = this.settingService.getConfigValue("issuer");
        const secret = this.settingService.getConfigValue("secret");
        return await this.jwtService.signAsync({
            sub: userId,
            ...payload,
        }, {
            audience,
            issuer,
            secret,
            expiresIn,
        });
    }
    async validateUserUsingGoogle(user) {
        try {
            const response = await this.httpService.axiosRef.get(`https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${user.googleAccessToken}`);
            const userProfile = response.data;
            if (userProfile.email === user.email &&
                userProfile.id === user.googleId) {
                return userProfile;
            }
            else {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_USER_PROFILE);
            }
        }
        catch (error) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.GOOGLE_OAUTH_PROFILE_FETCH_FAILED);
        }
    }
    async signInUsingGoogle(accessCode) {
        const user = await this.userRepository.findOne({
            where: {
                accessCode: accessCode,
            },
            relations: {
                roles: true,
            },
        });
        if (!user) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        this.checkAccountBlocked(user);
        try {
            await this.validateUserUsingGoogle(user);
        }
        catch (e) {
            await this.incrementFailedAttempts(user);
            throw e;
        }
        await this.resetFailedAttempts(user);
        const tokens = await this.generateTokens(user);
        return {
            user: {
                email: user.email,
                mobile: user.mobile,
                username: user.username,
                id: user.id,
                roles: user.roles.map((role) => role.name),
            },
            ...tokens,
        };
    }
    async validateUserUsingFacebook(user) {
        if (!user.facebookAccessToken || !user.facebookId) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        try {
            const response = await this.httpService.axiosRef.get(`https://graph.facebook.com/me`, {
                params: { fields: "id,name,email" },
                headers: {
                    Authorization: `Bearer ${user.facebookAccessToken}`,
                },
            });
            const userProfile = response.data;
            if (userProfile.id === user.facebookId &&
                (!user.email || !userProfile.email || userProfile.email === user.email)) {
                return userProfile;
            }
            else {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_USER_PROFILE);
            }
        }
        catch (error) {
            if (error instanceof common_1.UnauthorizedException) {
                throw error;
            }
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
    }
    async signInUsingFacebook(accessCode) {
        const user = await this.userRepository.findOne({
            where: {
                accessCode: accessCode,
            },
            relations: {
                roles: true,
            },
        });
        if (!user) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        this.checkAccountBlocked(user);
        try {
            await this.validateUserUsingFacebook(user);
        }
        catch (e) {
            await this.incrementFailedAttempts(user);
            throw e;
        }
        await this.resetFailedAttempts(user);
        const tokens = await this.generateTokens(user);
        return {
            user: {
                email: user.email,
                mobile: user.mobile,
                username: user.username,
                id: user.id,
                roles: user.roles.map((role) => role.name),
            },
            ...tokens,
        };
    }
    async validateUserUsingMicrosoft(user) {
        try {
            const response = await this.httpService.axiosRef.get(`https://graph.microsoft.com/v1.0/me`, {
                headers: {
                    Authorization: `Bearer ${user.microsoftAccessToken}`,
                },
            });
            const userProfile = response.data;
            const profileEmail = userProfile.mail || userProfile.userPrincipalName;
            if (userProfile.id === user.microsoftId &&
                (!user.email || profileEmail === user.email)) {
                return userProfile;
            }
            else {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_USER_PROFILE);
            }
        }
        catch (error) {
            throw new common_1.UnauthorizedException("Microsoft OAuth profile fetch failed");
        }
    }
    async signInUsingMicrosoft(accessCode) {
        const user = await this.userRepository.findOne({
            where: {
                accessCode: accessCode,
            },
            relations: {
                roles: true,
            },
        });
        if (!user) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        this.checkAccountBlocked(user);
        try {
            await this.validateUserUsingMicrosoft(user);
        }
        catch (e) {
            await this.incrementFailedAttempts(user);
            throw e;
        }
        await this.resetFailedAttempts(user);
        const tokens = await this.generateTokens(user);
        return {
            user: {
                email: user.email,
                mobile: user.mobile,
                username: user.username,
                id: user.id,
                roles: user.roles.map((role) => role.name),
            },
            ...tokens,
        };
    }
    async signInUsingApple(accessCode) {
        const user = await this.userRepository.findOne({
            where: {
                accessCode: accessCode,
            },
            relations: {
                roles: true,
            },
        });
        if (!user) {
            throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        this.checkAccountBlocked(user);
        await this.resetFailedAttempts(user);
        const tokens = await this.generateTokens(user);
        return {
            user: {
                email: user.email,
                mobile: user.mobile,
                username: user.username,
                id: user.id,
                roles: user.roles.map((role) => role.name),
            },
            ...tokens,
        };
    }
    async isPasswordlessRegistrationEnabled() {
        return this.settingService.getConfigValue("passwordLessAuth");
    }
    checkAccountBlocked(user) {
        const maxFailedAttempts = this.settingService.getConfigValue("maxFailedLoginAttempts");
        if (maxFailedAttempts > 0 &&
            user.failedLoginAttempts >= maxFailedAttempts) {
            throw new common_1.ForbiddenException(error_messages_1.ERROR_MESSAGES.ACCOUNT_BLOCKED);
        }
    }
    async incrementFailedAttempts(user) {
        const nextFailedAttempts = (user.failedLoginAttempts ?? 0) + 1;
        user.failedLoginAttempts = nextFailedAttempts;
        await this.userRepository.update(user.id, {
            failedLoginAttempts: nextFailedAttempts,
        });
    }
    async resetFailedAttempts(user) {
        if (user.failedLoginAttempts === 0)
            return;
        user.failedLoginAttempts = 0;
        await this.userRepository.update(user.id, { failedLoginAttempts: 0 });
    }
    async logout(refreshToken) {
        try {
            const payload = this.jwtService.decode(refreshToken);
            if (!payload || !payload.sub) {
                throw new common_1.UnauthorizedException(error_messages_1.ERROR_MESSAGES.INVALID_REFRESH_TOKEN);
            }
            const userId = payload.sub;
            await this.refreshTokenIdsStorage.invalidate(userId);
            const user = await this.userRepository.findOne({
                where: {
                    id: userId,
                },
            });
            await this.userActivityHistoryService.logEvent("logout", user);
            return { message: success_messages_1.SUCCESS_MESSAGES.LOGOUT_SUCCESS };
        }
        catch (err) {
            throw err instanceof common_1.UnauthorizedException ||
                err instanceof common_1.InternalServerErrorException
                ? err
                : new common_1.InternalServerErrorException(error_messages_1.ERROR_MESSAGES.LOGOUT_FAILED);
        }
    }
    async activateUser(userId) {
        const user = await this.userService.findOne(userId, {});
        if (!user) {
            throw new common_1.NotFoundException(error_messages_1.ERROR_MESSAGES.USER_NOT_FOUND);
        }
        user.active = true;
        await this.userRepository.save(user);
    }
    async me(activeUser) {
        const user = await this.userRepository.findOne({
            where: {
                id: activeUser.sub,
            },
            relations: {
                roles: true,
            },
        });
        const refreshTokenState = await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(user.id);
        const response = {
            user: {
                email: user.email,
                mobile: user.mobile,
                username: user.username,
                id: user.id,
                roles: user.roles.map((role) => role.name),
            },
            refreshToken: refreshTokenState.currentRefreshToken,
        };
        return response;
    }
    async generateSsoCode(activeUser, rawAccessToken) {
        const refreshTokenState = await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(activeUser.sub);
        if (!refreshTokenState?.currentRefreshToken) {
            throw new common_1.UnauthorizedException("No active session found");
        }
        const ssoCode = await this.ssoCodeStorage.generateCode(activeUser.sub, rawAccessToken, refreshTokenState.currentRefreshToken);
        return { ssoCode };
    }
    async exchangeSsoCode(code) {
        const { userId, accessToken, refreshToken } = await this.ssoCodeStorage.consumeCode(code);
        const user = await this.userRepository.findOne({
            where: { id: userId },
            relations: { roles: true },
        });
        if (!user) {
            throw new common_1.UnauthorizedException("User not found");
        }
        return { accessToken, refreshToken, user: this.buildUserPayload(user) };
    }
};
exports.AuthenticationService = AuthenticationService;
AuthenticationService.SIGNUP_DTO_KEYS = new Set([
    "username",
    "email",
    "password",
    "fullName",
    "mobile",
    "roles",
    "forcePasswordChange",
    "isAllowedToGenerateApiKeys",
    "failedLoginAttempts",
]);
exports.AuthenticationService = AuthenticationService = AuthenticationService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(14, (0, typeorm_1.InjectDataSource)()),
    __metadata("design:paramtypes", [user_service_1.UserService,
        user_repository_1.UserRepository,
        hashing_service_1.HashingService,
        jwt_1.JwtService,
        refresh_token_ids_storage_service_1.RefreshTokenIdsStorageService,
        axios_1.HttpService,
        mail_factory_1.MailFactory,
        sms_factory_1.SmsFactory,
        whatsapp_factory_1.WhatsAppFactory,
        event_emitter_1.EventEmitter2,
        setting_service_1.SettingService,
        role_metadata_service_1.RoleMetadataService,
        user_activity_history_service_1.UserActivityHistoryService,
        sso_code_storage_service_1.SsoCodeStorageService,
        typeorm_2.DataSource,
        solid_registry_1.SolidRegistry])
], AuthenticationService);
function parseUniqueConstraintError(detail) {
    const match = detail.match(/Key \(([^)]+)\)=\(([^)]+)\) already exists\./);
    if (match) {
        const field = match[1];
        const value = match[2];
        const fieldMap = {
            username: "username",
            email: "email address",
            full_name_user_key: "full name",
        };
        const friendlyField = fieldMap[field] || field;
        return `A user with ${friendlyField} "${value}" already exists.`;
    }
    return detail;
}
//# sourceMappingURL=authentication.service.js.map