"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
function _export(target, all) {
    for(var name in all)Object.defineProperty(target, name, {
        enumerable: true,
        get: Object.getOwnPropertyDescriptor(all, name).get
    });
}
_export(exports, {
    get authMiddleware () {
        return authMiddleware;
    },
    get buildToken () {
        return buildToken;
    },
    get centralServerLogin () {
        return centralServerLogin;
    },
    get comparePassword () {
        return comparePassword;
    },
    get loginHandler () {
        return loginHandler;
    },
    get refreshHandler () {
        return refreshHandler;
    },
    get setFacilityHandler () {
        return setFacilityHandler;
    }
});
const _nodecrypto = /*#__PURE__*/ _interop_require_default(require("node:crypto"));
const _bcrypt = require("bcrypt");
const _config = /*#__PURE__*/ _interop_require_default(require("config"));
const _jose = /*#__PURE__*/ _interop_require_wildcard(require("jose"));
const _ms = /*#__PURE__*/ _interop_require_default(require("ms"));
const _zod = /*#__PURE__*/ _interop_require_wildcard(require("zod"));
const _constants = require("@tamanu/constants");
const _api = require("@opentelemetry/api");
const _errors = require("@tamanu/errors");
const _logging = require("@tamanu/shared/services/logging");
const _rolesToPermissions = require("@tamanu/shared/permissions/rolesToPermissions");
const _timeZoneCheck = require("@tamanu/shared/utils/timeZoneCheck");
const _createSessionIdentifier = require("@tamanu/shared/audit/createSessionIdentifier");
const _selectFacilityIds = require("@tamanu/utils/selectFacilityIds");
const _settings = require("@tamanu/settings");
const _packagejson = require("../../package.json");
const _audit = require("@tamanu/database/utils/audit");
const _sync = require("../sync");
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: 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 { tokenDuration, secret } = _config.default.auth;
const jwtSecretKey = secret ?? _nodecrypto.default.randomBytes(32).toString('hex');
const CENTRAL_LOGIN_INCOMPATIBLE_ERROR_TYPES = new Set([
    _errors.ERROR_TYPE.CLIENT_INCOMPATIBLE,
    _errors.ERROR_TYPE.REMOTE_INCOMPATIBLE
]);
const CENTRAL_LOGIN_NON_FALLBACK_ERROR_TYPES = new Set([
    _errors.ERROR_TYPE.FORBIDDEN,
    _errors.ERROR_TYPE.RATE_LIMITED
]);
function shouldThrowCentralLoginError(error) {
    return error.type?.startsWith(_errors.ERROR_TYPE.AUTH) || CENTRAL_LOGIN_NON_FALLBACK_ERROR_TYPES.has(error.type);
}
function getCentralLoginFallbackReason(error) {
    if (CENTRAL_LOGIN_INCOMPATIBLE_ERROR_TYPES.has(error.type)) {
        return 'central server is incompatible';
    }
    return 'central server login failed';
}
function shouldSkipCentralLoginForTest() {
    return process.env.NODE_ENV === 'test' && !process.env.IS_PLAYWRIGHT_TEST && !process.env.ENABLE_CENTRAL_LOGIN_IN_TEST;
}
async function buildToken({ user, expiresIn = tokenDuration ?? '1h', deviceId = undefined, facilityId = undefined, impersonateRoleId = undefined }) {
    const secretKey = _nodecrypto.default.createSecretKey(new TextEncoder().encode(jwtSecretKey));
    const { canonicalHostName = 'localhost' } = _config.default;
    let expirationTime;
    try {
        if (!expiresIn) {
            throw new Error('No duration provided');
        }
        const timeMs = (0, _ms.default)(expiresIn);
        if (timeMs === undefined || timeMs === null) {
            throw new Error(`ms() returned ${timeMs} for duration: ${expiresIn}`);
        }
        expirationTime = Math.floor((Date.now() + timeMs) / 1000);
    } catch (error) {
        throw new Error(`Invalid time period format: ${expiresIn} (${error.message})`);
    }
    return await new _jose.SignJWT({
        userId: user.id,
        deviceId,
        facilityId,
        impersonateRoleId
    }).setProtectedHeader({
        alg: _constants.JWT_KEY_ALG,
        kid: _constants.JWT_KEY_ID
    }).setJti(_nodecrypto.default.randomBytes(32).toString('base64url')).setIssuer(canonicalHostName).setIssuedAt().setExpirationTime(expirationTime).setAudience(_constants.JWT_TOKEN_TYPES.ACCESS).sign(secretKey);
}
async function comparePassword(user, password) {
    try {
        // do the password comparison even if the user is invalid so
        // that the login check doesn't reveal whether a user exists or not
        return await (0, _bcrypt.compare)(password, user?.password ?? 'invalid-hash');
    } catch (e) {
        return false;
    }
}
async function centralServerLogin({ models, email, password, deviceId, facilityDeviceId, settings }) {
    // try logging in to central server
    const centralServer = new _sync.CentralServerConnection({
        deviceId
    });
    const response = await centralServer.login(email, password, {
        scopes: [],
        body: {
            facilityIds: (0, _selectFacilityIds.selectFacilityIds)(_config.default),
            facilityDeviceId
        },
        backoff: {
            maxAttempts: 1
        }
    });
    // we've logged in as a valid central user - update local database to match
    const { user, localisation, allowedFacilities, primaryTimeZone } = response;
    const { id, ...userDetails } = user;
    const userModel = await models.User.sequelize.transaction(async ()=>{
        const [user] = await models.User.upsert({
            id,
            ...userDetails,
            deletedAt: null
        });
        await models.UserLocalisationCache.upsert({
            userId: id,
            localisation: JSON.stringify(localisation),
            deletedAt: null
        });
        return user;
    });
    await models.Device.ensureRegistration({
        settings,
        user: userModel,
        deviceId,
        scopes: []
    });
    return {
        central: true,
        user,
        localisation,
        allowedFacilities,
        primaryTimeZone
    };
}
async function localLogin({ models, settings, email, password, deviceId }) {
    const { auth: { secret, tokenDuration }, canonicalHostName } = _config.default;
    const primaryTimeZone = (0, _timeZoneCheck.getPrimaryTimeZone)(_config.default);
    const { user } = await models.User.loginFromCredential({
        email,
        password,
        deviceId
    }, {
        log: _logging.log,
        settings,
        tokenDuration,
        tokenIssuer: canonicalHostName,
        tokenSecret: secret
    });
    const allowedFacilities = await user.allowedFacilities();
    const localisation = await models.UserLocalisationCache.getLocalisation({
        where: {
            userId: user.id
        },
        order: [
            [
                'createdAt',
                'DESC'
            ]
        ]
    });
    return {
        central: false,
        user: user.get({
            plain: true
        }),
        allowedFacilities,
        localisation,
        primaryTimeZone
    };
}
async function centralServerLoginWithLocalFallback({ models, settings, email, password, deviceId, facilityDeviceId }) {
    // always log in locally when testing
    if (shouldSkipCentralLoginForTest()) {
        return await localLogin({
            models,
            settings,
            email,
            password,
            deviceId
        });
    }
    try {
        return await centralServerLogin({
            models,
            email,
            password,
            deviceId,
            facilityDeviceId,
            settings
        });
    } catch (e) {
        // if we get an authentication error from central server, throw instead of
        // falling back to local — bad credentials shouldn't silently succeed locally
        if (shouldThrowCentralLoginError(e)) {
            throw e;
        }
        _logging.log.warn(`centralServerLoginWithLocalFallback: ${getCentralLoginFallbackReason(e)}: ${e}`);
        return await localLogin({
            models,
            settings,
            email,
            password,
            deviceId
        });
    }
}
async function loginHandler(req, res, next) {
    const { body, deviceId: facilityDeviceId, models, settings } = req;
    const { deviceId, email, password } = await _zod.object({
        deviceId: _zod.string().min(1),
        email: _zod.email(),
        password: _zod.string().min(1)
    }).parseAsync(body);
    // no permission needed for login
    req.flagPermissionChecked();
    try {
        // For facility servers, settings is a map of facilityId -> ReadSettings
        // For login, we need global settings since there's no facility context yet
        const globalSettings = settings.global ?? (typeof settings.get === 'function' ? settings : new _settings.ReadSettings(models));
        const { central, user, localisation, allowedFacilities, primaryTimeZone } = await centralServerLoginWithLocalFallback({
            models,
            settings: globalSettings,
            email,
            password,
            deviceId,
            facilityDeviceId
        });
        // check if user has access to any facilities on this server
        const serverFacilities = (0, _selectFacilityIds.selectFacilityIds)(_config.default);
        const availableFacilities = await models.User.filterAllowedFacilities(allowedFacilities, serverFacilities);
        if (availableFacilities.length === 0) {
            throw new _errors.AuthPermissionError('User does not have access to any facilities on this server');
        }
        const [permissions, token, role] = await Promise.all([
            (0, _rolesToPermissions.getPermissionsForRoles)(models, user.role),
            buildToken({
                user,
                deviceId
            }),
            models.Role.findByPk(user.role)
        ]);
        res.send({
            token,
            central,
            localisation,
            permissions,
            role: role?.forResponse() ?? null,
            serverType: _constants.SERVER_TYPES.FACILITY,
            primaryTimeZone,
            availableFacilities
        });
    } catch (e) {
        next(e);
    }
}
async function setFacilityHandler(req, res, next) {
    const { user, body, userDevice: device, impersonateRoleId } = req;
    try {
        // Run after auth middleware, requires valid token but no other permission
        req.flagPermissionChecked();
        const userInstance = await req.models.User.findByPk(user.id);
        const { facilityId } = await _zod.object({
            facilityId: _zod.string().min(1)
        }).parseAsync(body);
        const hasAccess = await userInstance.canAccessFacility(facilityId);
        if (!hasAccess) {
            throw new _errors.AuthPermissionError('User does not have access to this facility');
        }
        const token = await buildToken({
            user,
            deviceId: device.id,
            facilityId,
            impersonateRoleId
        });
        const settings = await req.settings[facilityId]?.getFrontEndSettings();
        res.send({
            token,
            settings
        });
    } catch (e) {
        next(e);
    }
}
async function refreshHandler(req, res) {
    const { user, userDevice, facilityId, impersonateRoleId } = req;
    // Run after auth middleware, requires valid token but no other permission
    req.flagPermissionChecked();
    const token = await buildToken({
        user,
        facilityId,
        deviceId: userDevice.id,
        impersonateRoleId
    });
    res.send({
        token
    });
}
const authMiddleware = async (req, res, next)=>{
    const { canonicalHostName = 'localhost' } = _config.default;
    const { models, settings } = req;
    try {
        const { token, user, facility, device, impersonateRoleId } = await models.User.loginFromAuthorizationHeader(req.get('authorization'), {
            log: _logging.log,
            settings: settings.global ?? settings,
            tokenDuration,
            tokenIssuer: canonicalHostName,
            tokenSecret: jwtSecretKey
        });
        if (!device) {
            throw new _errors.MissingCredentialError('Missing deviceId');
        }
        // when we login to a multi-facility server, we don't initially have a facilityId
        if (facility) {
            req.facilityId = facility.id; // eslint-disable-line require-atomic-updates
        }
        const sessionId = (0, _createSessionIdentifier.createSessionIdentifier)(token);
        req.user = user; // eslint-disable-line require-atomic-updates
        req.userDevice = device; // eslint-disable-line require-atomic-updates
        req.sessionId = sessionId; // eslint-disable-line require-atomic-updates
        req.impersonateRoleId = impersonateRoleId; // eslint-disable-line require-atomic-updates
        req.getLocalisation = async ()=>req.models.UserLocalisationCache.getLocalisation({
                where: {
                    userId: req.user.id
                },
                order: [
                    [
                        'createdAt',
                        'DESC'
                    ]
                ]
            });
        const auditSettings = await settings?.[req.facilityId]?.get('audit');
        // Auditing middleware
        // eslint-disable-next-line require-atomic-updates
        req.audit = (0, _audit.initAuditActions)(req, {
            enabled: auditSettings?.accesses.enabled,
            userId: user.id,
            version: _packagejson.version,
            backEndContext: {
                serverType: _constants.SERVER_TYPES.FACILITY
            }
        });
        const spanAttributes = {
            'enduser.id': user.id,
            'enduser.role': user.role,
            'session.deviceId': device.id
        };
        if (facility) {
            spanAttributes['session.facilityId'] = facility.id;
        }
        // eslint-disable-next-line no-unused-expressions
        _api.trace.getActiveSpan()?.setAttributes(spanAttributes);
        _api.context.with(_api.propagation.setBaggage(_api.context.active(), _api.propagation.createBaggage(spanAttributes)), ()=>next());
    } catch (e) {
        next(e);
    }
};

//# sourceMappingURL=auth.js.map