@trap_stevo/legendarybuilderproreact-ui
Version:
The legendary UI & utility API that makes your application a legendary application. ~ Created by Steven Compton
577 lines (563 loc) • 23.1 kB
JavaScript
import _typeof from "@babel/runtime/helpers/typeof";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _regeneratorRuntime from "@babel/runtime/regenerator";
/*
Created by Hassan Steven Compton.
March 7, 2024.
*/
import React, { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback } from "react";
var AuthContext = /*#__PURE__*/createContext();
function getCryptoKey(_x, _x2, _x3) {
return _getCryptoKey.apply(this, arguments);
}
function _getCryptoKey() {
_getCryptoKey = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(seed, offset, depth) {
var enc, keyMaterial;
return _regeneratorRuntime.wrap(function _callee3$(_context3) {
while (1) switch (_context3.prev = _context3.next) {
case 0:
enc = new TextEncoder();
_context3.next = 3;
return crypto.subtle.importKey("raw", enc.encode(seed), "PBKDF2", false, ["deriveKey"]);
case 3:
keyMaterial = _context3.sent;
return _context3.abrupt("return", crypto.subtle.deriveKey({
name: "PBKDF2",
salt: enc.encode(offset),
iterations: depth,
hash: "SHA-256"
}, keyMaterial, {
name: "AES-GCM",
length: 256
}, false, ["encrypt", "decrypt"]));
case 5:
case "end":
return _context3.stop();
}
}, _callee3);
}));
return _getCryptoKey.apply(this, arguments);
}
;
function encryptData(_x4, _x5, _x6, _x7) {
return _encryptData.apply(this, arguments);
}
function _encryptData() {
_encryptData = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4(data, seed, offset, depth) {
var key, iv, encoded, ciphertext, full;
return _regeneratorRuntime.wrap(function _callee4$(_context4) {
while (1) switch (_context4.prev = _context4.next) {
case 0:
_context4.next = 2;
return getCryptoKey(seed, offset, depth);
case 2:
key = _context4.sent;
iv = crypto.getRandomValues(new Uint8Array(12));
encoded = new TextEncoder().encode(JSON.stringify(data));
_context4.next = 7;
return crypto.subtle.encrypt({
name: "AES-GCM",
iv: iv
}, key, encoded);
case 7:
ciphertext = _context4.sent;
full = new Uint8Array(iv.length + ciphertext.byteLength);
full.set(iv);
full.set(new Uint8Array(ciphertext), iv.length);
return _context4.abrupt("return", btoa(String.fromCharCode.apply(String, _toConsumableArray(full))));
case 12:
case "end":
return _context4.stop();
}
}, _callee4);
}));
return _encryptData.apply(this, arguments);
}
;
function decryptData(_x8, _x9, _x10, _x11) {
return _decryptData.apply(this, arguments);
}
function _decryptData() {
_decryptData = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee5(base64, seed, offset, depth) {
var binary, bytes, iv, ciphertext, key, decrypted;
return _regeneratorRuntime.wrap(function _callee5$(_context5) {
while (1) switch (_context5.prev = _context5.next) {
case 0:
binary = atob(base64);
bytes = Uint8Array.from(binary, function (_char) {
return _char.charCodeAt(0);
});
iv = bytes.slice(0, 12);
ciphertext = bytes.slice(12);
_context5.next = 6;
return getCryptoKey(seed, offset, depth);
case 6:
key = _context5.sent;
_context5.next = 9;
return crypto.subtle.decrypt({
name: "AES-GCM",
iv: iv
}, key, ciphertext);
case 9:
decrypted = _context5.sent;
return _context5.abrupt("return", JSON.parse(new TextDecoder().decode(decrypted)));
case 11:
case "end":
return _context5.stop();
}
}, _callee5);
}));
return _decryptData.apply(this, arguments);
}
;
function generateIdentityKey() {
var length = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 32;
var array = new Uint8Array(length);
crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(String, _toConsumableArray(array))).slice(0, length);
}
;
/**
* AuthProvider provides user authentication encapsulation within an application;
* keeping track of user credentials and user authenticated state.
*
* @component
* @param {boolean} [saveSessionCacheOnTimeout=false] - Determines whether to save session cache on timeout.
* @param {number} [signedSessionTimeout=18000] - Session timeout in seconds. Default is 5 hours.
* @param {number} [sessionCheckInterval=1000] - How often (in milliseconds) to check for session expiration.
* @param {Array} [keysToClearOnTimeout=[]] - List of keys to clear from session cache on timeout.
* @param {boolean} [autoSignIn=false] - Determines whether to automatically sign in the user if session data is found in localStorage.
* @param {boolean} [useAbsoluteExpiration=false] - Whether to use absolute expiration timestamp instead of session duration.
* @param {string} [signedUserExpirationKey=null] - Key in signedUser object for token-based absolute expiration (e.g. "expiresAt").
* @param {number} [sessionWarningThreshold=300] - Time (in seconds) to trigger onSessionTimeWarning.
* @param {function} [onSessionTimeWarning] - Callback triggered when time left is under threshold.
* @param {boolean} [encryptAuthIdentity=false] - Enables persistent encryption of session state.
* @param {number} [identityDepth=100000] - PBKDF2 iteration depth for deriving crypto key.
* @param {string} [identitySeed] - Optional passphrase for key derivation.
* @param {string} [identityOffset] - Optional salt for key derivation.
* @param {React.ReactNode} children - The content wrapped by this component.
* @returns {JSX.Element} The AuthProvider component encapsulating children with authentication context.
*/
export var AuthProvider = function AuthProvider(_ref) {
var _ref$saveSessionCache = _ref.saveSessionCacheOnTimeout,
saveSessionCacheOnTimeout = _ref$saveSessionCache === void 0 ? false : _ref$saveSessionCache,
_ref$signedSessionTim = _ref.signedSessionTimeout,
signedSessionTimeout = _ref$signedSessionTim === void 0 ? 18000 : _ref$signedSessionTim,
_ref$sessionCheckInte = _ref.sessionCheckInterval,
sessionCheckInterval = _ref$sessionCheckInte === void 0 ? 1000 : _ref$sessionCheckInte,
_ref$keysToClearOnTim = _ref.keysToClearOnTimeout,
keysToClearOnTimeout = _ref$keysToClearOnTim === void 0 ? [] : _ref$keysToClearOnTim,
_ref$autoSignIn = _ref.autoSignIn,
autoSignIn = _ref$autoSignIn === void 0 ? false : _ref$autoSignIn,
_ref$useAbsoluteExpir = _ref.useAbsoluteExpiration,
useAbsoluteExpiration = _ref$useAbsoluteExpir === void 0 ? false : _ref$useAbsoluteExpir,
_ref$signedUserExpira = _ref.signedUserExpirationKey,
signedUserExpirationKey = _ref$signedUserExpira === void 0 ? null : _ref$signedUserExpira,
_ref$sessionWarningTh = _ref.sessionWarningThreshold,
sessionWarningThreshold = _ref$sessionWarningTh === void 0 ? 300 : _ref$sessionWarningTh,
_ref$onSessionTimeWar = _ref.onSessionTimeWarning,
onSessionTimeWarning = _ref$onSessionTimeWar === void 0 ? null : _ref$onSessionTimeWar,
_ref$onEncryptionErro = _ref.onEncryptionError,
onEncryptionError = _ref$onEncryptionErro === void 0 ? null : _ref$onEncryptionErro,
_ref$encryptAuthIdent = _ref.encryptAuthIdentity,
encryptAuthIdentity = _ref$encryptAuthIdent === void 0 ? false : _ref$encryptAuthIdent,
_ref$identityDepth = _ref.identityDepth,
identityDepth = _ref$identityDepth === void 0 ? 100000 : _ref$identityDepth,
identitySeed = _ref.identitySeed,
identityOffset = _ref.identityOffset,
children = _ref.children;
var resolvedSeed = useMemo(function () {
if (identitySeed) {
return identitySeed;
}
var existing = localStorage.getItem("_session_identity_seed");
if (existing) {
return existing;
}
var key = generateIdentityKey();
localStorage.setItem("_session_identity_seed", key);
return key;
}, [identitySeed]);
var resolvedOffset = useMemo(function () {
if (identityOffset) {
return identityOffset;
}
var existing = localStorage.getItem("_session_identity_offset");
if (existing) {
return existing;
}
var key = generateIdentityKey();
localStorage.setItem("_session_identity_offset", key);
return key;
}, [identityOffset]);
/**
* Initializes the current signed state from cache.
*
* @function
* @name currentSignedState
* @returns {Object} The current signed user and signed-in status.
*/
var currentSignedState = useCallback(function () {
var savedUser = localStorage.getItem("SignedUserCredential");
var userSignedIn = localStorage.getItem("UserSignedIn");
return {
currentSignedUser: savedUser,
currentSignedIn: userSignedIn ? JSON.parse(userSignedIn) : false
};
}, []);
/**
* Responsible for the user's credentials.
*
* @type {Object}
*/
var _useState = useState({}),
_useState2 = _slicedToArray(_useState, 2),
signedUser = _useState2[0],
setSignedUser = _useState2[1];
/**
* Allows for checking the user's authentication state.
*
* @type {boolean}
*/
var _useState3 = useState(function () {
return currentSignedState().currentSignedIn;
}),
_useState4 = _slicedToArray(_useState3, 2),
signedIn = _useState4[0],
setSignedIn = _useState4[1];
var sessionInitialized = useRef(false);
/**
* Synchronizes the current state to the cache.
*
* @function
* @name syncStateToCache
* @param {Object} user - The user's credentials.
* @param {boolean} signedIn - The user's signed-in status.
*/
var syncStateToCache = useCallback( /*#__PURE__*/function () {
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(user, signedIn) {
var now, encrypted, expiresAt;
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
now = Date.now();
if (!(encryptAuthIdentity && resolvedSeed && resolvedOffset)) {
_context.next = 14;
break;
}
_context.prev = 2;
_context.next = 5;
return encryptData(user, resolvedSeed, resolvedOffset, identityDepth);
case 5:
encrypted = _context.sent;
localStorage.setItem("SignedUserCredential", encrypted);
_context.next = 12;
break;
case 9:
_context.prev = 9;
_context.t0 = _context["catch"](2);
if (onEncryptionError) {
onEncryptionError(_context.t0);
}
case 12:
_context.next = 15;
break;
case 14:
localStorage.setItem("SignedUserCredential", JSON.stringify(user));
case 15:
localStorage.setItem("UserSignedIn", JSON.stringify(signedIn));
localStorage.setItem("SignedSessionStart", now.toString());
localStorage.setItem("SignedSessionTimeout", signedSessionTimeout.toString());
if (!useAbsoluteExpiration) {
_context.next = 22;
break;
}
expiresAt = now + signedSessionTimeout * 1000;
localStorage.setItem("SignedSessionExpiresAt", expiresAt.toString());
return _context.abrupt("return");
case 22:
if (!localStorage.getItem("SignedSessionStart")) {
localStorage.setItem("SignedSessionStart", now.toString());
}
case 23:
case "end":
return _context.stop();
}
}, _callee, null, [[2, 9]]);
}));
return function (_x12, _x13) {
return _ref2.apply(this, arguments);
};
}(), [encryptAuthIdentity, onEncryptionError, resolvedSeed, resolvedOffset, identityDepth, signedSessionTimeout, useAbsoluteExpiration]);
/**
* Signs in user.
*
* @function
* @name signIn
* @description Sets the user's authentication state to true.
*/
var signIn = useCallback(function () {
setSignedIn(true);
}, []);
/**
* Signs out user.
*
* @function
* @name signOut
* @param {boolean} [saveSessionCache=false] - Determines whether to clear the user's saved session cache on sign out.
* @param {Array} [keysToClear=[]] - List of keys to clear from session cache on sign out.
* @description Sets the user's authentication state to false.
*/
var signOut = useCallback(function () {
var saveSessionCache = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var keysToClear = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
setSignedIn(false);
setSignedUser({});
localStorage.removeItem("SignedUserCredential");
localStorage.removeItem("UserSignedIn");
localStorage.removeItem("SignedSessionStart");
localStorage.removeItem("SignedSessionExpiresAt");
localStorage.removeItem("SignedSessionTimeout");
if (!saveSessionCache) {
if (keysToClear.length > 0) {
var sessionCache = JSON.parse(localStorage.getItem("HUDSessionCache")) || {};
keysToClear.forEach(function (key) {
return delete sessionCache[key];
});
localStorage.setItem("HUDSessionCache", JSON.stringify(sessionCache));
} else {
localStorage.removeItem("HUDSessionCache");
}
}
}, []);
/**
* Checks if the user authentication session timed out.
*
* @function
* @name checkSignedSessionTimeout
*/
var checkSignedSessionTimeout = useCallback(function () {
var now = Date.now();
var expiresAt = null;
if (signedUserExpirationKey && signedUser !== null && signedUser !== void 0 && signedUser[signedUserExpirationKey]) {
expiresAt = parseInt(signedUser[signedUserExpirationKey], 10);
} else if (useAbsoluteExpiration) {
var stored = parseInt(localStorage.getItem("SignedSessionExpiresAt"), 10);
if (!isNaN(stored)) {
expiresAt = stored;
}
} else {
var sessionStart = parseInt(localStorage.getItem("SignedSessionStart"), 10);
var timeout = parseInt(localStorage.getItem("SignedSessionTimeout"), 10) || signedSessionTimeout;
if (!isNaN(sessionStart)) {
expiresAt = sessionStart + timeout * 1000;
}
}
if (expiresAt) {
var timeLeft = Math.max(0, Math.floor((expiresAt - now) / 1000));
if (timeLeft <= 0) {
signOut(saveSessionCacheOnTimeout, keysToClearOnTimeout);
} else if (onSessionTimeWarning && timeLeft <= sessionWarningThreshold) {
onSessionTimeWarning(timeLeft);
}
}
}, [signedUser, signedUserExpirationKey, useAbsoluteExpiration, signedSessionTimeout, signOut, saveSessionCacheOnTimeout, keysToClearOnTimeout, onSessionTimeWarning, sessionWarningThreshold]);
useEffect(function () {
if (!autoSignIn) {
return;
}
var _currentSignedState = currentSignedState(),
currentSignedUser = _currentSignedState.currentSignedUser,
currentSignedIn = _currentSignedState.currentSignedIn;
_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
var parsed;
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
_context2.prev = 0;
if (!encryptAuthIdentity) {
_context2.next = 7;
break;
}
_context2.next = 4;
return decryptData(currentSignedUser, resolvedSeed, resolvedOffset, identityDepth);
case 4:
_context2.t0 = _context2.sent;
_context2.next = 8;
break;
case 7:
_context2.t0 = JSON.parse(currentSignedUser);
case 8:
parsed = _context2.t0;
if (parsed && _typeof(parsed) === "object") {
setSignedUser(parsed);
setSignedIn(currentSignedIn);
sessionInitialized.current = true;
}
_context2.next = 17;
break;
case 12:
_context2.prev = 12;
_context2.t1 = _context2["catch"](0);
setSignedUser({});
setSignedIn(false);
sessionInitialized.current = false;
case 17:
case "end":
return _context2.stop();
}
}, _callee2, null, [[0, 12]]);
}))();
}, [autoSignIn, encryptAuthIdentity, resolvedSeed, resolvedOffset, identityDepth]);
useEffect(function () {
if (sessionInitialized.current) {
return;
}
if (signedIn && signedUser && Object.keys(signedUser).length > 0) {
syncStateToCache(signedUser, true);
}
}, [sessionInitialized.current, signedIn, signedUser, syncStateToCache]);
useEffect(function () {
checkSignedSessionTimeout();
var interval = setInterval(checkSignedSessionTimeout, sessionCheckInterval);
return function () {
return clearInterval(interval);
};
}, [signedIn, checkSignedSessionTimeout, sessionCheckInterval]);
return /*#__PURE__*/React.createElement(AuthContext.Provider, {
value: {
signedUser: signedUser,
setSignedUser: setSignedUser,
signedIn: signedIn,
signIn: signIn,
signOut: signOut
}
}, children);
};
/**
* Allows access to the signed user's credentials and provides functions to checking the user's authentication state to signing a user out.
* This component allows access to signedUser, setSignedUser, signedIn, signIn, signOut.
*
* @hook
* @returns {Object} The authentication context including user state and auth functions.
* @example
* const { signedUser, signedIn, signIn, signOut } = useAuth();
*/
export var useAuth = function useAuth() {
var context = useContext(AuthContext);
if (!context) {
throw new Error("Must use within an AuthProvider");
}
return context;
};
/**
* useAuthSessionMonitor provides real-time monitoring of the current session.
*
* @hook
* @param {Object} options
* @param {number} [sessionTimeout] - Override session timeout in seconds. If not provided, uses AuthProvider fallback.
* @param {boolean} [useAbsoluteExpiration=false] - Whether to use localStorage "SignedSessionExpiresAt".
* @param {string|null} [signedUserExpirationKey=null] - Key from signedUser object for expiration.
* @param {function} [onExpire] - Called once when session expires.
* @param {function} [onTimeUpdate] - Called on each timeLeft update.
* @param {function} [onWarning] - Called once when timeLeft drops below threshold.
* @param {number} [warningThreshold=300] - Seconds before expiration to trigger onWarning.
* @returns {{ timeLeft: number, expiresAt: number|null, expired: boolean }}
*/
export function useAuthSessionMonitor() {
var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
sessionTimeout = _ref4.sessionTimeout,
_ref4$useAbsoluteExpi = _ref4.useAbsoluteExpiration,
useAbsoluteExpiration = _ref4$useAbsoluteExpi === void 0 ? false : _ref4$useAbsoluteExpi,
_ref4$signedUserExpir = _ref4.signedUserExpirationKey,
signedUserExpirationKey = _ref4$signedUserExpir === void 0 ? null : _ref4$signedUserExpir,
onExpire = _ref4.onExpire,
onTimeUpdate = _ref4.onTimeUpdate,
onWarning = _ref4.onWarning,
_ref4$warningThreshol = _ref4.warningThreshold,
warningThreshold = _ref4$warningThreshol === void 0 ? 300 : _ref4$warningThreshol;
var _useAuth = useAuth(),
signedUser = _useAuth.signedUser,
signedIn = _useAuth.signedIn;
var _useState5 = useState(null),
_useState6 = _slicedToArray(_useState5, 2),
timeLeft = _useState6[0],
setTimeLeft = _useState6[1];
var _useState7 = useState(null),
_useState8 = _slicedToArray(_useState7, 2),
expiresAt = _useState8[0],
setExpiresAt = _useState8[1];
var _useState9 = useState(false),
_useState10 = _slicedToArray(_useState9, 2),
warned = _useState10[0],
setWarned = _useState10[1];
var _useState11 = useState(false),
_useState12 = _slicedToArray(_useState11, 2),
expired = _useState12[0],
setExpired = _useState12[1];
useEffect(function () {
var interval = setInterval(function () {
if (!signedIn) {
setTimeLeft(null);
setExpiresAt(null);
setWarned(false);
setExpired(false);
return;
}
var now = Date.now();
var resolvedExpiresAt = null;
if (signedUserExpirationKey && signedUser !== null && signedUser !== void 0 && signedUser[signedUserExpirationKey]) {
resolvedExpiresAt = parseInt(signedUser[signedUserExpirationKey], 10);
} else if (useAbsoluteExpiration) {
var stored = localStorage.getItem("SignedSessionExpiresAt");
if (stored) {
resolvedExpiresAt = parseInt(stored, 10);
}
} else {
var _ref5;
var start = parseInt(localStorage.getItem("SignedSessionStart"), 10);
var timeout = (_ref5 = sessionTimeout !== null && sessionTimeout !== void 0 ? sessionTimeout : parseInt(localStorage.getItem("SignedSessionTimeout"), 10)) !== null && _ref5 !== void 0 ? _ref5 : 18000;
if (!isNaN(start)) {
resolvedExpiresAt = start + timeout * 1000;
}
}
if (resolvedExpiresAt) {
var left = Math.max(0, Math.floor((resolvedExpiresAt - now) / 1000));
setTimeLeft(left);
setExpiresAt(resolvedExpiresAt);
if (typeof onTimeUpdate === "function") {
onTimeUpdate(left);
}
if (left <= 0 && !expired) {
setExpired(true);
if (typeof onExpire === "function") {
onExpire();
}
}
if (left <= warningThreshold && !warned) {
setWarned(true);
if (typeof onWarning === "function") {
onWarning(left);
}
}
if (left > warningThreshold) {
setWarned(false);
}
} else {
setTimeLeft(null);
setExpiresAt(null);
}
}, 1000);
return function () {
return clearInterval(interval);
};
}, [signedUser, signedIn, sessionTimeout, signedUserExpirationKey, useAbsoluteExpiration, onExpire, onTimeUpdate, onWarning, warningThreshold, warned, expired]);
return {
timeLeft: timeLeft,
expiresAt: expiresAt,
expired: typeof timeLeft === "number" ? timeLeft <= 0 : false
};
}
;