UNPKG

@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
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 }; } ;