@propelauth/react
Version:
A React library for managing authentication, backed by PropelAuth
1,555 lines (1,500 loc) • 70.2 kB
JavaScript
import React, { useContext, useState, useRef, useEffect, useCallback, useReducer } from 'react';
/*! js-cookie v3.0.5 | MIT */
/* eslint-disable no-var */
function assign (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
target[key] = source[key];
}
}
return target
}
/* eslint-enable no-var */
/* eslint-disable no-var */
var defaultConverter = {
read: function (value) {
if (value[0] === '"') {
value = value.slice(1, -1);
}
return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent)
},
write: function (value) {
return encodeURIComponent(value).replace(
/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,
decodeURIComponent
)
}
};
/* eslint-enable no-var */
/* eslint-disable no-var */
function init (converter, defaultAttributes) {
function set (name, value, attributes) {
if (typeof document === 'undefined') {
return
}
attributes = assign({}, defaultAttributes, attributes);
if (typeof attributes.expires === 'number') {
attributes.expires = new Date(Date.now() + attributes.expires * 864e5);
}
if (attributes.expires) {
attributes.expires = attributes.expires.toUTCString();
}
name = encodeURIComponent(name)
.replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
.replace(/[()]/g, escape);
var stringifiedAttributes = '';
for (var attributeName in attributes) {
if (!attributes[attributeName]) {
continue
}
stringifiedAttributes += '; ' + attributeName;
if (attributes[attributeName] === true) {
continue
}
// Considers RFC 6265 section 5.2:
// ...
// 3. If the remaining unparsed-attributes contains a %x3B (";")
// character:
// Consume the characters of the unparsed-attributes up to,
// not including, the first %x3B (";") character.
// ...
stringifiedAttributes += '=' + attributes[attributeName].split(';')[0];
}
return (document.cookie =
name + '=' + converter.write(value, name) + stringifiedAttributes)
}
function get (name) {
if (typeof document === 'undefined' || (arguments.length && !name)) {
return
}
// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all.
var cookies = document.cookie ? document.cookie.split('; ') : [];
var jar = {};
for (var i = 0; i < cookies.length; i++) {
var parts = cookies[i].split('=');
var value = parts.slice(1).join('=');
try {
var found = decodeURIComponent(parts[0]);
jar[found] = converter.read(value, found);
if (name === found) {
break
}
} catch (e) {}
}
return name ? jar[name] : jar
}
return Object.create(
{
set,
get,
remove: function (name, attributes) {
set(
name,
'',
assign({}, attributes, {
expires: -1
})
);
},
withAttributes: function (attributes) {
return init(this.converter, assign({}, this.attributes, attributes))
},
withConverter: function (converter) {
return init(assign({}, this.converter, converter), this.attributes)
}
},
{
attributes: { value: Object.freeze(defaultAttributes) },
converter: { value: Object.freeze(converter) }
}
)
}
init(defaultConverter, { path: '/' });
let OrgRoleStructure = /*#__PURE__*/function (OrgRoleStructure) {
OrgRoleStructure["SingleRole"] = "single_role_in_hierarchy";
OrgRoleStructure["MultiRole"] = "multi_role";
return OrgRoleStructure;
}({});
function getAccessHelper$1(orgIdToOrgMemberInfo) {
function isRole(orgId, role) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
if (orgMemberInfo.orgRoleStructure === OrgRoleStructure.MultiRole) {
return orgMemberInfo.userAssignedRole === role || orgMemberInfo.userAssignedAdditionalRoles.includes(role);
} else {
return orgMemberInfo.userAssignedRole === role;
}
}
function isAtLeastRole(orgId, role) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
if (orgMemberInfo.orgRoleStructure === OrgRoleStructure.MultiRole) {
return orgMemberInfo.userAssignedRole === role || orgMemberInfo.userAssignedAdditionalRoles.includes(role);
} else {
return orgMemberInfo.userInheritedRolesPlusCurrentRole.includes(role);
}
}
function hasPermission(orgId, permission) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
return orgMemberInfo.userPermissions.includes(permission);
}
function hasAllPermissions(orgId, permissions) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
return permissions.every(permission => orgMemberInfo.userPermissions.includes(permission));
}
function getAccessHelperWithOrgId(orgId) {
return {
isRole(role) {
return isRole(orgId, role);
},
isAtLeastRole(role) {
return isAtLeastRole(orgId, role);
},
hasPermission(permission) {
return hasPermission(orgId, permission);
},
hasAllPermissions(permissions) {
return hasAllPermissions(orgId, permissions);
}
};
}
return {
isRole,
isAtLeastRole,
hasPermission,
hasAllPermissions,
getAccessHelperWithOrgId
};
}
function getOrgHelper$1(orgIdToOrgMemberInfo) {
return {
getOrg(orgId) {
if (orgIdToOrgMemberInfo.hasOwnProperty(orgId)) {
return orgIdToOrgMemberInfo[orgId];
} else {
return undefined;
}
},
getOrgIds() {
return Object.keys(orgIdToOrgMemberInfo);
},
getOrgs() {
return Object.values(orgIdToOrgMemberInfo);
},
getOrgByName(orgName) {
for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) {
if (orgMemberInfo.orgName === orgName || orgMemberInfo.urlSafeOrgName === orgName) {
return orgMemberInfo;
}
}
return undefined;
}
};
}
class UserClass {
// Metadata about the user
// If you used our migration APIs to migrate this user from a different system,
// this is their original ID from that system.
constructor(userFields, orgIdToUserOrgInfo) {
this.userId = userFields.userId;
this.orgIdToUserOrgInfo = orgIdToUserOrgInfo;
this.email = userFields.email;
this.firstName = userFields.firstName;
this.lastName = userFields.lastName;
this.username = userFields.username;
this.createdAt = userFields.createdAt;
this.pictureUrl = userFields.pictureUrl;
this.hasPassword = userFields.hasPassword;
this.hasMfaEnabled = userFields.hasMfaEnabled;
this.canCreateOrgs = userFields.canCreateOrgs;
this.legacyUserId = userFields.legacyUserId;
this.impersonatorUserId = userFields.impersonatorUserId;
this.properties = userFields.properties;
}
getOrg(orgId) {
if (!this.orgIdToUserOrgInfo) {
return undefined;
}
return this.orgIdToUserOrgInfo[orgId];
}
getOrgByName(orgName) {
if (!this.orgIdToUserOrgInfo) {
return undefined;
}
const urlSafeOrgName = orgName.toLowerCase().replace(/ /g, "-");
for (const orgId in this.orgIdToUserOrgInfo) {
const orgMemberInfo = this.orgIdToUserOrgInfo[orgId];
if ((orgMemberInfo === null || orgMemberInfo === void 0 ? void 0 : orgMemberInfo.urlSafeOrgName) === urlSafeOrgName) {
return orgMemberInfo;
}
}
return undefined;
}
getUserProperty(key) {
if (!this.properties) {
return undefined;
}
return this.properties[key];
}
getOrgs() {
if (!this.orgIdToUserOrgInfo) {
return [];
}
return Object.values(this.orgIdToUserOrgInfo);
}
isImpersonating() {
return !!this.impersonatorUserId;
}
isRole(orgId, role) {
const orgMemberInfo = this.getOrg(orgId);
if (!orgMemberInfo) {
return false;
}
return orgMemberInfo.isRole(role);
}
isAtLeastRole(orgId, role) {
const orgMemberInfo = this.getOrg(orgId);
if (!orgMemberInfo) {
return false;
}
return orgMemberInfo.isAtLeastRole(role);
}
hasPermission(orgId, permission) {
const orgMemberInfo = this.getOrg(orgId);
if (!orgMemberInfo) {
return false;
}
return orgMemberInfo.hasPermission(permission);
}
hasAllPermissions(orgId, permissions) {
const orgMemberInfo = this.getOrg(orgId);
if (!orgMemberInfo) {
return false;
}
return orgMemberInfo.hasAllPermissions(permissions);
}
static fromJSON(json) {
const obj = JSON.parse(json);
const orgIdToUserOrgInfo = {};
for (const orgId in obj.orgIdToUserOrgInfo) {
orgIdToUserOrgInfo[orgId] = OrgMemberInfoClass.fromJSON(JSON.stringify(obj.orgIdToUserOrgInfo[orgId]));
}
try {
return new UserClass({
userId: obj.userId,
email: obj.email,
createdAt: obj.createdAt,
firstName: obj.firstName,
lastName: obj.lastName,
username: obj.username,
legacyUserId: obj.legacyUserId,
impersonatorUserId: obj.impersonatorUserId,
properties: obj.properties,
pictureUrl: obj.pictureUrl,
hasPassword: obj.hasPassword,
hasMfaEnabled: obj.hasMfaEnabled,
canCreateOrgs: obj.canCreateOrgs
}, orgIdToUserOrgInfo);
} catch (e) {
console.error("Unable to parse User. Make sure the JSON string is a stringified `UserClass` type.", e);
throw e;
}
}
}
class OrgMemberInfoClass {
constructor(orgId, orgName, orgMetadata, urlSafeOrgName, userAssignedRole, userInheritedRolesPlusCurrentRole, userPermissions, orgRoleStructure, userAssignedAdditionalRoles, legacyOrgId) {
this.orgId = orgId;
this.orgName = orgName;
this.legacyOrgId = legacyOrgId;
this.orgMetadata = orgMetadata;
this.urlSafeOrgName = urlSafeOrgName;
this.orgRoleStructure = orgRoleStructure !== null && orgRoleStructure !== void 0 ? orgRoleStructure : OrgRoleStructure.SingleRole;
this.userAssignedRole = userAssignedRole;
this.userInheritedRolesPlusCurrentRole = userInheritedRolesPlusCurrentRole;
this.userPermissions = userPermissions;
this.userAssignedAdditionalRoles = userAssignedAdditionalRoles !== null && userAssignedAdditionalRoles !== void 0 ? userAssignedAdditionalRoles : [];
}
// validation methods
isRole(role) {
if (this.orgRoleStructure === OrgRoleStructure.MultiRole) {
return this.userAssignedRole === role || this.userAssignedAdditionalRoles.includes(role);
} else {
return this.userAssignedRole === role;
}
}
isAtLeastRole(role) {
if (this.orgRoleStructure === OrgRoleStructure.MultiRole) {
return this.userAssignedRole === role || this.userAssignedAdditionalRoles.includes(role);
} else {
return this.userInheritedRolesPlusCurrentRole.includes(role);
}
}
hasPermission(permission) {
return this.userPermissions.includes(permission);
}
hasAllPermissions(permissions) {
return permissions.every(permission => this.hasPermission(permission));
}
static fromJSON(json) {
const obj = JSON.parse(json);
try {
return new OrgMemberInfoClass(obj.orgId, obj.orgName, obj.orgMetadata, obj.urlSafeOrgName, obj.userAssignedRole, obj.userInheritedRolesPlusCurrentRole, obj.userPermissions, obj.orgRoleStructure, obj.userAssignedAdditionalRoles, obj.legacyOrgId);
} catch (e) {
console.error("Unable to parse UserOrgInfo. Make sure the JSON string is a stringified `UserOrgInfo` type.", e);
throw e;
}
}
}
function convertOrgIdToOrgMemberInfo(orgIdToOrgMemberInfo) {
if (orgIdToOrgMemberInfo === undefined) {
return undefined;
}
const orgIdToUserOrgInfo = {};
for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) {
orgIdToUserOrgInfo[orgMemberInfo.orgId] = new OrgMemberInfoClass(orgMemberInfo.orgId, orgMemberInfo.orgName, orgMemberInfo.orgMetadata, orgMemberInfo.urlSafeOrgName, orgMemberInfo.userAssignedRole, orgMemberInfo.userInheritedRolesPlusCurrentRole, orgMemberInfo.userPermissions, orgMemberInfo.orgRoleStructure, orgMemberInfo.userAssignedAdditionalRoles, orgMemberInfo.legacyOrgId);
}
return orgIdToUserOrgInfo;
}
function fetchAuthenticationInfo(authUrl, activeOrgId) {
const queryParams = new URLSearchParams();
if (activeOrgId) {
queryParams.append("active_org_id", activeOrgId);
}
let path = `${authUrl}/api/v1/refresh_token`;
if (queryParams.toString()) {
path += `?${queryParams.toString()}`;
}
return fetch(path, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json"
}
}).then(res => {
if (res.status === 401) {
return null;
} else if (res.status === 0) {
logCorsError();
return Promise.reject({
status: 503,
message: "Unable to process authentication response"
});
} else if (!res.ok) {
return Promise.reject({
status: res.status,
message: res.statusText
});
} else {
return parseResponse(res);
}
});
}
function logout(authUrl) {
return fetch(`${authUrl}/api/v1/logout`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
}
}).then(res => {
if (res.status === 0) {
logCorsError();
return Promise.reject({
status: 503,
message: "Unable to process authentication response"
});
} else if (!res.ok) {
console.error("Logout error", res.status, res.statusText);
return Promise.reject({
status: res.status,
message: res.statusText
});
} else {
return res.json();
}
});
}
function parseResponse(res) {
return res.text().then(httpResponse => {
try {
const authInfoWithoutUserClass = parseJsonConvertingSnakeToCamel(httpResponse);
return withExtraArgs(authInfoWithoutUserClass);
} catch (e) {
console.error("Unable to process authentication response", e);
return Promise.reject({
status: 500,
message: "Unable to process authentication response"
});
}
}, e => {
console.error("Unable to process authentication response", e);
return Promise.reject({
status: 500,
message: "Unable to process authentication response"
});
});
}
// The API responds with snake_case, but TypeScript convention is camelCase.
// When parsing JSON, we pass in reviver function to convert from snake_case to camelCase.
function parseJsonConvertingSnakeToCamel(str) {
return JSON.parse(str, function (key, value) {
if (key === "org_id") {
this.orgId = value;
} else if (key === "org_name") {
this.orgName = value;
} else if (key === "org_metadata") {
this.orgMetadata = value;
} else if (key === "url_safe_org_name") {
this.urlSafeOrgName = value;
} else if (key === "user_role") {
this.userAssignedRole = value;
} else if (key === "inherited_user_roles_plus_current_role") {
this.userInheritedRolesPlusCurrentRole = value;
} else if (key === "user_permissions") {
this.userPermissions = value;
} else if (key === "access_token") {
this.accessToken = value;
} else if (key === "expires_at_seconds") {
this.expiresAtSeconds = value;
} else if (key === "org_id_to_org_member_info") {
this.orgIdToOrgMemberInfo = value;
} else if (key === "user_id") {
this.userId = value;
} else if (key === "email_confirmed") {
this.emailConfirmed = value;
} else if (key === "first_name") {
this.firstName = value;
} else if (key === "last_name") {
this.lastName = value;
} else if (key === "picture_url") {
this.pictureUrl = value;
} else if (key === "mfa_enabled") {
this.mfaEnabled = value;
} else if (key === "has_password") {
this.hasPassword = value;
} else if (key === "can_create_orgs") {
this.canCreateOrgs = value;
} else if (key === "created_at") {
this.createdAt = value;
} else if (key === "last_active_at") {
this.lastActiveAt = value;
} else if (key === "legacy_user_id") {
this.legacyUserId = value;
} else if (key === "legacy_org_id") {
this.legacyOrgId = value;
} else if (key === "impersonator_user") {
this.impersonatorUserId = value;
} else if (key === "org_role_structure") {
this.orgRoleStructure = value;
} else if (key === "additional_roles") {
this.userAssignedAdditionalRoles = value;
} else {
return value;
}
});
}
function withExtraArgs(authInfoWithoutExtraArgs) {
if (authInfoWithoutExtraArgs.orgIdToOrgMemberInfo) {
authInfoWithoutExtraArgs.orgHelper = getOrgHelper$1(authInfoWithoutExtraArgs.orgIdToOrgMemberInfo);
authInfoWithoutExtraArgs.accessHelper = getAccessHelper$1(authInfoWithoutExtraArgs.orgIdToOrgMemberInfo);
}
authInfoWithoutExtraArgs.userClass = new UserClass({
userId: authInfoWithoutExtraArgs.user.userId,
email: authInfoWithoutExtraArgs.user.email,
createdAt: authInfoWithoutExtraArgs.user.createdAt,
firstName: authInfoWithoutExtraArgs.user.firstName,
lastName: authInfoWithoutExtraArgs.user.lastName,
username: authInfoWithoutExtraArgs.user.username,
properties: authInfoWithoutExtraArgs.user.properties,
pictureUrl: authInfoWithoutExtraArgs.user.pictureUrl,
hasPassword: authInfoWithoutExtraArgs.user.hasPassword,
hasMfaEnabled: authInfoWithoutExtraArgs.user.mfaEnabled,
canCreateOrgs: authInfoWithoutExtraArgs.user.canCreateOrgs,
legacyUserId: authInfoWithoutExtraArgs.user.legacyUserId,
impersonatorUserId: authInfoWithoutExtraArgs.impersonatorUserId
}, convertOrgIdToOrgMemberInfo(authInfoWithoutExtraArgs.orgIdToOrgMemberInfo));
return Promise.resolve(authInfoWithoutExtraArgs);
}
function logCorsError() {
console.error("Request to PropelAuth failed due to a CORS error. There are a few likely causes: \n" + " 1. In the Frontend Integration section of your dashboard, make sure your requests are coming either the specified Application URL or localhost with a matching port.\n" + " 2. Make sure your server is hosted on HTTPS in production.");
}
const DEFAULT_RETRIES = 3;
const runWithRetriesOnAnyError = async fn => {
return runWithRetriesInner(fn, DEFAULT_RETRIES);
};
const runWithRetriesInner = async (fn, numRetriesLeft) => {
try {
return await fn();
} catch (e) {
if (numRetriesLeft <= 0) {
throw e;
}
await delay(numRetriesLeftToDelay(numRetriesLeft));
return runWithRetriesInner(fn, numRetriesLeft - 1);
}
};
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const numRetriesLeftToDelay = numRetriesLeft => {
// We could be fancy, but we only retry 3 times so...
if (numRetriesLeft >= 3) {
return 100;
} else if (numRetriesLeft === 2) {
return 200;
} else {
return 300;
}
};
function currentTimeSeconds() {
return Date.now() / 1000;
}
function hasLocalStorage() {
return typeof localStorage !== "undefined";
}
function hasWindow() {
return typeof window !== "undefined";
}
function getLocalStorageNumber(key) {
if (!hasLocalStorage()) {
return null;
}
const value = localStorage.getItem(key);
if (!value) {
return null;
}
const num = parseInt(value, 10);
if (Number.isNaN(num)) {
return null;
}
return num;
}
const LOGGED_IN_AT_KEY = "__PROPEL_AUTH_LOGGED_IN_AT";
const LOGGED_OUT_AT_KEY = "__PROPEL_AUTH_LOGGED_OUT_AT";
const AUTH_TOKEN_REFRESH_BEFORE_EXPIRATION_SECONDS = 10 * 60;
const DEFAULT_MIN_SECONDS_BEFORE_REFRESH = 60 * 2;
const ACTIVE_ORG_ACCESS_TOKEN_REFRESH_EXPIRATION_SECONDS = 60 * 5;
const encodeBase64 = str => {
const encode = window ? window.btoa : btoa;
return encode(str);
};
function validateAndCleanupOptions(authOptions) {
try {
// This helps make sure we have a consistent URL ignoring things like trailing slashes
const authUrl = new URL(authOptions.authUrl);
authOptions.authUrl = authUrl.origin;
} catch (e) {
console.error("Invalid authUrl", e);
throw new Error("Unable to initialize auth client");
}
if (authOptions.enableBackgroundTokenRefresh === undefined) {
authOptions.enableBackgroundTokenRefresh = true;
}
}
function createClient(authOptions) {
const {
minSecondsBeforeRefresh = DEFAULT_MIN_SECONDS_BEFORE_REFRESH
} = authOptions;
validateAndCleanupOptions(authOptions);
// Internal state
const clientState = {
initialLoadFinished: false,
authenticationInfo: null,
observers: [],
accessTokenObservers: [],
lastLoggedInAtMessage: getLocalStorageNumber(LOGGED_IN_AT_KEY),
lastLoggedOutAtMessage: getLocalStorageNumber(LOGGED_OUT_AT_KEY),
authUrl: authOptions.authUrl,
refreshInterval: null,
lastRefresh: null,
accessTokenActiveOrgMap: {}
};
// Helper functions
function notifyObservers(isLoggedIn) {
for (let i = 0; i < clientState.observers.length; i++) {
const observer = clientState.observers[i];
if (observer) {
observer(isLoggedIn);
}
}
}
function notifyObserversOfAccessTokenChange(accessToken) {
for (let i = 0; i < clientState.accessTokenObservers.length; i++) {
const observer = clientState.accessTokenObservers[i];
if (observer) {
observer(accessToken);
}
}
}
function userJustLoggedOut(accessToken, previousAccessToken) {
// Edge case: the first time we go to the page, if we can't load the
// auth token we should treat it as a logout event
return !accessToken && (previousAccessToken || !clientState.initialLoadFinished);
}
function userJustLoggedIn(accessToken, previousAccessToken) {
return !previousAccessToken && accessToken;
}
function updateLastLoggedOutAt() {
const loggedOutAt = currentTimeSeconds();
clientState.lastLoggedOutAtMessage = loggedOutAt;
if (hasLocalStorage()) {
localStorage.setItem(LOGGED_OUT_AT_KEY, String(loggedOutAt));
}
}
function updateLastLoggedInAt() {
const loggedInAt = currentTimeSeconds();
clientState.lastLoggedInAtMessage = loggedInAt;
if (hasLocalStorage()) {
localStorage.setItem(LOGGED_IN_AT_KEY, String(loggedInAt));
}
}
/**
* Invalidates all org's access tokens.
*/
function resetAccessTokenActiveOrgMap() {
clientState.accessTokenActiveOrgMap = {};
}
function setAuthenticationInfoAndUpdateDownstream(authenticationInfo) {
var _clientState$authenti;
const previousAccessToken = (_clientState$authenti = clientState.authenticationInfo) === null || _clientState$authenti === void 0 ? void 0 : _clientState$authenti.accessToken;
clientState.authenticationInfo = authenticationInfo;
const accessToken = authenticationInfo === null || authenticationInfo === void 0 ? void 0 : authenticationInfo.accessToken;
if (userJustLoggedOut(accessToken, previousAccessToken)) {
notifyObservers(false);
updateLastLoggedOutAt();
} else if (userJustLoggedIn(accessToken, previousAccessToken)) {
notifyObservers(true);
updateLastLoggedInAt();
}
if (previousAccessToken !== accessToken) {
notifyObserversOfAccessTokenChange(accessToken);
}
resetAccessTokenActiveOrgMap();
clientState.lastRefresh = currentTimeSeconds();
clientState.initialLoadFinished = true;
}
async function forceRefreshToken(returnCached) {
try {
// Happy case, we fetch auth info and save it
const authenticationInfo = await runWithRetriesOnAnyError(() => fetchAuthenticationInfo(clientState.authUrl));
setAuthenticationInfoAndUpdateDownstream(authenticationInfo);
return authenticationInfo;
} catch (e) {
// If there was an error, we sometimes still want to return the value we have cached
// (e.g. if we were prefetching), so in those cases we swallow the exception
if (returnCached) {
return clientState.authenticationInfo;
} else {
setAuthenticationInfoAndUpdateDownstream(null);
throw e;
}
}
}
const getSignupPageUrl = options => {
let qs = new URLSearchParams();
let url = `${clientState.authUrl}/signup`;
if (options) {
const {
postSignupRedirectUrl,
userSignupQueryParameters
} = options;
if (postSignupRedirectUrl) {
qs.set("rt", encodeBase64(postSignupRedirectUrl));
}
if (userSignupQueryParameters) {
Object.entries(userSignupQueryParameters).forEach(([key, value]) => {
qs.set(key, value);
});
}
}
if (qs.toString()) {
url += `?${qs.toString()}`;
}
return url;
};
const getLoginPageUrl = options => {
let qs = new URLSearchParams();
let url = `${clientState.authUrl}/login`;
if (options) {
const {
postLoginRedirectUrl,
userSignupQueryParameters
} = options;
if (postLoginRedirectUrl) {
qs.set("rt", encodeBase64(postLoginRedirectUrl));
}
if (userSignupQueryParameters) {
Object.entries(userSignupQueryParameters).forEach(([key, value]) => {
qs.set(key, value);
});
}
}
if (qs.toString()) {
url += `?${qs.toString()}`;
}
return url;
};
const getAccountPageUrl = options => {
let qs = new URLSearchParams();
let url = `${clientState.authUrl}/account`;
if (options) {
const {
redirectBackToUrl
} = options;
if (redirectBackToUrl) {
qs.set("rt", encodeBase64(redirectBackToUrl));
}
}
if (qs.toString()) {
url += `?${qs.toString()}`;
}
return url;
};
const getOrgPageUrl = (orgId, options) => {
let qs = new URLSearchParams();
let url = `${clientState.authUrl}/org`;
if (orgId) {
qs.set("id", orgId);
}
if (options) {
if (options.redirectBackToUrl) {
qs.set("rt", encodeBase64(options.redirectBackToUrl));
}
}
if (qs.toString()) {
url += `?${qs.toString()}`;
}
return url;
};
const getCreateOrgPageUrl = options => {
let qs = new URLSearchParams();
let url = `${clientState.authUrl}/create_org`;
if (options) {
const {
redirectBackToUrl
} = options;
if (redirectBackToUrl) {
qs.set("rt", encodeBase64(redirectBackToUrl));
}
}
if (qs.toString()) {
url += `?${qs.toString()}`;
}
return url;
};
const getSetupSAMLPageUrl = (orgId, options) => {
let qs = new URLSearchParams();
if (options) {
if (options.redirectBackToUrl) {
qs.set("rt", encodeBase64(options.redirectBackToUrl));
}
}
qs.set("id", orgId);
return `${clientState.authUrl}/saml?${qs.toString()}`;
};
const client = {
addLoggedInChangeObserver(loggedInChangeObserver) {
const hasObserver = clientState.observers.includes(loggedInChangeObserver);
if (hasObserver) {
console.error("Observer has been attached already.");
} else if (!loggedInChangeObserver) {
console.error("Cannot add a null observer");
} else {
clientState.observers.push(loggedInChangeObserver);
}
},
removeLoggedInChangeObserver(loggedInChangeObserver) {
const observerIndex = clientState.observers.indexOf(loggedInChangeObserver);
if (observerIndex === -1) {
console.error("Cannot find observer to remove");
} else {
clientState.observers.splice(observerIndex, 1);
}
},
addAccessTokenChangeObserver(observer) {
const hasObserver = clientState.accessTokenObservers.includes(observer);
if (hasObserver) {
console.error("Observer has been attached already.");
} else if (!observer) {
console.error("Cannot add a null observer");
} else {
clientState.accessTokenObservers.push(observer);
}
},
removeAccessTokenChangeObserver(observer) {
const observerIndex = clientState.accessTokenObservers.indexOf(observer);
if (observerIndex === -1) {
console.error("Cannot find observer to remove");
} else {
clientState.accessTokenObservers.splice(observerIndex, 1);
}
},
async getAuthenticationInfoOrNull(forceRefresh) {
const currentTimeSecs = currentTimeSeconds();
if (forceRefresh) {
return await forceRefreshToken(false);
} else if (!clientState.authenticationInfo) {
return await forceRefreshToken(false);
} else if (currentTimeSecs + AUTH_TOKEN_REFRESH_BEFORE_EXPIRATION_SECONDS > clientState.authenticationInfo.expiresAtSeconds) {
// Small edge case: If we were being proactive
// and the auth information hasn't expired yet, swallow any exceptions
const returnCached = currentTimeSecs < clientState.authenticationInfo.expiresAtSeconds;
return await forceRefreshToken(returnCached);
} else {
return clientState.authenticationInfo;
}
},
async getAccessTokenForOrg(orgId) {
// First, check if there is a valid access token for the org ID in the
// valid time frame.
const currentTimeSecs = currentTimeSeconds();
const activeOrgAccessToken = clientState.accessTokenActiveOrgMap[orgId];
if (!!activeOrgAccessToken) {
if (currentTimeSecs < activeOrgAccessToken.fetchedAt + ACTIVE_ORG_ACCESS_TOKEN_REFRESH_EXPIRATION_SECONDS) {
return {
accessToken: activeOrgAccessToken.accessToken,
error: undefined
};
}
}
// Fetch the access token for the org ID and update.
try {
const authenticationInfo = await runWithRetriesOnAnyError(() => fetchAuthenticationInfo(clientState.authUrl, orgId));
if (!authenticationInfo) {
// Only null if 401 unauthorized.
return {
error: "user_not_in_org",
accessToken: null
};
}
const {
accessToken
} = authenticationInfo;
clientState.accessTokenActiveOrgMap[orgId] = {
accessToken,
fetchedAt: currentTimeSecs
};
return {
accessToken,
error: undefined
};
} catch (e) {
return {
error: "unexpected_error",
accessToken: null
};
}
},
getSignupPageUrl(options) {
return getSignupPageUrl(options);
},
getLoginPageUrl(options) {
return getLoginPageUrl(options);
},
getAccountPageUrl(options) {
return getAccountPageUrl(options);
},
getOrgPageUrl(orgId, options) {
return getOrgPageUrl(orgId, options);
},
getCreateOrgPageUrl(options) {
return getCreateOrgPageUrl(options);
},
getSetupSAMLPageUrl(orgId, options) {
return getSetupSAMLPageUrl(orgId, options);
},
redirectToSignupPage(options) {
window.location.href = getSignupPageUrl(options);
},
redirectToLoginPage(options) {
window.location.href = getLoginPageUrl(options);
},
redirectToAccountPage(options) {
window.location.href = getAccountPageUrl(options);
},
redirectToOrgPage(orgId, options) {
window.location.href = getOrgPageUrl(orgId, options);
},
redirectToCreateOrgPage(options) {
window.location.href = getCreateOrgPageUrl(options);
},
redirectToSetupSAMLPage(orgId, options) {
window.location.href = getSetupSAMLPageUrl(orgId, options);
},
async logout(redirectAfterLogout) {
const logoutResponse = await logout(clientState.authUrl);
setAuthenticationInfoAndUpdateDownstream(null);
if (redirectAfterLogout) {
window.location.href = logoutResponse.redirect_to;
}
},
destroy() {
clientState.observers = [];
clientState.accessTokenObservers = [];
window.removeEventListener("storage", onStorageChange);
if (clientState.refreshInterval) {
clearInterval(clientState.refreshInterval);
}
}
};
const onStorageChange = async function () {
// If localStorage isn't available, nothing to do here.
// This usually happens in frameworks that have some SSR components
if (!hasLocalStorage()) {
return;
}
const loggedOutAt = getLocalStorageNumber(LOGGED_OUT_AT_KEY);
const loggedInAt = getLocalStorageNumber(LOGGED_IN_AT_KEY);
// If we've detected a logout event after the last one our client is aware of, trigger a refresh
if (loggedOutAt && (!clientState.lastLoggedOutAtMessage || loggedOutAt > clientState.lastLoggedOutAtMessage)) {
clientState.lastLoggedOutAtMessage = loggedOutAt;
if (clientState.authenticationInfo) {
await forceRefreshToken(true);
}
}
// If we've detected a login event after the last one our client is aware of, trigger a refresh
if (loggedInAt && (!clientState.lastLoggedInAtMessage || loggedInAt > clientState.lastLoggedInAtMessage)) {
clientState.lastLoggedInAtMessage = loggedInAt;
if (!clientState.authenticationInfo) {
await forceRefreshToken(true);
}
}
};
// If we were offline or on a different tab, when we return, refetch auth info
// Some browsers trigger focus more often than we'd like, so we'll debounce a little here as well
const onOnlineOrFocus = async function () {
if (clientState.lastRefresh && currentTimeSeconds() > clientState.lastRefresh + minSecondsBeforeRefresh) {
await forceRefreshToken(true);
} else {
await client.getAuthenticationInfoOrNull();
}
};
if (hasWindow()) {
window.addEventListener("storage", onStorageChange);
window.addEventListener("online", onOnlineOrFocus);
window.addEventListener("focus", onOnlineOrFocus);
if (authOptions.enableBackgroundTokenRefresh) {
client.getAuthenticationInfoOrNull();
clientState.refreshInterval = window.setInterval(client.getAuthenticationInfoOrNull, 60000);
}
}
return client;
}
const DEPRECATED_ORG_SELECTION_LOCAL_STORAGE_KEY = "__last_selected_org";
/**
* @deprecated This hook is deprecated and no longer supported.
*/
function useActiveOrg() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useActiveOrg must be used within an AuthProvider or RequiredAuthProvider");
}
if (context.loading || !context.authInfo || !context.authInfo.orgHelper) {
return null;
}
const proposedActiveOrgIdOrName = context.activeOrgFn();
if (!proposedActiveOrgIdOrName) {
return null;
}
const orgHelper = context.authInfo.orgHelper;
return orgHelper.getOrg(proposedActiveOrgIdOrName) || orgHelper.getOrgByName(proposedActiveOrgIdOrName);
}
function saveOrgSelectionToLocalStorage(orgIdOrName) {
if (localStorage) {
localStorage.setItem(DEPRECATED_ORG_SELECTION_LOCAL_STORAGE_KEY, orgIdOrName);
}
}
function loadOrgSelectionFromLocalStorage() {
if (localStorage) {
return localStorage.getItem(DEPRECATED_ORG_SELECTION_LOCAL_STORAGE_KEY);
}
return null;
}
const useClientRef = props => {
const [accessTokenChangeCounter, setAccessTokenChangeCounter] = useState(0);
const {
authUrl,
minSecondsBeforeRefresh
} = props;
// Use a ref to store the client so that it doesn't get recreated on every render
const clientRef = useRef(null);
if (clientRef.current === null) {
const client = createClient({
authUrl,
enableBackgroundTokenRefresh: true,
minSecondsBeforeRefresh
});
client.addAccessTokenChangeObserver(() => setAccessTokenChangeCounter(x => x + 1));
clientRef.current = {
authUrl,
client
};
}
// If the authUrl changes, destroy the old client and create a new one
useEffect(() => {
if (clientRef.current === null) {
return;
} else if (clientRef.current.authUrl === authUrl) {
return;
} else {
clientRef.current.client.destroy();
const newClient = createClient({
authUrl,
enableBackgroundTokenRefresh: true,
minSecondsBeforeRefresh
});
newClient.addAccessTokenChangeObserver(() => setAccessTokenChangeCounter(x => x + 1));
clientRef.current = {
authUrl,
client: newClient
};
}
}, [authUrl]);
return {
clientRef,
accessTokenChangeCounter
};
};
const useClientRefCallback = (clientRef, callback) => {
return useCallback((...inputs) => {
const client = clientRef.current?.client;
if (!client) {
throw new Error("Client is not initialized");
}
return callback(client)(...inputs);
}, [callback]);
};
const AuthContext = /*#__PURE__*/React.createContext(undefined);
const initialAuthInfoState = {
loading: true,
authInfo: null
};
function authInfoStateReducer(_state, action) {
if (!action.authInfo) {
return {
loading: false,
authInfo: action.authInfo
};
} else if (_state.loading) {
return {
loading: false,
authInfo: action.authInfo
};
} else if (_state?.authInfo?.accessToken !== action.authInfo?.accessToken) {
return {
loading: false,
authInfo: action.authInfo
};
} else {
return _state;
}
}
const AuthProvider = props => {
const {
authUrl,
minSecondsBeforeRefresh,
getActiveOrgFn: deprecatedGetActiveOrgFn,
children,
defaultDisplayWhileLoading,
defaultDisplayIfLoggedOut
} = props;
const [authInfoState, dispatch] = useReducer(authInfoStateReducer, initialAuthInfoState);
const {
clientRef,
accessTokenChangeCounter
} = useClientRef({
authUrl,
minSecondsBeforeRefresh
});
// Refresh the token when the user has logged in or out
useEffect(() => {
let didCancel = false;
async function refreshToken() {
const client = clientRef.current?.client;
if (!client) {
return;
}
try {
const authInfo = await client.getAuthenticationInfoOrNull();
if (!didCancel) {
dispatch({
authInfo
});
}
} catch (_) {
// Important errors are logged in the client library
}
}
refreshToken();
return () => {
didCancel = true;
};
}, [accessTokenChangeCounter]);
// Deprecation warning
useEffect(() => {
if (deprecatedGetActiveOrgFn) {
console.warn("The `getActiveOrgFn` prop is deprecated.");
}
}, []);
const logout = useClientRefCallback(clientRef, client => client.logout);
const redirectToLoginPage = useClientRefCallback(clientRef, client => client.redirectToLoginPage);
const redirectToSignupPage = useClientRefCallback(clientRef, client => client.redirectToSignupPage);
const redirectToAccountPage = useClientRefCallback(clientRef, client => client.redirectToAccountPage);
const redirectToOrgPage = useClientRefCallback(clientRef, client => client.redirectToOrgPage);
const redirectToCreateOrgPage = useClientRefCallback(clientRef, client => client.redirectToCreateOrgPage);
const redirectToSetupSAMLPage = useClientRefCallback(clientRef, client => client.redirectToSetupSAMLPage);
const getLoginPageUrl = useClientRefCallback(clientRef, client => client.getLoginPageUrl);
const getSignupPageUrl = useClientRefCallback(clientRef, client => client.getSignupPageUrl);
const getAccountPageUrl = useClientRefCallback(clientRef, client => client.getAccountPageUrl);
const getOrgPageUrl = useClientRefCallback(clientRef, client => client.getOrgPageUrl);
const getCreateOrgPageUrl = useClientRefCallback(clientRef, client => client.getCreateOrgPageUrl);
const getSetupSAMLPageUrl = useClientRefCallback(clientRef, client => client.getSetupSAMLPageUrl);
const getAccessTokenForOrg = useClientRefCallback(clientRef, client => client.getAccessTokenForOrg);
const getAccessToken = useClientRefCallback(clientRef, client => {
return async () => {
const authInfo = await client.getAuthenticationInfoOrNull();
return authInfo?.accessToken;
};
});
const refreshAuthInfo = useCallback(async () => {
if (clientRef.current === null) {
return;
}
const client = clientRef.current.client;
const authInfo = await client.getAuthenticationInfoOrNull(true);
dispatch({
authInfo
});
}, [dispatch]);
// TODO: Remove this, as both `getActiveOrgFn` and `loadOrgSelectionFromLocalStorage` are deprecated.
const deprecatedActiveOrgFn = deprecatedGetActiveOrgFn || loadOrgSelectionFromLocalStorage;
const value = {
loading: authInfoState.loading,
authInfo: authInfoState.authInfo,
logout,
defaultDisplayWhileLoading,
defaultDisplayIfLoggedOut,
redirectToLoginPage,
redirectToSignupPage,
redirectToAccountPage,
redirectToOrgPage,
redirectToCreateOrgPage,
redirectToSetupSAMLPage,
getLoginPageUrl,
activeOrgFn: deprecatedActiveOrgFn,
getSignupPageUrl,
getAccountPageUrl,
getOrgPageUrl,
getCreateOrgPageUrl,
getSetupSAMLPageUrl,
authUrl,
refreshAuthInfo,
tokens: {
getAccessTokenForOrg,
getAccessToken
}
};
return /*#__PURE__*/React.createElement(AuthContext.Provider, {
value: value
}, children);
};
// User information that we will hard code within the AuthProvider
/**
* A version of the AuthProvider specifically used for testing. It won't make any external requests, but will
* instead set up the AuthProvider to act as if the information provided was returned from the API.
*/
const AuthProviderForTesting = ({
loading,
userInformation,
activeOrgFn,
authUrl,
children
}) => {
const authInfo = getAuthInfoForTesting(userInformation);
const activeOrgFnWithDefault = activeOrgFn ? activeOrgFn : () => null;
const getAccessTokenForOrg = useCallback(orgId => {
if (userInformation?.getAccessTokenForOrg) {
return userInformation.getAccessTokenForOrg(orgId);
}
return Promise.resolve({
error: undefined,
accessToken: "ACCESS_TOKEN"
});
}, [userInformation?.getAccessTokenForOrg]);
const contextValue = {
loading: !!loading,
authInfo,
logout: () => Promise.resolve(),
redirectToLoginPage: () => {},
redirectToSignupPage: () => {},
redirectToAccountPage: () => {},
redirectToOrgPage: () => {},
redirectToCreateOrgPage: () => {},
redirectToSetupSAMLPage: () => {},
getLoginPageUrl: () => "",
getSignupPageUrl: () => "",
getAccountPageUrl: () => "",
getOrgPageUrl: () => "",
getCreateOrgPageUrl: () => "",
getSetupSAMLPageUrl: () => "",
authUrl: authUrl ?? "https://auth.example.com",
activeOrgFn: activeOrgFnWithDefault,
refreshAuthInfo: () => Promise.resolve(),
tokens: {
getAccessTokenForOrg: getAccessTokenForOrg,
getAccessToken: () => Promise.resolve(userInformation?.accessToken ?? "ACCESS_TOKEN")
}
};
return /*#__PURE__*/React.createElement(AuthContext.Provider, {
value: contextValue
}, children);
};
function getAuthInfoForTesting(userInformation) {
if (!userInformation) {
return null;
}
const orgIdToOrgMemberInfo = {};
for (const orgMemberInfo of userInformation.orgMemberInfos) {
orgIdToOrgMemberInfo[orgMemberInfo.orgId] = orgMemberInfo;
}
const accessTokenWithDefault = userInformation.accessToken === undefined ? "PLACEHOLDER_ACCESS_TOKEN" : userInformation.accessToken;
return {
accessToken: accessTokenWithDefault,
expiresAtSeconds: 1701596820,
orgHelper: getOrgHelper(orgIdToOrgMemberInfo),
accessHelper: getAccessHelper(orgIdToOrgMemberInfo),
orgIdToOrgMemberInfo: orgIdToOrgMemberInfo,
user: userInformation.user,
userClass: new UserClass(userInformation.user, toOrgIdToUserOrgInfo(orgIdToOrgMemberInfo))
};
}
function toOrgIdToUserOrgInfo(orgIdToOrgMemberInfo) {
const orgIdToUserOrgInfo = {};
for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) {
orgIdToUserOrgInfo[orgMemberInfo.orgId] = new OrgMemberInfoClass(orgMemberInfo.orgId, orgMemberInfo.orgName, {}, orgMemberInfo.urlSafeOrgName, orgMemberInfo.userAssignedRole, orgMemberInfo.userInheritedRolesPlusCurrentRole, orgMemberInfo.userPermissions);
}
return orgIdToUserOrgInfo;
}
// These helpers come from @propelauth/javascript, down the road we may want to export them from that library
// instead of copying
function getOrgHelper(orgIdToOrgMemberInfo) {
return {
getOrg(orgId) {
if (Object.prototype.hasOwnProperty.call(orgIdToOrgMemberInfo, orgId)) {
return orgIdToOrgMemberInfo[orgId];
} else {
return undefined;
}
},
getOrgIds() {
return Object.keys(orgIdToOrgMemberInfo);
},
getOrgs() {
return Object.values(orgIdToOrgMemberInfo);
},
getOrgByName(orgName) {
for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) {
if (orgMemberInfo.orgName === orgName || orgMemberInfo.urlSafeOrgName === orgName) {
return orgMemberInfo;
}
}
return undefined;
}
};
}
function getAccessHelper(orgIdToOrgMemberInfo) {
function isRole(orgId, role) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
return orgMemberInfo.userAssignedRole === role;
}
function isAtLeastRole(orgId, role) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
return orgMemberInfo.userInheritedRolesPlusCurrentRole.includes(role);
}
function hasPermission(orgId, permission) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
return orgMemberInfo.userPermissions.includes(permission);
}
function hasAllPermissions(orgId, permissions) {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId];
if (orgMemberInfo === undefined) {
return false;
}
return permissions.every(permission => orgMemberInfo.userPermissions.includes(permission));
}
function getAccessHelperWithOrgId(orgId) {
return {
isRole(role) {
return isRole(orgId, role);
},
isAtLeastRole(role) {
return isAtLeastRole(orgId, role);
},
hasPermission(permission) {
return hasPermission(orgId, permission);
},
hasAllPermissions(permissions) {
return hasAllPermissions(orgId, permissions);
}
};
}
return {
isRole,
isAtLeastRole,
hasPermission,
hasAllPermissions,
getAccessHelperWithOrgId
};
}
function useAuthInfo() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuthInfo must be used within an AuthProvider or RequiredAuthProvider");
}
const {
loading,
authInfo,
refreshAuthInfo,
tokens
} = context;
if (loading) {
return {
loading: true,
isLoggedIn: undefined,
accessToken: undefined,
orgHelper: undefined,
accessHelper: undefined,
user: undefined,
userClass: undefined,
isImpersonating: undefined,
impersonatorUserId: undefined,
refreshAuthInfo,
tokens,
accessTokenExpiresAtSeconds: undefined
};
} else if (authInfo && authInfo.accessToken) {
return {
loading: false,
isLoggedIn: true,
accessToken: authInfo.accessToken,
orgHelper: authInfo.orgHelper,
accessHelper: authInfo.accessHelper,
user: authInfo.user,
userClass: authInfo.userClass,
isImpersonating: !!authInfo.impersonatorUserId,
impersonatorUserId: authInfo.impersonatorUserId,
refreshAuthInfo,
tokens,
accessTokenExpiresAtSeconds: authInfo.expiresAtSeconds
};
}
return {
loading: false,
isLoggedIn: false,
accessToken: null,
user: null,
userClass: null,
orgHelper: null,
accessHelper: null,
isImpersonating: false,
impersonatorUserId: undefined,
refreshAuthInfo,
tokens,
accessTokenExpiresAtSeconds: undefined
};
}
function useOrgHelper() {
const authInfo = useAuthInfo();
if (authInfo.loading) {
return {
loading: true,
orgHelper: null
};
} else if (authInfo.isLoggedIn) {
return {
loading: false,
orgHelper: authInfo.orgHelper
};
} else {
return {
loading: false,
orgHelper: null
};
}
}
function useAccessHelper() {
const authInfo = useAuthInfo();
if (authInfo.loading) {
return {
loading: true,
accessHelper: null
};
} else if (authInfo.isLoggedIn) {
return {
loading: false,
accessHelper: authInfo.accessHelper
};
} else {
return {
loading: false,
accessHelper: null
};
}
}
function useAuthUrl() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuthUrl must be used within an AuthProvider or RequiredAuthProvider");
}
const {
authUrl
} = context;
return authUrl;
}
function useHostedPageUrls() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useHostedPageUrls must be used within an AuthProvider or RequiredAuthProvider");
}
const {
getLoginPageUrl,
getSignupPageUrl,
getAccountPageUrl,
getOrgPageUrl,
getCreateOrgPageUrl,
getSetupSAMLPageUrl
} = context;
return {
getLoginPageUrl,
getSignupPageUrl,
getAccountPageUrl,
getOrgPageUrl,
getCreateOrgPageUrl,
getSetupSAMLPageUrl
};
}
function useLogoutFunction() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useLogoutFunction must be used within an AuthProvider or RequiredAuthProvider");
}
const {