UNPKG

react-cookie-auth

Version:

Authentication library with HTTP-only cookies and Page Visibility API for handling sleep/wake cycles

796 lines (773 loc) 39.8 kB
'use strict'; var toolkit = require('@reduxjs/toolkit'); var react = require('react'); var reactRedux = require('react-redux'); var react$1 = require('@reduxjs/toolkit/query/react'); var jsxRuntime = require('react/jsx-runtime'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * LocalStorage keys used by the authentication library */ var LocalStorageKeys; (function (LocalStorageKeys) { LocalStorageKeys["CURRENT_USER"] = "react_cookie_auth_current_user"; LocalStorageKeys["LAST_REFRESH_TIME"] = "react_cookie_auth_last_refresh_time"; })(LocalStorageKeys || (LocalStorageKeys = {})); /** * Authentication error types */ var AuthErrorType; (function (AuthErrorType) { AuthErrorType["INVALID_CREDENTIALS"] = "invalid_credentials"; AuthErrorType["ACCOUNT_DISABLED"] = "account_disabled"; AuthErrorType["ACCOUNT_LOCKED"] = "account_locked"; AuthErrorType["TOKEN_EXPIRED"] = "token_expired"; AuthErrorType["TOKEN_INVALID"] = "token_invalid"; AuthErrorType["TOKEN_BLACKLISTED"] = "token_blacklisted"; AuthErrorType["SYSTEM_ERROR"] = "system_error"; })(AuthErrorType || (AuthErrorType = {})); /** * Checks if code is running in a browser environment */ var IS_SERVER = typeof window === 'undefined'; /** * Retrieves an item from localStorage and parses it from JSON * * @param key The localStorage key to retrieve * @returns The parsed value, or null if not found or on server */ var getItemFromStorage = function (key) { if (IS_SERVER) { return null; } var item = localStorage.getItem(key); try { return item ? JSON.parse(item) : null; } catch (e) { console.error("Error parsing item from localStorage: ".concat(key), e); return null; } }; /** * Stores an item in localStorage after converting it to JSON * * @param key The localStorage key to store under * @param value The value to store */ var setItemToStorage = function (key, value) { if (IS_SERVER) return; try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.error("Error setting item to localStorage: ".concat(key), e); } }; /** * Removes an item from localStorage * * @param key The localStorage key to remove */ var removeItemFromStorage = function (key) { if (IS_SERVER) return; try { localStorage.removeItem(key); } catch (e) { console.error("Error removing item from localStorage: ".concat(key), e); } }; /** * Clears all authentication-related items from localStorage */ var clearAuthStorage = function () { if (IS_SERVER) return; try { Object.values(LocalStorageKeys).forEach(function (key) { localStorage.removeItem(key); }); } catch (e) { console.error('Error clearing auth storage', e); } }; /** * Initialize user from localStorage if available */ var initialState = { user: getItemFromStorage(LocalStorageKeys.CURRENT_USER) || null, }; /** * Auth slice for managing authentication state */ var authSlice = toolkit.createSlice({ name: 'auth', initialState: initialState, reducers: { /** * Sets the current user * If the user is null, it clears the user (logout) */ setUser: function (state, action) { state.user = action.payload; // Persist user to localStorage, or remove if null if (typeof window !== 'undefined') { if (action.payload) { setItemToStorage(LocalStorageKeys.CURRENT_USER, action.payload); } else { // On logout (user is null), remove both user and lastRefreshTime from localStorage localStorage.removeItem(LocalStorageKeys.CURRENT_USER); localStorage.removeItem(LocalStorageKeys.LAST_REFRESH_TIME); } } }, }, }); // Export actions var setUser = authSlice.actions.setUser; // Export selectors var selectUser = function (state) { return state.auth.user; }; var isAuthenticated = function (state) { return !!state.auth.user; }; // Export reducer with explicit type annotation var authReducer = authSlice.reducer; /** * Default refresh token interval: 10 minutes */ var DEFAULT_REFRESH_TIMEOUT = 1000 * 60 * 10; /** * Random delay range for refresh token after visibility change (in ms) */ var VISIBILITY_CHANGE_MIN_DELAY = 1000; // 1 second var VISIBILITY_CHANGE_MAX_DELAY = 5000; // 5 seconds /** * Helper function to get a random delay between min and max values * Used to prevent refresh token storms when a device wakes up * * @returns Random delay in milliseconds */ var getRandomDelay = function () { return Math.floor(Math.random() * (VISIBILITY_CHANGE_MAX_DELAY - VISIBILITY_CHANGE_MIN_DELAY) + VISIBILITY_CHANGE_MIN_DELAY); }; /** * Helper function for creating a delay promise * * @param ms Number of milliseconds to delay * @returns Promise that resolves after specified delay */ var delay = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); }; /** * Custom hook for handling refresh token operations * * @param refreshTokenMutation The RTK Query mutation for refreshing tokens * @param refreshInterval How often to refresh the token (ms) * @param maxRetryAttempts Maximum number of retry attempts on failure * @param retryDelay Delay between retry attempts (ms) * @returns A function that handles token refresh logic */ var useRefreshToken = function (refreshTokenMutation, refreshInterval, maxRetryAttempts, retryDelay) { if (refreshInterval === void 0) { refreshInterval = DEFAULT_REFRESH_TIMEOUT; } if (maxRetryAttempts === void 0) { maxRetryAttempts = 3; } if (retryDelay === void 0) { retryDelay = 1000; } var dispatch = reactRedux.useDispatch(); var refreshTokenFunction = react.useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { var lastRefreshTime, currentTime, retryCount, success, response, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: lastRefreshTime = getItemFromStorage(LocalStorageKeys.LAST_REFRESH_TIME); currentTime = Date.now(); if (!(!lastRefreshTime || currentTime - lastRefreshTime > refreshInterval * 0.9)) return [3 /*break*/, 10]; retryCount = 0; success = false; _a.label = 1; case 1: if (!(retryCount < maxRetryAttempts && !success)) return [3 /*break*/, 10]; _a.label = 2; case 2: _a.trys.push([2, 4, , 9]); return [4 /*yield*/, refreshTokenMutation().unwrap()]; case 3: response = _a.sent(); // Update the Redux state with the user data from the response if (response && response.user) { dispatch(setUser(response.user)); // Update last refresh time in localStorage setItemToStorage(LocalStorageKeys.LAST_REFRESH_TIME, currentTime); success = true; } return [3 /*break*/, 9]; case 4: error_1 = _a.sent(); if (!('status' in error_1 && error_1.status === 401)) return [3 /*break*/, 5]; // Clear user state on error dispatch(setUser(null)); // setUser(null) already removes lastRefreshTime in authSlice // Don't retry on 401 errors return [3 /*break*/, 10]; case 5: if (!(retryCount < maxRetryAttempts - 1)) return [3 /*break*/, 7]; retryCount++; // Wait before retrying return [4 /*yield*/, delay(retryDelay)]; case 6: // Wait before retrying _a.sent(); return [3 /*break*/, 8]; case 7: console.error('Token refresh failed after maximum retry attempts', error_1); _a.label = 8; case 8: return [3 /*break*/, 9]; case 9: return [3 /*break*/, 1]; case 10: return [2 /*return*/]; } }); }); }, [refreshTokenMutation, dispatch, refreshInterval, maxRetryAttempts, retryDelay]); return refreshTokenFunction; }; /** * Creates an authentication API service with RTK Query * * @param config The authentication library configuration * @returns The configured auth API */ var createAuthApi = function (config) { // Create the base api with the configured base URL var api = react$1.createApi({ reducerPath: 'authApi', baseQuery: react$1.fetchBaseQuery({ baseUrl: config.apiBaseUrl }), endpoints: function (builder) { return ({ login: builder.mutation({ query: function (credentials) { return ({ url: config.loginEndpoint, method: 'POST', body: credentials, }); }, // Set the LAST_REFRESH_TIME on successful login to prevent unnecessary token refresh onQueryStarted: function (_, _a) { var queryFulfilled = _a.queryFulfilled; return __awaiter(void 0, void 0, void 0, function () { var result, error_1; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 3, , 4]); return [4 /*yield*/, queryFulfilled]; case 1: _b.sent(); // Set the current time as the last refresh time setItemToStorage(LocalStorageKeys.LAST_REFRESH_TIME, Date.now()); return [4 /*yield*/, queryFulfilled]; case 2: result = _b.sent(); if (config.onLoginSuccess && result.data.user) { config.onLoginSuccess(result.data.user); } return [3 /*break*/, 4]; case 3: error_1 = _b.sent(); // Login failed, call the error callback if provided if (config.onAuthError) { config.onAuthError(error_1); } return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }, }), refreshToken: builder.mutation({ query: function () { return ({ url: config.refreshTokenEndpoint, method: 'POST', // No body needed as refresh token is in the cookies }); }, // The server should return user data alongside new tokens }), logout: builder.mutation({ query: function () { return ({ url: config.logoutEndpoint, method: 'POST', // No body needed as tokens are in the cookies }); }, // Clear all auth-related data on logout onQueryStarted: function (_, _a) { var queryFulfilled = _a.queryFulfilled; return __awaiter(void 0, void 0, void 0, function () { var error_2; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); return [4 /*yield*/, queryFulfilled]; case 1: _b.sent(); // Clear LAST_REFRESH_TIME to ensure consistent cleanup localStorage.removeItem(LocalStorageKeys.LAST_REFRESH_TIME); // Call the optional logout success callback if provided if (config.onLogoutSuccess) { config.onLogoutSuccess(); } return [3 /*break*/, 3]; case 2: error_2 = _b.sent(); // Logout failed, call the error callback if provided if (config.onAuthError) { config.onAuthError(error_2); } return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }, }), }); }, tagTypes: ['Auth'], }); return api; }; /** * Auth component that handles authentication state and refresh token logic * * This component: * 1. Validates authentication status on mount via refresh token * 2. Sets up periodic refresh of auth cookies using Page Visibility API * 3. Prevents interval queuing during device sleep/wake cycles * 4. Calls an optional callback when auth state changes */ var Auth = function (_a) { var children = _a.children, refreshFunction = _a.refreshFunction, refreshInterval = _a.refreshInterval, onAuthStateChange = _a.onAuthStateChange, invalidateAuthTags = _a.invalidateAuthTags; var isUserAuthenticated = reactRedux.useSelector(isAuthenticated); var prevAuthStateRef = react.useRef(isUserAuthenticated); // References for managing intervals and visibility state var refreshTokenIntervalRef = react.useRef(null); var pendingTimeoutRef = react.useRef(null); var wasHiddenRef = react.useRef(false); // Handle authentication state changes react.useEffect(function () { if (isUserAuthenticated !== prevAuthStateRef.current) { prevAuthStateRef.current = isUserAuthenticated; // Notify about auth state change if callback provided if (onAuthStateChange) { onAuthStateChange(isUserAuthenticated); } // Invalidate auth tags if function provided if (invalidateAuthTags) { invalidateAuthTags(); } } }, [isUserAuthenticated, onAuthStateChange, invalidateAuthTags]); // Helper function to clear any existing interval var clearRefreshInterval = function () { if (refreshTokenIntervalRef.current) { clearInterval(refreshTokenIntervalRef.current); refreshTokenIntervalRef.current = null; } }; // Helper function to clear any pending timeout var clearPendingTimeout = function () { if (pendingTimeoutRef.current) { clearTimeout(pendingTimeoutRef.current); pendingTimeoutRef.current = null; } }; // Helper function to set up the refresh interval var setupRefreshInterval = function () { clearRefreshInterval(); refreshTokenIntervalRef.current = window.setInterval(refreshFunction, refreshInterval); }; // Handle page visibility changes to prevent interval queuing during sleep var handleVisibilityChange = function () { if (document.visibilityState === 'hidden') { // Page is hidden (device may sleep) - clear the interval wasHiddenRef.current = true; clearRefreshInterval(); clearPendingTimeout(); } else if (document.visibilityState === 'visible' && isUserAuthenticated) { // Only do the single refresh if we were previously hidden if (wasHiddenRef.current) { // Reset the hidden state wasHiddenRef.current = false; // Clean up any existing intervals and timeouts to prevent duplicates clearRefreshInterval(); clearPendingTimeout(); // Page is visible again - perform a single refresh after random delay // to prevent all tabs from refreshing simultaneously var delayMs = getRandomDelay(); pendingTimeoutRef.current = window.setTimeout(function () { try { // Clear the timeout reference pendingTimeoutRef.current = null; // Perform the refresh operation refreshFunction(); // Then restart the normal interval setupRefreshInterval(); } catch (error) { console.error('Error in refresh token after visibility change:', error); } }, delayMs); } } }; // Set up periodic token refresh check with Page Visibility API react.useEffect(function () { if (isUserAuthenticated) { // Reset the hidden state on mount or auth change wasHiddenRef.current = false; // Initial refresh when component mounts refreshFunction(); // Set up the normal refresh interval setupRefreshInterval(); // Make sure we remove any existing listener before adding a new one document.removeEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange); } else { // Clear the interval if the user is not authenticated clearRefreshInterval(); clearPendingTimeout(); } return function () { // Cleanup on component unmount clearRefreshInterval(); clearPendingTimeout(); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [refreshFunction, isUserAuthenticated, refreshInterval]); return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children }); }; /** * Gets a user-friendly error message based on the authentication error type * * @param errorType The authentication error type * @returns A user-friendly error message */ var getAuthErrorMessage = function (errorType) { switch (errorType) { case AuthErrorType.INVALID_CREDENTIALS: return 'Invalid username or password'; case AuthErrorType.ACCOUNT_DISABLED: return 'This account has been disabled'; case AuthErrorType.ACCOUNT_LOCKED: return 'This account has been locked. Please contact support'; case AuthErrorType.TOKEN_EXPIRED: return 'Your session has expired. Please login again'; case AuthErrorType.TOKEN_INVALID: return 'Invalid authentication token'; case AuthErrorType.TOKEN_BLACKLISTED: return 'This session has been invalidated'; case AuthErrorType.SYSTEM_ERROR: return 'A system error occurred. Please try again later'; default: return 'An unknown authentication error occurred'; } }; /** * Determines if an error is an authentication error * * @param error The error object * @returns True if the error is an authentication error */ var isAuthError = function (error) { if (!error || !error.data || !error.data.error_type) { return false; } return Object.values(AuthErrorType).includes(error.data.error_type); }; /** * Gets the auth error type from an error response * * @param error The error object * @returns The authentication error type or undefined */ var getAuthErrorType = function (error) { if (!error || !error.data || !error.data.error_type) { return undefined; } return error.data.error_type; }; /** * Reusable modal component used by AuthModal and LogoutModal * Uses the HTML dialog element with the showModal API */ function Modal(_a) { var isOpen = _a.isOpen, onClose = _a.onClose, children = _a.children, _b = _a.size, size = _b === void 0 ? 'medium' : _b, _c = _a.className, className = _c === void 0 ? '' : _c; var modalRef = react.useRef(null); react.useEffect(function () { if (!modalRef.current) return; if (isOpen) { modalRef.current.showModal(); } else { modalRef.current.close(); } }, [isOpen]); var modalSizeClasses = { small: 'max-w-sm', medium: 'max-w-md', large: 'max-w-lg', xl: 'max-w-xl', '2xl': 'max-w-2xl', '3xl': 'max-w-3xl', full: 'max-w-full', }; return (jsxRuntime.jsxs("dialog", __assign({ className: "modal", ref: modalRef, onClose: onClose }, { children: [jsxRuntime.jsx("div", __assign({ className: "modal-box ".concat(modalSizeClasses[size], " ").concat(className) }, { children: children })), jsxRuntime.jsx("form", __assign({ method: "dialog", className: "modal-backdrop" }, { children: jsxRuntime.jsx("button", { children: "close" }) }))] }))); } /** * Simple error display component for authentication errors */ var AuthErrorDisplay = function (_a) { var error = _a.error, _b = _a.errorClassName, errorClassName = _b === void 0 ? 'text-red-500 w-full mb-4' : _b; if (!error) return null; var errorData = error === null || error === void 0 ? void 0 : error.data; if (!errorData) return jsxRuntime.jsx("div", __assign({ className: errorClassName }, { children: "Authentication failed" })); var errorMessage = 'Failed to log in'; if (errorData.error_type && Object.values(AuthErrorType).includes(errorData.error_type)) { errorMessage = getAuthErrorMessage(errorData.error_type); } else if (errorData.detail) { errorMessage = errorData.detail; } return jsxRuntime.jsx("div", __assign({ className: errorClassName }, { children: errorMessage })); }; /** * Authentication modal component for login functionality * Configurable via the config prop */ var AuthModal = function (_a) { var isOpen = _a.isOpen, onClose = _a.onClose, useLoginMutation = _a.useLoginMutation, _b = _a.config, config = _b === void 0 ? {} : _b; // Default configuration with fallbacks var _c = config.titleText, titleText = _c === void 0 ? 'Login' : _c, _d = config.usernameLabel, usernameLabel = _d === void 0 ? 'Username' : _d, _e = config.usernamePlaceholder, usernamePlaceholder = _e === void 0 ? 'Enter your username' : _e, _f = config.passwordLabel, passwordLabel = _f === void 0 ? 'Password' : _f, _g = config.passwordPlaceholder, passwordPlaceholder = _g === void 0 ? 'Enter your password' : _g, _h = config.submitButtonText, submitButtonText = _h === void 0 ? 'Login' : _h, _j = config.loadingText, loadingText = _j === void 0 ? 'Loading...' : _j, _k = config.modalSize, modalSize = _k === void 0 ? 'medium' : _k, _l = config.formClassName, formClassName = _l === void 0 ? 'bg-white p-8 rounded-lg' : _l, _m = config.inputClassName, inputClassName = _m === void 0 ? 'border rounded p-2 w-full mt-1 mb-4' : _m, _o = config.buttonClassName, buttonClassName = _o === void 0 ? 'bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded w-full' : _o, _p = config.errorClassName, errorClassName = _p === void 0 ? 'text-red-500 w-full mb-4' : _p, _q = config.showRememberMe, showRememberMe = _q === void 0 ? false : _q, _r = config.rememberMeLabel, rememberMeLabel = _r === void 0 ? 'Remember me' : _r, onLoginSuccess = config.onLoginSuccess; // Component state var _s = react.useState({ username: '', password: '', rememberMe: false, }), formData = _s[0], setFormData = _s[1]; // Redux hooks var dispatch = reactRedux.useDispatch(); var login = useLoginMutation[0], _t = useLoginMutation[1], isLoading = _t.isLoading, error = _t.error; // Form handlers var handleChange = function (e) { var _a = e.target, name = _a.name, value = _a.value; setFormData(function (data) { var _a; return (__assign(__assign({}, data), (_a = {}, _a[name] = value, _a))); }); }; var handleSubmit = function (e) { return __awaiter(void 0, void 0, void 0, function () { var username, password, rememberMe, data, err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: e.preventDefault(); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); username = formData.username, password = formData.password, rememberMe = formData.rememberMe; return [4 /*yield*/, login({ username: username, password: password, remember: rememberMe, // Include remember me flag in the API call }).unwrap()]; case 2: data = _a.sent(); // Update Redux state dispatch(setUser(data.user)); // Close the modal onClose(); // Call success callback if provided if (onLoginSuccess) { onLoginSuccess(data.user.username); } return [3 /*break*/, 4]; case 3: err_1 = _a.sent(); // Error is handled by the AuthErrorDisplay component console.error('Failed to login:', err_1); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; return (jsxRuntime.jsx(Modal, __assign({ isOpen: isOpen, onClose: onClose, size: modalSize }, { children: jsxRuntime.jsxs("div", __assign({ className: formClassName }, { children: [jsxRuntime.jsx("h2", __assign({ className: "text-2xl font-semibold mb-4" }, { children: titleText })), jsxRuntime.jsxs("form", __assign({ onSubmit: handleSubmit }, { children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", __assign({ htmlFor: "username", className: "block text-gray-700" }, { children: usernameLabel })), jsxRuntime.jsx("input", { id: "username", name: "username", className: inputClassName, value: formData.username, placeholder: usernamePlaceholder, onChange: handleChange, required: true })] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", __assign({ htmlFor: "password", className: "block text-gray-700" }, { children: passwordLabel })), jsxRuntime.jsx("input", { type: "password", id: "password", name: "password", value: formData.password, onChange: handleChange, className: inputClassName, placeholder: passwordPlaceholder, required: true })] }), showRememberMe && (jsxRuntime.jsxs("div", __assign({ className: "flex items-center mb-4" }, { children: [jsxRuntime.jsx("input", { type: "checkbox", id: "rememberMe", name: "rememberMe", checked: formData.rememberMe, onChange: function (e) { return setFormData(function (data) { return (__assign(__assign({}, data), { rememberMe: e.target.checked })); }); }, className: "mr-2" }), jsxRuntime.jsx("label", __assign({ htmlFor: "rememberMe", className: "text-gray-700" }, { children: rememberMeLabel }))] }))), error && jsxRuntime.jsx(AuthErrorDisplay, { error: error, errorClassName: errorClassName }), jsxRuntime.jsx("button", __assign({ type: "submit", className: buttonClassName, disabled: isLoading }, { children: isLoading ? loadingText : submitButtonText }))] }))] })) }))); }; /** * Logout confirmation modal component * Configurable via the config prop */ var LogoutModal = function (_a) { var isOpen = _a.isOpen, onClose = _a.onClose, useLogoutMutation = _a.useLogoutMutation, _b = _a.config, config = _b === void 0 ? {} : _b; // Default configuration with fallbacks var _c = config.titleText, titleText = _c === void 0 ? 'Logout' : _c, _d = config.confirmationText, confirmationText = _d === void 0 ? 'Are you sure you want to log out?' : _d, _e = config.confirmButtonText, confirmButtonText = _e === void 0 ? 'Yes' : _e, _f = config.cancelButtonText, cancelButtonText = _f === void 0 ? 'No' : _f, _g = config.loadingText, loadingText = _g === void 0 ? 'Loading...' : _g, errorText = config.errorText, _h = config.modalSize, modalSize = _h === void 0 ? 'small' : _h, _j = config.contentClassName, contentClassName = _j === void 0 ? 'p-6' : _j, _k = config.buttonClassName, buttonClassName = _k === void 0 ? 'bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded' : _k, _l = config.cancelButtonClassName, cancelButtonClassName = _l === void 0 ? 'bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded' : _l, _m = config.confirmButtonClassName, confirmButtonClassName = _m === void 0 ? 'bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded' : _m, _o = config.spinnerClassName, spinnerClassName = _o === void 0 ? 'animate-spin h-6 w-6 border-4 border-blue-500 rounded-full border-t-transparent' : _o, _p = config.headerClassName, headerClassName = _p === void 0 ? '' : _p, onLogoutSuccess = config.onLogoutSuccess, onLogoutError = config.onLogoutError; // Redux hooks var dispatch = reactRedux.useDispatch(); var logout = useLogoutMutation[0], isLoading = useLogoutMutation[1].isLoading; /** * Handle logout action */ var handleLogout = function () { return __awaiter(void 0, void 0, void 0, function () { var error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); // With cookie-based auth, we don't need to pass tokens return [4 /*yield*/, logout().unwrap()]; case 1: // With cookie-based auth, we don't need to pass tokens _a.sent(); // Clear user from Redux state dispatch(setUser(null)); // Close the modal onClose(); // Call success callback if provided if (onLogoutSuccess) { onLogoutSuccess(); } return [3 /*break*/, 3]; case 2: error_1 = _a.sent(); console.error('Failed to logout:', error_1); // Call error callback if provided if (onLogoutError) { onLogoutError(error_1); } // Even if there's an error, still close the modal // to prevent users from being stuck onClose(); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }; return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [isLoading && (jsxRuntime.jsx("div", __assign({ className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" }, { children: jsxRuntime.jsx("div", { className: spinnerClassName }) }))), jsxRuntime.jsx(Modal, __assign({ isOpen: isOpen, onClose: onClose, size: modalSize }, { children: jsxRuntime.jsxs("div", __assign({ className: contentClassName }, { children: [jsxRuntime.jsx("h2", __assign({ className: "text-2xl font-semibold mb-4 ".concat(headerClassName) }, { children: titleText })), jsxRuntime.jsx("p", __assign({ className: "mb-6" }, { children: confirmationText })), errorText && jsxRuntime.jsx("p", __assign({ className: "text-red-500 mb-4" }, { children: errorText })), jsxRuntime.jsxs("div", __assign({ className: "flex justify-between" }, { children: [jsxRuntime.jsx("button", __assign({ onClick: onClose, className: "".concat(buttonClassName, " ").concat(cancelButtonClassName), disabled: isLoading }, { children: cancelButtonText })), jsxRuntime.jsx("button", __assign({ onClick: handleLogout, className: "".concat(buttonClassName, " ").concat(confirmButtonClassName), disabled: isLoading }, { children: isLoading ? loadingText : confirmButtonText }))] }))] })) }))] })); }; /** * Initialize the authentication library with the given configuration * * @param config The configuration for the authentication library * @returns An object containing Redux store, hooks, and API endpoints */ function initAuth(config) { var _a; // Create the auth API with the provided configuration var authApi = createAuthApi(config); // Configure the Redux store var store = toolkit.configureStore({ reducer: (_a = { auth: authReducer }, _a[authApi.reducerPath] = authApi.reducer, _a), middleware: function (getDefaultMiddleware) { return getDefaultMiddleware().concat(authApi.middleware); }, }); // Export hooks and API endpoints for the consuming application return { store: store, hooks: { useRefreshToken: function (refreshTokenMutation) { return useRefreshToken(refreshTokenMutation, config.refreshTokenInterval, config.maxRetryAttempts, config.retryDelay); }, useLoginMutation: authApi.endpoints.login.useMutation, useLogoutMutation: authApi.endpoints.logout.useMutation, useRefreshTokenMutation: authApi.endpoints.refreshToken.useMutation, }, actions: { setUser: setUser, }, selectors: { isAuthenticated: isAuthenticated, getUser: selectUser, }, }; } exports.Auth = Auth; exports.AuthModal = AuthModal; exports.LogoutModal = LogoutModal; exports.Modal = Modal; exports.clearAuthStorage = clearAuthStorage; exports.getAuthErrorMessage = getAuthErrorMessage; exports.getAuthErrorType = getAuthErrorType; exports.getItemFromStorage = getItemFromStorage; exports.initAuth = initAuth; exports.isAuthError = isAuthError; exports.removeItemFromStorage = removeItemFromStorage; exports.setItemToStorage = setItemToStorage; //# sourceMappingURL=index.js.map