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
JavaScript
;
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