UNPKG

@openmrs/esm-api

Version:

The javascript module for interacting with the OpenMRS API

329 lines (328 loc) • 11.8 kB
/** @module @category API */ import { reportError } from "@openmrs/esm-error-handling"; import { createGlobalStore } from "@openmrs/esm-state"; import { isUndefined } from "lodash-es"; import { Observable } from "rxjs"; import { openmrsFetch, restBaseUrl, sessionEndpoint } from "./openmrs-fetch.js"; /** @internal */ export const sessionStore = createGlobalStore('session', { loaded: false, session: null }); let lastFetchTimeMillis = 0; function getCurrentUser(opts = { includeAuthStatus: true }) { if (lastFetchTimeMillis < Date.now() - 1000 * 60 || !sessionStore.getState().loaded) { refetchCurrentUser(); } return new Observable((subscriber)=>{ const handler = (state)=>{ if (state.loaded) { if (opts.includeAuthStatus) { subscriber.next(state.session); } else { subscriber.next(state.session?.user); } } }; handler(sessionStore.getState()); // The observable subscribe function should return an unsubscribe function, // which happens to be exactly what `subscribe` returns. return sessionStore.subscribe(handler); }); } export { getCurrentUser }; /** * Returns the global session store containing the current user's session information. * If the session data is stale (older than 1 minute) or not yet loaded, this function * will trigger a refetch of the current user's session. * * @returns The global session store that can be subscribed to for session updates. * * @example * ```ts * import { getSessionStore } from '@openmrs/esm-api'; * const store = getSessionStore(); * const unsubscribe = store.subscribe((state) => { * if (state.loaded) { * console.log('Session:', state.session); * } * }); * ``` */ export function getSessionStore() { if (lastFetchTimeMillis < Date.now() - 1000 * 60 || !sessionStore.getState().loaded) { refetchCurrentUser(); } return sessionStore; } // NB locale is string only if this returns true function isValidLocale(locale) { if (locale === undefined || typeof locale !== 'string') { return false; } try { new Intl.Locale(locale); } catch (e) { return false; } return true; } /** * Sets the document's language attribute based on the user's locale preference * from the session data. This affects the HTML `lang` attribute which is used * for accessibility and internationalization. * * The locale is determined from either the session's locale or the user's * default locale property. Underscores in the locale are converted to hyphens * to match BCP 47 language tag format. * * @param data The session object containing locale information. */ export function setUserLanguage(data) { let locale = data.locale ?? data.user?.userProperties?.defaultLocale; if (locale && locale.includes('_')) { locale = locale.replaceAll('_', '-'); } if (isValidLocale(locale) && locale !== document.documentElement.getAttribute('lang')) { document.documentElement.setAttribute('lang', locale); } } sessionStore.subscribe((state)=>{ if (state.loaded && state.session) { setUserLanguage(state.session); } }); function userHasPrivilege(requiredPrivilege, user) { if (typeof requiredPrivilege === 'string') { return !isUndefined(user.privileges.find((p)=>requiredPrivilege === p.display)); } else if (Array.isArray(requiredPrivilege)) { return requiredPrivilege.every((rp)=>!isUndefined(user.privileges.find((p)=>rp === p.display))); } else if (!isUndefined(requiredPrivilege)) { console.error(`Could not understand privileges "${requiredPrivilege}"`); } return true; } function isSuperUser(user) { return !isUndefined(user.roles.find((role)=>role.display === 'System Developer')); } /** * The `refetchCurrentUser` function causes a network request to redownload * the user. All subscribers to the current user will be notified of the * new users once the new version of the user object is downloaded. * * @returns The same observable as returned by {@link getCurrentUser}. * * @example * ```js * import { refetchCurrentUser } from '@openmrs/esm-api' * refetchCurrentUser() * ``` */ export function refetchCurrentUser(username, password) { lastFetchTimeMillis = Date.now(); let headers = {}; if (username && password) { headers['Authorization'] = `Basic ${window.btoa(`${username}:${password}`)}`; } return handleSessionResponse(openmrsFetch(sessionEndpoint, { headers })); } /** * Clears the current user session from the session store, setting the session * to an unauthenticated state. This is typically called during logout to reset * the application's authentication state. * * @example * ```ts * import { clearCurrentUser } from '@openmrs/esm-api'; * // During logout * clearCurrentUser(); * ``` */ export function clearCurrentUser() { sessionStore.setState({ loaded: true, session: { authenticated: false, sessionId: '' } }); } /** * Checks whether the given user has access based on the required privilege(s). * A user has access if they have the required privilege(s) or if they are a * "System Developer" (super user). If no privilege is required, access is granted. * * @param requiredPrivilege A single privilege string or an array of privilege strings * that the user must have. If an array is provided, the user must have ALL privileges. * @param user The user object containing their privileges and roles. * @returns `true` if the user has access, `false` otherwise. Returns `true` if no * privilege is required, and `false` if the user is undefined but a privilege is required. * * @example * ```ts * import { userHasAccess } from '@openmrs/esm-api'; * const hasAccess = userHasAccess('View Patients', currentUser); * const hasMultipleAccess = userHasAccess(['View Patients', 'Edit Patients'], currentUser); * ``` */ export function userHasAccess(requiredPrivilege, user) { if (user === undefined) { // if the user hasn't been loaded, then return false iff there is a required privilege return !Boolean(requiredPrivilege); } if (!Boolean(requiredPrivilege)) { // if user exists but no requiredPrivilege is defined return true; } return userHasPrivilege(requiredPrivilege, user) || isSuperUser(user); } /** * Returns a Promise that resolves with the currently logged-in user object. * If the user is already loaded in the session store, the Promise resolves immediately. * Otherwise, it subscribes to the session store and resolves when a logged-in user * becomes available. * * @returns A Promise that resolves with the LoggedInUser object once available. * * @example * ```ts * import { getLoggedInUser } from '@openmrs/esm-api'; * const user = await getLoggedInUser(); * console.log('Logged in as:', user.display); * ``` */ export function getLoggedInUser() { let user; let unsubscribe; return new Promise((res)=>{ const handler = (state)=>{ if (state.loaded && state.session.user) { user = state.session.user; res(state.session.user); if (unsubscribe) { unsubscribe(); } } }; handler(sessionStore.getState()); if (!user) { unsubscribe = sessionStore.subscribe(handler); } }); } /** * Returns a Promise that resolves with the current session location, if one is set. * The session location represents the physical location where the user is currently * working (e.g., a clinic or ward). * * @returns A Promise that resolves with the SessionLocation object, or `undefined` * if no session location is set. * * @example * ```ts * import { getSessionLocation } from '@openmrs/esm-api'; * const location = await getSessionLocation(); * if (location) { * console.log('Current location:', location.display); * } * ``` */ export function getSessionLocation() { return new Promise((res, rej)=>{ const sub = getCurrentUser().subscribe((session)=>{ res(session.sessionLocation); }, rej); sub.unsubscribe(); }); } /** * Sets the session location for the current user. The session location represents * the physical location where the user is working (e.g., a clinic or ward). * This triggers a server request to update the session and refreshes the local * session store. * * @param locationUuid The UUID of the location to set as the session location. * @param abortController An AbortController to allow cancellation of the request. * @returns A Promise that resolves with the updated SessionStore. * * @example * ```ts * import { setSessionLocation } from '@openmrs/esm-api'; * const abortController = new AbortController(); * await setSessionLocation('location-uuid-here', abortController); * ``` */ export async function setSessionLocation(locationUuid, abortController) { return handleSessionResponse(openmrsFetch(sessionEndpoint, { method: 'POST', body: { sessionLocation: locationUuid }, headers: { 'Content-Type': 'application/json' }, signal: abortController.signal })); } /** * Updates the user properties for a specific user. User properties are key-value * pairs that store user-specific settings and preferences. After updating the * properties on the server, the current user session is refetched to reflect * the changes. * * @param userUuid The UUID of the user whose properties should be updated. * @param userProperties An object containing the properties to set or update. * @param abortController Optional AbortController to allow cancellation of the request. * If not provided, a new AbortController is created. * @returns A Promise that resolves with the updated SessionStore after refetching * the current user. * * @example * ```ts * import { getLoggedInUser, setUserProperties } from '@openmrs/esm-api'; * const user = await getLoggedInUser(); * await setUserProperties(user.uuid, { * defaultLocale: 'en_GB', * customSetting: 'value' * }); * ``` */ export async function setUserProperties(userUuid, userProperties, abortController) { if (!abortController) { abortController = new AbortController(); } await openmrsFetch(`${restBaseUrl}/user/${userUuid}`, { method: 'POST', body: { userProperties }, headers: { 'Content-Type': 'application/json' }, signal: abortController.signal }); return refetchCurrentUser(); } function handleSessionResponse(result) { return new Promise((resolve, reject)=>{ result.then((res)=>{ let nextState; if (typeof res?.data === 'object') { nextState = { loaded: true, session: res.data }; sessionStore.setState(nextState); resolve(nextState); } else { nextState = { loaded: false, session: null }; sessionStore.setState(nextState); reject(nextState); } }).catch((err)=>{ reportError(`Failed to fetch new session information: ${err}`); const nextState = { loaded: false, session: null }; sessionStore.setState(nextState); reject(nextState); }); }); }