"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "User", {
    enumerable: true,
    get: function() {
        return User;
    }
});
const _nodecrypto = require("node:crypto");
const _bcrypt = require("bcrypt");
const _jose = /*#__PURE__*/ _interop_require_wildcard(require("jose"));
const _lodash = require("lodash");
const _sequelize = require("sequelize");
const _zod = /*#__PURE__*/ _interop_require_wildcard(require("zod"));
const _constants = require("@tamanu/constants");
const _errors = require("@tamanu/errors");
const _rolesToPermissions = require("@tamanu/shared/permissions/rolesToPermissions");
const _middleware = require("@tamanu/shared/permissions/middleware");
const _Model = require("./Model");
const _password = require("@tamanu/utils/password");
function _define_property(obj, key, value) {
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}
function _getRequireWildcardCache(nodeInterop) {
    if (typeof WeakMap !== "function") return null;
    var cacheBabelInterop = new WeakMap();
    var cacheNodeInterop = new WeakMap();
    return (_getRequireWildcardCache = function(nodeInterop) {
        return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
    })(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
    if (!nodeInterop && obj && obj.__esModule) {
        return obj;
    }
    if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
        return {
            default: obj
        };
    }
    var cache = _getRequireWildcardCache(nodeInterop);
    if (cache && cache.has(obj)) {
        return cache.get(obj);
    }
    var newObj = {
        __proto__: null
    };
    var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
    for(var key in obj){
        if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
            var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
            if (desc && (desc.get || desc.set)) {
                Object.defineProperty(newObj, key, desc);
            } else {
                newObj[key] = obj[key];
            }
        }
    }
    newObj.default = obj;
    if (cache) {
        cache.set(obj, newObj);
    }
    return newObj;
}
const DEFAULT_SALT_ROUNDS = 10;
let User = class User extends _Model.Model {
    static hashPassword(pw) {
        return (0, _bcrypt.hash)(pw, User.SALT_ROUNDS ?? DEFAULT_SALT_ROUNDS);
    }
    static isPasswordHashed(password) {
        return (0, _password.isBcryptHash)(password);
    }
    static getSystemUser() {
        return this.findByPk(_constants.SYSTEM_USER_UUID);
    }
    forResponse() {
        const values = Object.assign({}, this.dataValues);
        delete values.password;
        return values;
    }
    async setPassword(pw) {
        this.password = await User.hashPassword(pw);
    }
    static async sanitizeForInsert(values) {
        const { password, ...otherValues } = values;
        if (!password) return values;
        // Only hash if the password is not already hashed (to avoid rehashing when syncing)
        const hashedPassword = User.isPasswordHashed(password) ? password : await User.hashPassword(password);
        return {
            ...otherValues,
            password: hashedPassword
        };
    }
    static async update(values, options) {
        const sanitizedValues = await this.sanitizeForInsert(values);
        return super.update(sanitizedValues, options);
    }
    static async create(values, ...args) {
        const sanitizedValues = await this.sanitizeForInsert(values);
        return super.create(sanitizedValues, ...args);
    }
    static async bulkCreate(records, ...args) {
        const sanitizedRecords = await Promise.all(records.map((r)=>this.sanitizeForInsert(r)));
        return super.bulkCreate(sanitizedRecords, ...args);
    }
    static async upsert(values, ...args) {
        const sanitizedValues = await this.sanitizeForInsert(values);
        return super.upsert(sanitizedValues, ...args);
    }
    static async getForAuthByEmail(email) {
        const user = await this.scope('withPassword').findOne({
            where: {
                // email addresses are case insensitive so compare them as such
                email: _sequelize.Sequelize.where(_sequelize.Sequelize.fn('lower', _sequelize.Sequelize.col('email')), _sequelize.Sequelize.fn('lower', email)),
                visibilityStatus: _constants.VISIBILITY_STATUSES.CURRENT
            }
        });
        if (!user) {
            return null;
        }
        return user;
    }
    static initModel({ primaryKey, ...options }) {
        super.init({
            id: primaryKey,
            displayId: _sequelize.DataTypes.STRING,
            email: {
                type: _sequelize.DataTypes.STRING,
                allowNull: false,
                unique: true
            },
            password: _sequelize.DataTypes.STRING,
            displayName: {
                type: _sequelize.DataTypes.STRING,
                allowNull: false
            },
            role: {
                type: _sequelize.DataTypes.STRING,
                defaultValue: 'practitioner',
                allowNull: false
            },
            phoneNumber: {
                type: _sequelize.DataTypes.STRING
            },
            deviceRegistrationQuota: {
                type: _sequelize.DataTypes.INTEGER,
                allowNull: false,
                defaultValue: 0
            },
            visibilityStatus: {
                type: _sequelize.DataTypes.STRING,
                defaultValue: _constants.VISIBILITY_STATUSES.CURRENT
            }
        }, {
            ...options,
            defaultScope: {
                attributes: {
                    exclude: [
                        'password'
                    ]
                }
            },
            scopes: {
                withPassword: {
                    attributes: {
                        include: [
                            'password'
                        ]
                    }
                }
            },
            indexes: [
                {
                    fields: [
                        'email'
                    ]
                }
            ],
            syncDirection: _constants.SYNC_DIRECTIONS.PULL_FROM_CENTRAL,
            hooks: {
                async beforeUpdate (user) {
                    if (user.changed('password')) {
                        // Only hash if the password is not already hashed (to avoid rehashing when syncing)
                        if (!User.isPasswordHashed(user.password)) {
                            // eslint-disable-next-line require-atomic-updates
                            user.password = await User.hashPassword(user.password);
                        }
                    }
                }
            }
        });
    }
    static initRelations(models) {
        this.hasMany(models.Discharge, {
            foreignKey: 'dischargerId',
            as: 'discharges'
        });
        this.hasMany(models.ImagingRequest, {
            foreignKey: 'completedById'
        });
        this.hasMany(models.PatientProgramRegistration, {
            foreignKey: 'clinicianId'
        });
        this.hasMany(models.PatientProgramRegistrationCondition, {
            foreignKey: 'clinicianId'
        });
        this.hasMany(models.PatientProgramRegistrationCondition, {
            foreignKey: 'deletionClinicianId'
        });
        this.hasMany(models.UserPreference, {
            foreignKey: 'userId'
        });
        this.belongsToMany(models.Facility, {
            through: 'UserFacility',
            as: 'facilities',
            where: {
                deletedAt: null
            }
        });
        this.hasMany(models.UserDesignation, {
            foreignKey: 'userId',
            as: 'designations'
        });
        this.belongsToMany(models.ReferenceData, {
            through: models.UserDesignation,
            foreignKey: 'userId',
            as: 'designationData'
        });
        this.hasMany(models.UserLeave, {
            foreignKey: 'userId',
            as: 'leaves'
        });
    }
    static buildSyncFilter() {
        return null; // syncs everywhere
    }
    static getFullReferenceAssociations() {
        const { models } = this.sequelize;
        return [
            {
                model: models.UserDesignation,
                as: 'designations',
                include: {
                    model: models.ReferenceData,
                    as: 'referenceData'
                }
            },
            {
                model: models.Facility,
                as: 'facilities',
                attributes: [
                    'id'
                ]
            }
        ];
    }
    static async buildSyncLookupQueryDetails() {
        return null; // syncs everywhere
    }
    isSuperUser() {
        return this.role === 'admin' || this.id === _constants.SYSTEM_USER_UUID;
    }
    async checkPermission(action, subject, field = '') {
        const { Permission } = this.sequelize.models;
        const ability = await (0, _rolesToPermissions.getAbilityForUser)({
            Permission
        }, this);
        const subjectName = (0, _middleware.getSubjectName)(subject);
        const hasPermission = ability.can(action, subject, field);
        if (!hasPermission) {
            const rule = ability.relevantRuleFor(action, subject, field);
            const reason = rule && rule.reason || `Cannot perform action "${action}" on ${subjectName}.`;
            throw new _errors.ForbiddenError(reason);
        }
    }
    async hasPermission(action, subject, field = '') {
        try {
            await this.checkPermission(action, subject, field);
            return true;
        } catch (e) {
            return false;
        }
    }
    async canSync(facilityIds, { settings }) {
        const restrictUsersToSync = await settings.get('auth.restrictUsersToSync');
        if (!restrictUsersToSync) return true;
        if (this.isSuperUser()) return true;
        // Permission to sync any facility
        if (await this.hasPermission('sync', 'Facility')) return true;
        // Permission to sync specific facilities
        for (const facilityId of facilityIds){
            if (await this.hasPermission('sync', 'Facility', facilityId)) return true;
        }
        return false;
    }
    async allowedFacilities() {
        const { Facility, Setting } = this.sequelize.models;
        if (this.isSuperUser()) return _constants.CAN_ACCESS_ALL_FACILITIES;
        const restrictUsersToFacilities = await Setting.get('auth.restrictUsersToFacilities');
        const hasLoginPermission = await this.hasPermission('login', 'Facility');
        const hasAllNonSensitiveFacilityAccess = !restrictUsersToFacilities || hasLoginPermission;
        const sensitiveFacilities = await Facility.count({
            where: {
                isSensitive: true
            }
        });
        if (hasAllNonSensitiveFacilityAccess && sensitiveFacilities === 0) return _constants.CAN_ACCESS_ALL_FACILITIES;
        // Get user's linked facilities
        if (!this.facilities) {
            await this.reload({
                include: 'facilities'
            });
        }
        const explicitlyAllowedFacilities = this.facilities?.map(({ id, name })=>({
                id,
                name
            })) ?? [];
        if (hasAllNonSensitiveFacilityAccess) {
            // Combine any explicitly linked facilities with all non-sensitive facilities
            const nonSensitiveFacilities = await Facility.findAll({
                where: {
                    isSensitive: false
                },
                attributes: [
                    'id',
                    'name'
                ],
                raw: true
            });
            const combinedFacilities = (0, _lodash.unionBy)(explicitlyAllowedFacilities, nonSensitiveFacilities, 'id');
            return combinedFacilities;
        }
        // Otherwise return only the facilities the user is linked to (including sensitive ones)
        return explicitlyAllowedFacilities;
    }
    async allowedFacilityIds() {
        const allowedFacilities = await this.allowedFacilities();
        if (allowedFacilities === _constants.CAN_ACCESS_ALL_FACILITIES) {
            return _constants.CAN_ACCESS_ALL_FACILITIES;
        }
        return allowedFacilities.map((f)=>f.id);
    }
    async canAccessFacility(id) {
        const allowedFacilityIds = await this.allowedFacilityIds();
        if (allowedFacilityIds === _constants.CAN_ACCESS_ALL_FACILITIES) return true;
        return allowedFacilityIds.includes(id);
    }
    static async filterAllowedFacilities(allowedFacilities, facilityIds) {
        if (Array.isArray(allowedFacilities)) {
            return allowedFacilities.filter((f)=>facilityIds.includes(f.id));
        } else {
            if (allowedFacilities === _constants.CAN_ACCESS_ALL_FACILITIES) {
                const facilitiesMatchingIds = await this.sequelize.models.Facility.findAll({
                    where: {
                        id: facilityIds
                    }
                });
                return facilitiesMatchingIds?.map(({ id, name })=>({
                        id,
                        name
                    })) ?? [];
            }
        }
        return [];
    }
    static async loginFromCredential(payload, { log, settings, tokenSecret, tokenIssuer, tokenDuration }) {
        const { Device, UserLoginAttempt } = this.sequelize.models;
        const { email, password, facilityIds, deviceId, scopes = [], clientHeader } = await this.LoginPayload.parseAsync(payload).catch((error)=>{
            throw new _errors.MissingCredentialError().withCause(error);
        });
        const internalClient = Boolean(clientHeader && Object.values(_constants.SERVER_TYPES).includes(clientHeader));
        if (internalClient && !deviceId) {
            throw new _errors.MissingCredentialError('Missing deviceId');
        }
        const user = await this.getForAuthByEmail(email);
        if (!user && await settings.get('security.reportNoUserError')) {
            // an attacker can use this to get a list of user accounts
            // but hiding this error entirely can make debugging a hassle
            // so we just put it behind a flag
            throw new _errors.InvalidCredentialError('No such user');
        }
        if (!user) {
            // Keep track of bad requests for non-existent user accounts
            log.info(`Trying to login with non-existent user account: ${email}`);
            // To mitigate timing attacks for discovering user accounts,
            // we perform a fake password comparison that takes a similar amount of time
            await (0, _bcrypt.compare)(password, '');
            // and return the same error (ish) data as for a true password mismatch
            throw new _errors.InvalidCredentialError();
        }
        if (user.visibilityStatus !== _constants.VISIBILITY_STATUSES.CURRENT) {
            throw new _errors.AuthPermissionError('User no longer exists');
        }
        // Check if user is locked out
        const { isUserLockedOut, remainingLockout } = await UserLoginAttempt.checkIsUserLockedOut({
            settings,
            userId: user.id,
            deviceId
        });
        if (isUserLockedOut) {
            log.info(`Trying to login with locked user account: ${email}`);
            throw new _errors.RateLimitedError(remainingLockout, _constants.LOCKED_OUT_ERROR_MESSAGE);
        }
        const hashedPassword = user?.password || '';
        if (!await (0, _bcrypt.compare)(password, hashedPassword)) {
            const { lockoutDuration, remainingAttempts } = await UserLoginAttempt.createFailedLoginAttempt({
                settings,
                userId: user.id,
                deviceId
            });
            if (remainingAttempts === 0) {
                throw new _errors.RateLimitedError(lockoutDuration, _constants.LOCKED_OUT_ERROR_MESSAGE);
            }
            if (remainingAttempts <= 3) {
                throw new _errors.InvalidCredentialError().withExtraData({
                    lockoutAttempts: remainingAttempts,
                    lockoutDuration
                });
            }
            throw new _errors.InvalidCredentialError();
        }
        // Manage necessary checks for device authorization (check or create accordingly)
        const device = await Device.ensureRegistration({
            settings,
            user,
            deviceId,
            scopes
        });
        // Create successful login attempt
        await UserLoginAttempt.create({
            userId: user.id,
            deviceId,
            outcome: _constants.LOGIN_ATTEMPT_OUTCOMES.SUCCEEDED
        });
        const secret = (0, _nodecrypto.createSecretKey)(new TextEncoder().encode(tokenSecret));
        const token = await new _jose.SignJWT({
            userId: user.id,
            deviceId: device?.id
        }).setProtectedHeader({
            alg: _constants.JWT_KEY_ALG,
            kid: _constants.JWT_KEY_ID
        }).setJti((0, _nodecrypto.randomBytes)(32).toString('base64url')).setIssuedAt().setIssuer(tokenIssuer).setAudience(_constants.JWT_TOKEN_TYPES.ACCESS).setExpirationTime(tokenDuration).sign(secret);
        const shouldReturnSettings = clientHeader && [
            _constants.SERVER_TYPES.WEBAPP,
            _constants.SERVER_TYPES.FACILITY,
            _constants.SERVER_TYPES.MOBILE
        ].includes(clientHeader) && !facilityIds;
        const frontEndSettings = shouldReturnSettings ? await settings.getFrontEndSettings() : undefined;
        return {
            token,
            user,
            device,
            internalClient,
            settings: frontEndSettings
        };
    }
    static async loginFromToken(token, { tokenSecret, tokenIssuer }) {
        const { Device, Facility } = this.sequelize.models;
        const secret = (0, _nodecrypto.createSecretKey)(new TextEncoder().encode(tokenSecret));
        const contents = await _jose.jwtVerify(token, ({ alg })=>{
            if (alg === 'HS256') {
                return secret;
            }
            throw new _errors.InvalidTokenError('Unsupported algorithm');
        }, {
            issuer: tokenIssuer,
            audience: _constants.JWT_TOKEN_TYPES.ACCESS,
            clockTolerance: 10
        }).catch((error)=>{
            throw new _errors.InvalidTokenError().withCause(error);
        });
        const TokenPayload = _zod.object({
            userId: _zod.string().min(1),
            deviceId: _zod.string().min(1).optional(),
            facilityId: _zod.string().min(1).optional()
        });
        const { userId, deviceId, facilityId } = await TokenPayload.parseAsync(contents.payload).catch((error)=>{
            throw new _errors.InvalidTokenError('Invalid token payload').withCause(error);
        });
        const user = await this.findByPk(userId);
        if (!user) {
            throw new _errors.InvalidTokenError('User does not exist').withExtraData({
                userId
            });
        }
        if (user.visibilityStatus !== _constants.VISIBILITY_STATUSES.CURRENT) {
            throw new _errors.AuthPermissionError('User no longer exists');
        }
        const device = deviceId ? await Device.findByPk(deviceId) ?? undefined : undefined;
        if (deviceId && !device) {
            throw new _errors.InvalidTokenError('Device does not exist').withExtraData({
                deviceId
            });
        }
        const facility = facilityId ? await Facility.findByPk(facilityId) ?? undefined : undefined;
        if (facilityId && !facility) {
            throw new _errors.InvalidTokenError('Facility does not exist').withExtraData({
                facilityId
            });
        }
        // Get the user as a plain object
        const plainUser = user.get({
            plain: true
        });
        // Set the prototype to the User constructor (to perform permission checks)
        Object.setPrototypeOf(plainUser, {
            constructor: {
                name: 'User'
            }
        });
        return {
            token,
            user: plainUser,
            device,
            facility
        };
    }
    static async loginFromAuthorizationHeader(header, context) {
        if (!header) {
            throw new _errors.MissingCredentialError('Missing authorization header');
        }
        const prefix = 'Bearer ';
        if (!header.startsWith(prefix)) {
            throw new _errors.InvalidCredentialError('Only Bearer token is supported');
        }
        const token = header.slice(prefix.length);
        if (token.length === 0) {
            throw new _errors.MissingCredentialError('Missing authorization token');
        }
        return await this.loginFromToken(token, context);
    }
};
_define_property(User, "SALT_ROUNDS", DEFAULT_SALT_ROUNDS);
_define_property(User, "LoginPayload", _zod.object({
    email: _zod.email(),
    password: _zod.string().min(1),
    facilityIds: _zod.array(_zod.string().min(1)).min(1).optional(),
    deviceId: _zod.string().optional(),
    scopes: _zod.array(_zod.nativeEnum(_constants.DEVICE_SCOPES)).optional(),
    clientHeader: _zod.string().min(1).optional()
}));

//# sourceMappingURL=User.js.map