UNPKG

nexus-react-core

Version:

A comprehensive React toolkit with services, hooks, and Redux store management

472 lines 18.1 kB
"use strict"; 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