nexus-react-core
Version:
A comprehensive React toolkit with services, hooks, and Redux store management
472 lines • 18.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.publicApiCall = exports.getStreakMultiplier = exports.getStreakCalendar = exports.deleteNotification = exports.markNotificationAsRead = exports.markAllNotificationsAsRead = exports.updateStreak = exports.fetchStreakStatus = exports.handleUserLogin = exports.getLatestNotificationsWithRefresh = exports.fetchUserNotifications = exports.getUserMongoId = exports.checkUserRole = exports.apiCall = exports.setRouter = void 0;
const config_1 = require("../config");
const csrfUtils_1 = require("../utils/csrfUtils");
const axios_1 = __importDefault(require("axios"));
let storage;
if (typeof window !== 'undefined' && window.localStorage) {
// Web environment
storage = {
getItem: async (key) => localStorage.getItem(key),
setItem: async (key, value) => localStorage.setItem(key, value),
removeItem: async (key) => localStorage.removeItem(key),
};
}
else {
// React Native
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
storage = {
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: (key) => AsyncStorage.removeItem(key),
};
}
let routerInstance = null;
// Function to set the router instance
const setRouter = (router) => {
routerInstance = router;
};
exports.setRouter = setRouter;
/**
* Makes API calls with automatic token refresh
* @param endpoint API endpoint (without base URL)
* @param method HTTP method
* @param data Request data (optional)
* @param headers Additional headers (optional)
*/
const apiCall = async (endpoint, method = "GET", data, headers) => {
const config = (0, config_1.getConfig)();
// Get current tokens
const accessToken = await storage.getItem("accessToken");
// Create request config with type assertion
const requestConfig = {
method,
url: `${config.apiBaseUrl}${endpoint}`,
headers: {
"Content-Type": "application/json",
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
...headers,
},
...(method === "GET" ? { params: data } : { data }),
withCredentials: true, // Always include credentials for CSRF cookies
};
// Add CSRF token to headers
try {
await (0, csrfUtils_1.fetchCsrfToken)();
}
catch (csrfError) {
console.warn("Failed to fetch CSRF token for login, proceeding anyway:", csrfError);
// Continue without CSRF token as a fallback
}
try {
// Make the API call
console.log(`Making ${method} request to ${endpoint}`);
const response = await (0, axios_1.default)(requestConfig);
return response.data;
}
catch (error) {
// Check status and response for better error handling
const status = error.response?.status;
const errorMessage = error.response?.data?.message || error.message;
console.log(`API call error: ${status} - ${errorMessage}`, {
endpoint,
method,
status,
message: errorMessage,
data: error.response?.data,
});
// Handle CSRF token errors (403 status may indicate CSRF token expiry)
if (status === 403 &&
(errorMessage?.includes("CSRF") ||
errorMessage?.includes("csrf") ||
errorMessage?.includes("token"))) {
console.log("CSRF token invalid or expired, refreshing...");
try {
// Try to refresh the CSRF token
await (0, csrfUtils_1.fetchCsrfToken)();
// Retry the request with the new token
requestConfig.headers = await (0, csrfUtils_1.addCsrfToken)(requestConfig.headers);
console.log("Retrying request with new CSRF token");
const retryResponse = await (0, axios_1.default)(requestConfig);
return retryResponse.data;
}
catch (csrfError) {
console.error("Failed to refresh CSRF token:", csrfError);
// Continue with normal error handling
}
}
// Handle 401 Unauthorized errors (expired access token)
if (status === 401) {
try {
// Try to refresh the token
const refreshToken = await storage.getItem("refreshToken");
if (!refreshToken) {
// No refresh token available, redirect to login
if (routerInstance) {
routerInstance.push("/login");
}
throw new Error("Authentication required");
}
console.log("Access token expired, refreshing...");
// Call refresh token endpoint
const refreshResponse = await axios_1.default.post(`${config.apiBaseUrl}/auth/refresh-token`, { refreshToken }, {
headers: {
"Content-Type": "application/json",
},
withCredentials: true, // Include cookies for CSRF
});
// Save new tokens
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = refreshResponse.data;
await storage.setItem("accessToken", newAccessToken);
if (newRefreshToken) {
await storage.setItem("refreshToken", newRefreshToken);
}
console.log("Token refreshed successfully");
// Retry the original request with new token
const retryConfig = {
...requestConfig,
headers: {
...requestConfig.headers,
Authorization: `Bearer ${newAccessToken}`,
},
};
// Ensure CSRF token is still included
retryConfig.headers = await (0, csrfUtils_1.addCsrfToken)(retryConfig.headers);
console.log("Retrying original request with new access token");
const retryResponse = await (0, axios_1.default)(retryConfig);
return retryResponse.data;
}
catch (refreshError) {
// Refresh token is invalid or expired
console.error("Failed to refresh access token:", refreshError);
await storage.removeItem("accessToken");
await storage.removeItem("refreshToken");
(0, csrfUtils_1.clearCsrfToken)(); // Clear CSRF token on logout
if (routerInstance) {
routerInstance.push("/login");
}
throw new Error("Session expired. Please login again.");
}
}
// Enhanced error object with more context
const enhancedError = new Error(`${errorMessage || "Unknown error"}`);
// @ts-ignore - Add additional properties to the error object
enhancedError.originalError = error;
// @ts-ignore
enhancedError.requestInfo = { endpoint, method, data };
// @ts-ignore
enhancedError.responseInfo = {
status: error.response?.status,
data: error.response?.data,
};
throw enhancedError;
}
};
exports.apiCall = apiCall;
/**
* Utility function to check if user has a specific role
* @param roleToCheck The role to check against
* @returns boolean indicating if user has the specified role
*/
const checkUserRole = async (roleToCheck) => {
const storedRole = await storage.getItem('userRole');
return storedRole === roleToCheck;
};
exports.checkUserRole = checkUserRole;
/**
* Utility function to get the user's MongoDB ID
*/
const getUserMongoId = async () => {
try {
// Check directly stored MongoDB ID first
const storedMongoId = await storage.getItem('userMongoId');
if (storedMongoId && storedMongoId.length > 0 && !storedMongoId.startsWith('0x')) {
return storedMongoId;
}
// Try userData
const userDataString = await storage.getItem('userData');
if (userDataString) {
try {
const userData = JSON.parse(userDataString);
if (userData._id && typeof userData._id === 'string' && !userData._id.startsWith('0x')) {
return userData._id;
}
if (userData.id && typeof userData.id === 'string' && !userData.id.startsWith('0x')) {
return userData.id;
}
}
catch (e) { }
}
// Try JWT token as last resort
const token = await storage.getItem('accessToken');
if (token && token.split('.').length === 3) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const payload = JSON.parse(jsonPayload);
if (payload.userId && !payload.userId.startsWith('0x'))
return payload.userId;
if (payload._id && !payload._id.startsWith('0x'))
return payload._id;
}
catch (e) { }
}
return '';
}
catch (error) {
return '';
}
};
exports.getUserMongoId = getUserMongoId;
/**
* Fetches notifications for a user
*/
const fetchUserNotifications = async (userId, forceRefresh = false) => {
if (!userId)
return { notifications: [] };
try {
const response = await (0, exports.apiCall)('/notifications', 'GET');
return { notifications: response.data || [] };
}
catch (error) {
console.error('Error fetching notifications:', error);
return { notifications: [] };
}
};
exports.fetchUserNotifications = fetchUserNotifications;
/**
* Fetches latest notifications
*/
const getLatestNotificationsWithRefresh = async (userId) => {
if (!userId)
return { notifications: [] };
try {
const response = await (0, exports.apiCall)('/notifications', 'GET');
return { notifications: response.data || [] };
}
catch (error) {
console.error('Error fetching latest notifications:', error);
return { notifications: [] };
}
};
exports.getLatestNotificationsWithRefresh = getLatestNotificationsWithRefresh;
/**
* Add a function to handle user login that will initialize the socket for notifications
*/
const handleUserLogin = async (userData) => {
try {
const userId = userData._id || userData.id || userData.userId || userData.walletAddress;
if (userId) {
console.log('Handling user login, initializing shared notification socket for user:', userId);
// Store MongoDB ID if available
if (userData._id && !userData._id.startsWith('0x')) {
await storage.setItem('userMongoId', userData._id);
}
}
}
catch (error) {
console.error('Error handling user login:', error);
}
};
exports.handleUserLogin = handleUserLogin;
/**
* Fetches the current streak status for a user
*/
const fetchStreakStatus = async (userId) => {
if (!userId) {
return {
currentStreak: 0,
longestStreak: 0,
multiplier: 1.0,
isActive: false,
streakDates: [],
lastActivityDate: null,
timeLeftToLoseStreak: null,
streakLossWarning: false
};
}
try {
const response = await (0, exports.apiCall)(`/streaks/status`, 'GET');
const data = response.data;
return {
...data,
streakDates: data.streakDates.map((d) => new Date(d)),
lastActivityDate: data.lastActivityDate ? new Date(data.lastActivityDate) : null,
timeLeftToLoseStreak: data.timeLeftToLoseStreak,
streakLossWarning: data.streakLossWarning
};
}
catch (error) {
console.error('Error fetching streak status:', error);
throw error;
}
};
exports.fetchStreakStatus = fetchStreakStatus;
/**
* Updates the streak for a user
*/
const updateStreak = async (userId) => {
if (!userId) {
throw new Error('User ID is required');
}
try {
const response = await (0, exports.apiCall)(`/streaks/update`, 'POST');
return response.data;
}
catch (error) {
console.error('Error updating streak:', error);
throw error;
}
};
exports.updateStreak = updateStreak;
/**
* Marks all notifications as read for a user
*/
const markAllNotificationsAsRead = async (userId) => {
if (!userId)
return;
try {
await (0, exports.apiCall)('/notifications/mark-all-read', 'PUT');
}
catch (error) {
console.error('Error marking all notifications as read:', error);
}
};
exports.markAllNotificationsAsRead = markAllNotificationsAsRead;
/**
* Marks a specific notification as read
*/
const markNotificationAsRead = async (notificationId, userId) => {
if (!notificationId || !userId)
return;
try {
await (0, exports.apiCall)(`/notifications/${notificationId}/read`, 'PUT');
}
catch (error) {
console.error('Error marking notification as read:', error);
}
};
exports.markNotificationAsRead = markNotificationAsRead;
/**
* Deletes a notification
*/
const deleteNotification = async (notificationId, userId) => {
if (!notificationId || !userId) {
return { success: false, error: 'Missing required parameters' };
}
try {
await (0, exports.apiCall)(`/notifications/${notificationId}`, 'DELETE');
return { success: true };
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error deleting notification'
};
}
};
exports.deleteNotification = deleteNotification;
/**
* Gets the streak calendar for a specific month and year
*/
const getStreakCalendar = async (userId, year, month) => {
if (!userId) {
return [];
}
try {
const response = await (0, exports.apiCall)(`/streaks/calendar?year=${year}&month=${month}`, 'GET');
return response.data.map((d) => new Date(d));
}
catch (error) {
console.error('Error fetching streak calendar:', error);
return [];
}
};
exports.getStreakCalendar = getStreakCalendar;
/**
* Gets the current streak multiplier for a user
*/
const getStreakMultiplier = async (userId) => {
try {
const response = await (0, exports.apiCall)(`/streaks/multiplier/${userId}`, 'GET');
return response.data.multiplier || 1;
}
catch (error) {
console.error('Error fetching streak multiplier:', error);
return 1;
}
};
exports.getStreakMultiplier = getStreakMultiplier;
/**
* Makes API calls that don't require authentication (like login/signup)
* @param endpoint API endpoint (without base URL)
* @param method HTTP method
* @param data Request data (optional)
* @param headers Additional headers (optional)
*/
const publicApiCall = async (endpoint, method = "POST", data, headers) => {
const config = (0, config_1.getConfig)();
try {
// Always fetch a fresh CSRF token before login/register
if (endpoint.includes("/auth/login") ||
endpoint.includes("/auth/register") ||
endpoint.includes("/auth/rebelLogin")) {
console.log("Login/register request detected. Fetching fresh CSRF token...");
try {
await (0, csrfUtils_1.fetchCsrfToken)();
}
catch (csrfError) {
console.warn("Failed to fetch CSRF token for login, proceeding anyway:", csrfError);
// Continue without CSRF token as a fallback
}
}
// Create request config with type assertion
const requestConfig = {
method,
url: `${config.apiBaseUrl}${endpoint}`,
headers: {
"Content-Type": "application/json",
...headers,
},
...(method === "GET" ? { params: data } : { data }),
withCredentials: true, // Always include credentials for CSRF cookies
};
// Add CSRF token to headers
requestConfig.headers = await (0, csrfUtils_1.addCsrfToken)(requestConfig.headers);
console.log(`Making public ${method} request to ${endpoint}`);
const response = await (0, axios_1.default)(requestConfig);
return response.data;
}
catch (error) {
const status = error.response?.status;
const errorMessage = error.response?.data?.message || error.message;
console.log(`Public API call error: ${status} - ${errorMessage}`, {
endpoint,
method,
status,
message: errorMessage,
data: error.response?.data,
});
// Enhanced error object with more context
const enhancedError = new Error(`${errorMessage || "Unknown error"}`);
// @ts-ignore - Add additional properties to the error object
enhancedError.originalError = error;
// @ts-ignore
enhancedError.requestInfo = { endpoint, method, data };
// @ts-ignore
enhancedError.responseInfo = {
status: error.response?.status,
data: error.response?.data,
};
throw enhancedError;
}
};
exports.publicApiCall = publicApiCall;
//# sourceMappingURL=apiCall.js.map