/* * This file belongs to Hoist, an application development toolkit * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) * * Copyright © 2026 Extremely Heavy Industries Inc. */ import {HoistInputModel} from '@xh/hoist/cmp/input'; import {HoistModel, XH} from '@xh/hoist/core'; import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx'; import {throwIf} from '@xh/hoist/utils/js'; import {createRef} from 'react'; /** * @internal */ export class ImpersonationBarModel extends HoistModel { override telemetryPrefix = 'xh.client.identity'; override xhImpl = true; @observable showRequested: boolean = false; @observable.ref targets: string[] = []; @bindable pendingTarget: string = null; // For managed focus of desktop select. inputRef = createRef(); constructor() { super(); makeObservable(this); } init() { this.addAutorun(() => { if (this.isOpen) { this.ensureTargetsLoaded(); // We don't have an impersonation message on mobile, so instead we // display the currently impersonated user in the dropdown. if (XH.isMobileApp && XH.identityService.isImpersonating) { this.pendingTarget = XH.getUsername(); } } }); } get isOpen(): boolean { return this.showRequested || XH.identityService.isImpersonating; } @action show() { throwIf( !XH.identityService.canAuthUserImpersonate, 'User does not have right to impersonate or impersonation is disabled.' ); this.showRequested = true; } @action hide() { this.showRequested = false; } @action toggleVisibility() { if (this.isOpen) { XH.identityService.isImpersonating ? this.inputRef.current?.focus() : this.hide(); } else { this.show(); } } //--------------------- // Handlers //--------------------- readonly onCommit = async () => { const {pendingTarget} = this; if (!pendingTarget) return; try { await XH.identityService.impersonateAsync(pendingTarget); } catch (e) { this.pendingTarget = null; XH.handleException(e, {logOnServer: false}); // likely to be an unknown user } }; readonly onClose = () => { XH.identityService.isImpersonating ? XH.identityService.endImpersonateAsync() : this.hide(); }; //-------------------- // Implementation //-------------------- private ensureTargetsLoaded() { if (this.targets.length) return; this.runner() .span('impersonationTargets') .run(async ctx => { const targets = await XH.fetchJson({url: 'xh/impersonationTargets'}, ctx); this.setTargets(targets); }) .catchDefault(); } @action private setTargets(targets) { this.targets = targets .map(t => t.username) .filter(t => t !== XH.getUsername()) .sort(); } }