@openmrs/esm-api
Version:
The javascript module for interacting with the OpenMRS API
329 lines (328 loc) • 11.8 kB
JavaScript
/** @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);
});
});
}