diff --git a/index.js b/index.js index ac7b766..cc358ae 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,6 @@ import onMessageReceived from "~/notifications/onMessageReceived"; import { createLogger } from "~/lib/logger"; import * as Sentry from "@sentry/react-native"; import AsyncStorage from "@react-native-async-storage/async-storage"; -import { handleHttpResponse } from "~/lib/geolocation/httpResponseHandler"; // setup notification, this have to stay in index.js notifee.onBackgroundEvent(notificationBackgroundEvent); @@ -34,8 +33,7 @@ registerRootComponent(App); // Constants for persistence const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time"; // const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000; -// const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING -const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING +const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING // Helper functions for persisting sync time const getLastSyncTime = async () => { @@ -331,47 +329,54 @@ const HeadlessTask = async (event) => { break; case "http": - try { - // Use shared HTTP response handler for headless context - await handleHttpResponse(params, "headless"); + // Validate HTTP parameters + if (!params || typeof params !== "object" || !params.response) { + geolocBgLogger.warn("Invalid HTTP params", { params }); + break; + } - // Update last sync time on successful HTTP response (keep existing logic) - const httpStatus = params.status; - if (httpStatus === 200) { - try { - const now = Date.now(); - await setLastSyncTime(now); + const httpStatus = params.response?.status; + const isHttpSuccess = httpStatus === 200; - Sentry.addBreadcrumb({ - message: "Last sync time updated (HTTP success)", - category: "headless-task", - level: "info", - data: { newSyncTime: new Date(now).toISOString() }, - }); - } catch (syncTimeError) { - geolocBgLogger.error("Failed to update sync time", { - error: syncTimeError, - }); + Sentry.addBreadcrumb({ + message: "HTTP response received", + category: "headless-task", + level: isHttpSuccess ? "info" : "warning", + data: { + status: httpStatus, + success: params.response?.success, + hasResponse: !!params.response, + }, + }); - Sentry.captureException(syncTimeError, { - tags: { - module: "headless-task", - operation: "update-sync-time-http", - }, - }); - } + geolocBgLogger.debug("HTTP response received", { + response: params.response, + }); + + // Update last sync time on successful HTTP response + if (isHttpSuccess) { + try { + const now = Date.now(); + await setLastSyncTime(now); + + Sentry.addBreadcrumb({ + message: "Last sync time updated (HTTP success)", + category: "headless-task", + level: "info", + data: { newSyncTime: new Date(now).toISOString() }, + }); + } catch (syncTimeError) { + geolocBgLogger.error("Failed to update sync time", { + error: syncTimeError, + }); + + Sentry.captureException(syncTimeError, { + tags: { + module: "headless-task", + operation: "update-sync-time-http", + }, + }); } - } catch (httpHandlerError) { - geolocBgLogger.error("HTTP response handler failed", { - error: httpHandlerError, - }); - - Sentry.captureException(httpHandlerError, { - tags: { - module: "headless-task", - operation: "http-response-handler", - }, - }); } break; diff --git a/src/lib/geolocation/httpResponseHandler.js b/src/lib/geolocation/httpResponseHandler.js deleted file mode 100644 index 30b03c3..0000000 --- a/src/lib/geolocation/httpResponseHandler.js +++ /dev/null @@ -1,225 +0,0 @@ -import { createLogger } from "~/lib/logger"; -import { BACKGROUND_SCOPES } from "~/lib/logger/scopes"; -import * as Sentry from "@sentry/react-native"; -import BackgroundGeolocation from "react-native-background-geolocation"; -import throttle from "lodash.throttle"; - -import { authActions, getAuthState } from "~/stores"; -import { setLastSyncTime } from "~/lib/geolocation/syncTimeManager"; - -// Create logger for HTTP response handling -const httpLogger = createLogger({ - module: BACKGROUND_SCOPES.GEOLOCATION, - feature: "http-handler", -}); - -// Throttling configuration for auth reload -const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle - -// The core auth reload function that will be throttled -async function _reloadAuth() { - httpLogger.info("Refreshing authentication token via sync endpoint"); - - try { - // Get current auth state to check if we have an auth token - const { authToken, userToken } = getAuthState(); - - if (!authToken) { - httpLogger.warn("No auth token available for refresh"); - return; - } - - httpLogger.debug( - "Auth token available, updating BackgroundGeolocation config", - ); - - // Update BackgroundGeolocation config to include X-Auth-Token header - await BackgroundGeolocation.setConfig({ - headers: { - Authorization: `Bearer ${userToken}`, // Keep existing user token (may be expired) - "X-Auth-Token": authToken, // Add auth token for refresh - }, - }); - - // Trigger sync to refresh token - await BackgroundGeolocation.changePace(true); - await BackgroundGeolocation.sync(); - - httpLogger.info("Token refresh sync triggered successfully"); - } catch (error) { - httpLogger.error("Failed to refresh authentication token", { - error: error.message, - stack: error.stack, - }); - } -} - -// Create throttled version of auth reload with lodash -const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, { - leading: true, - trailing: false, // Prevent trailing calls to avoid duplicate refreshes -}); - -/** - * Shared HTTP response handler for both foreground and headless contexts - * @param {Object} response - The HTTP response object from BackgroundGeolocation - * @param {string} context - Either 'foreground' or 'headless' - */ -export const handleHttpResponse = async (response, context = "foreground") => { - // Log the full response including headers if available - httpLogger.debug("HTTP response received", { - status: response?.status, - success: response?.success, - responseText: response?.responseText, - url: response?.url, - method: response?.method, - isSync: response?.isSync, - context, - requestHeaders: - response?.request?.headers || "Headers not available in response", - }); - - // Add Sentry breadcrumb for HTTP responses - Sentry.addBreadcrumb({ - message: `Background geolocation HTTP response (${context})`, - category: "geolocation-http", - level: response?.status === 200 ? "info" : "warning", - data: { - status: response?.status, - success: response?.success, - url: response?.url, - isSync: response?.isSync, - recordCount: response?.count, - context, - }, - }); - - const statusCode = response?.status; - - // Log status code and response - httpLogger.debug("Processing HTTP response", { - status: statusCode, - responseText: response?.responseText, - context, - }); - - switch (statusCode) { - case 200: - await handleSuccessResponse(response, context); - break; - case 410: - await handleAuthTokenNotFound(response, context); - break; - case 401: - await handleUnauthorized(response, context); - break; - default: - httpLogger.debug("Unhandled HTTP status code", { - status: statusCode, - context, - }); - } -}; - -/** - * Handle successful HTTP response (status 200) - */ -const handleSuccessResponse = async (response, context) => { - try { - const responseBody = response?.responseText - ? JSON.parse(response.responseText) - : null; - - if (responseBody?.userBearerJwt) { - httpLogger.info("Token refresh successful, updating stored token", { - context, - }); - - // Use auth action to update both in-memory and persistent storage - await authActions.setUserToken(responseBody.userBearerJwt); - - // Update BackgroundGeolocation config with new token and remove X-Auth-Token header - await BackgroundGeolocation.setConfig({ - headers: { - Authorization: `Bearer ${responseBody.userBearerJwt}`, - }, - }); - - httpLogger.debug( - "Updated BackgroundGeolocation with refreshed token and removed X-Auth-Token header", - { context }, - ); - - Sentry.addBreadcrumb({ - message: "Token refreshed successfully", - category: "geolocation-auth", - level: "info", - data: { context }, - }); - } - } catch (e) { - httpLogger.debug("Failed to parse successful response", { - error: e.message, - responseText: response?.responseText, - context, - }); - } -}; - -/** - * Handle auth token expired (status 410) - */ -const handleAuthTokenNotFound = async (response, context) => { - httpLogger.info("Auth token not found (410), logging out", { context }); - - Sentry.addBreadcrumb({ - message: "Auth token expired - logging out", - category: "geolocation-auth", - level: "warning", - data: { context }, - }); - - authActions.logout(); -}; - -/** - * Handle unauthorized request (status 401) - */ -const handleUnauthorized = async (response, context) => { - httpLogger.info("Unauthorized (401), attempting to refresh token", { - context, - }); - - // Add more detailed logging of the error response - try { - const errorBody = response?.responseText - ? JSON.parse(response.responseText) - : null; - - httpLogger.debug("Unauthorized error details", { - errorBody, - errorType: errorBody?.error?.type, - errorMessage: errorBody?.error?.message, - errorPath: errorBody?.error?.errors?.[0]?.path, - context, - }); - - Sentry.addBreadcrumb({ - message: "Unauthorized - refreshing token", - category: "geolocation-auth", - level: "warning", - data: { - errorType: errorBody?.error?.type, - errorMessage: errorBody?.error?.message, - context, - }, - }); - } catch (e) { - httpLogger.debug("Failed to parse error response", { - error: e.message, - responseText: response?.responseText, - context, - }); - } - await reloadAuth(); -}; diff --git a/src/lib/geolocation/syncTimeManager.js b/src/lib/geolocation/syncTimeManager.js deleted file mode 100644 index 2fb449f..0000000 --- a/src/lib/geolocation/syncTimeManager.js +++ /dev/null @@ -1,67 +0,0 @@ -import { memoryAsyncStorage } from "~/lib/memoryAsyncStorage"; -import { createLogger } from "~/lib/logger"; -import { BACKGROUND_SCOPES } from "~/lib/logger/scopes"; -import * as Sentry from "@sentry/react-native"; - -// Constants for persistence -const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time"; - -// Create logger for sync time management -const syncTimeLogger = createLogger({ - module: BACKGROUND_SCOPES.GEOLOCATION, - feature: "sync-time", -}); - -/** - * Get the last sync time from storage - * @returns {Promise} The last sync time in milliseconds, or current time if not found - */ -export const getLastSyncTime = async () => { - try { - const value = await memoryAsyncStorage.getItem(LAST_SYNC_TIME_KEY); - const lastSyncTime = value ? parseInt(value, 10) : Date.now(); - - syncTimeLogger.debug("Retrieved last sync time", { - value, - lastSyncTime, - isDefault: !value, - }); - - return lastSyncTime; - } catch (error) { - syncTimeLogger.error("Failed to get last sync time", { - error: error.message, - }); - - Sentry.captureException(error, { - tags: { module: "sync-time-manager", operation: "get-last-sync-time" }, - }); - - // Return current time as fallback - return Date.now(); - } -}; - -/** - * Set the last sync time in storage - * @param {number} time - The sync time in milliseconds - */ -export const setLastSyncTime = async (time) => { - try { - await memoryAsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString()); - - syncTimeLogger.debug("Set last sync time", { - time, - timeISO: new Date(time).toISOString(), - }); - } catch (error) { - syncTimeLogger.error("Failed to set last sync time", { - error: error.message, - time, - }); - - Sentry.captureException(error, { - tags: { module: "sync-time-manager", operation: "set-last-sync-time" }, - }); - } -}; diff --git a/src/lib/memoryAsyncStorage.js b/src/lib/memoryAsyncStorage.js index 038faee..90caee6 100644 --- a/src/lib/memoryAsyncStorage.js +++ b/src/lib/memoryAsyncStorage.js @@ -152,20 +152,19 @@ export const memoryAsyncStorage = { storageLogger.debug("Set in memory cache", { key }); // Try to persist to AsyncStorage - (async () => { - try { - await AsyncStorage.setItem(key, value); - storageLogger.debug("Persisted to AsyncStorage", { key }); - } catch (error) { - storageLogger.warn( - "Failed to persist to AsyncStorage, kept in memory only", - { - key, - error: error.message, - }, - ); - } - })(); + try { + await AsyncStorage.setItem(key, value); + storageLogger.debug("Persisted to AsyncStorage", { key }); + } catch (error) { + storageLogger.warn( + "Failed to persist to AsyncStorage, kept in memory only", + { + key, + error: error.message, + }, + ); + // Continue - value is at least in memory + } }, /** @@ -179,18 +178,16 @@ export const memoryAsyncStorage = { storageLogger.debug("Deleted from memory cache", { key }); // Try to delete from AsyncStorage - (async () => { - try { - await AsyncStorage.removeItem(key); - storageLogger.debug("Deleted from AsyncStorage", { key }); - } catch (error) { - storageLogger.warn("Failed to delete from AsyncStorage", { - key, - error: error.message, - }); - // Continue - at least removed from memory - } - })(); + try { + await AsyncStorage.removeItem(key); + storageLogger.debug("Deleted from AsyncStorage", { key }); + } catch (error) { + storageLogger.warn("Failed to delete from AsyncStorage", { + key, + error: error.message, + }); + // Continue - at least removed from memory + } }, /** @@ -245,16 +242,14 @@ export const memoryAsyncStorage = { storageLogger.info("Cleared memory cache"); // Try to clear AsyncStorage - (async () => { - try { - await AsyncStorage.clear(); - storageLogger.info("Cleared AsyncStorage"); - } catch (error) { - storageLogger.warn("Failed to clear AsyncStorage", { - error: error.message, - }); - } - })(); + try { + await AsyncStorage.clear(); + storageLogger.info("Cleared AsyncStorage"); + } catch (error) { + storageLogger.warn("Failed to clear AsyncStorage", { + error: error.message, + }); + } }, /** diff --git a/src/location/trackLocation.js b/src/location/trackLocation.js index e224dd2..5bab556 100644 --- a/src/location/trackLocation.js +++ b/src/location/trackLocation.js @@ -18,7 +18,6 @@ import { import setLocationState from "~/location/setLocationState"; import { storeLocation } from "~/utils/location/storage"; -import { handleHttpResponse } from "~/lib/geolocation/httpResponseHandler"; import env from "~/env"; @@ -77,6 +76,53 @@ export default async function trackLocation() { isStaging: env.IS_STAGING, }); + // Throttling configuration for auth reload only + const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle + + // The core auth reload function that will be throttled + async function _reloadAuth() { + locationLogger.info("Refreshing authentication token via sync endpoint"); + + try { + // Get current auth state to check if we have an auth token + const { authToken, userToken } = getAuthState(); + + if (!authToken) { + locationLogger.warn("No auth token available for refresh"); + return; + } + + locationLogger.debug( + "Auth token available, updating BackgroundGeolocation config", + ); + + // Update BackgroundGeolocation config to include X-Auth-Token header + await BackgroundGeolocation.setConfig({ + headers: { + Authorization: `Bearer ${userToken}`, // Keep existing user token (may be expired) + "X-Auth-Token": authToken, // Add auth token for refresh + }, + }); + + // Trigger sync to refresh token + await BackgroundGeolocation.changePace(true); + await BackgroundGeolocation.sync(); + + locationLogger.info("Token refresh sync triggered successfully"); + } catch (error) { + locationLogger.error("Failed to refresh authentication token", { + error: error.message, + stack: error.stack, + }); + } + } + + // Create throttled version of auth reload with lodash + const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, { + leading: true, + trailing: false, // Prevent trailing calls to avoid duplicate refreshes + }); + // Handle auth function - no throttling or cooldown async function handleAuth(userToken) { locationLogger.info("Handling auth token update", { @@ -106,6 +152,25 @@ export default async function trackLocation() { }, ); + // Verify the current configuration + try { + const currentConfig = await BackgroundGeolocation.getConfig(); + locationLogger.debug("Current background geolocation config", { + hasHeaders: !!currentConfig.headers, + headerKeys: currentConfig.headers + ? Object.keys(currentConfig.headers) + : [], + authHeader: currentConfig.headers?.Authorization + ? currentConfig.headers.Authorization.substring(0, 15) + "..." + : "Not set", + url: currentConfig.url, + }); + } catch (error) { + locationLogger.error("Failed to get background geolocation config", { + error: error.message, + }); + } + const state = await BackgroundGeolocation.getState(); try { const decodedToken = jwtDecode(userToken); @@ -175,8 +240,132 @@ export default async function trackLocation() { }); BackgroundGeolocation.onHttp(async (response) => { - // Use shared HTTP response handler for foreground context - await handleHttpResponse(response, "foreground"); + // Log the full response including headers if available + locationLogger.debug("HTTP response received", { + status: response?.status, + success: response?.success, + responseText: response?.responseText, + url: response?.url, + method: response?.method, + isSync: response?.isSync, + requestHeaders: + response?.request?.headers || "Headers not available in response", + }); + + // Add Sentry breadcrumb for HTTP responses + Sentry.addBreadcrumb({ + message: "Background geolocation HTTP response", + category: "geolocation-http", + level: response?.status === 200 ? "info" : "warning", + data: { + status: response?.status, + success: response?.success, + url: response?.url, + isSync: response?.isSync, + recordCount: response?.count, + }, + }); + + // Log the current auth token for comparison + const { userToken } = getAuthState(); + locationLogger.debug("Current auth state token", { + tokenAvailable: !!userToken, + tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null, + }); + + const statusCode = response?.status; + + // log status code and response + locationLogger.debug("HTTP response received", { + status: statusCode, + responseText: response?.responseText, + }); + + switch (statusCode) { + case 200: + // Successful response, check for token refresh + try { + const responseBody = response?.responseText + ? JSON.parse(response.responseText) + : null; + + if (responseBody?.userBearerJwt) { + locationLogger.info( + "Token refresh successful, updating stored token", + ); + + // Use auth action to update both in-memory and persistent storage + await authActions.setUserToken(responseBody.userBearerJwt); + + // Update BackgroundGeolocation config with new token + await BackgroundGeolocation.setConfig({ + headers: { + Authorization: `Bearer ${responseBody.userBearerJwt}`, + }, + }); + + locationLogger.debug( + "Updated BackgroundGeolocation with refreshed token and removed X-Auth-Token header", + ); + + Sentry.addBreadcrumb({ + message: "Token refreshed successfully", + category: "geolocation-auth", + level: "info", + }); + } + } catch (e) { + locationLogger.debug("Failed to parse successful response", { + error: e.message, + responseText: response?.responseText, + }); + } + break; + case 410: + // Auth token expired, logout + locationLogger.info("Auth token expired (410), logging out"); + Sentry.addBreadcrumb({ + message: "Auth token expired - logging out", + category: "geolocation-auth", + level: "warning", + }); + authActions.logout(); + break; + case 401: + // Unauthorized: User token expired, refresh using throttled reload + locationLogger.info("Unauthorized (401), attempting to refresh token"); + + // Add more detailed logging of the error response + try { + const errorBody = response?.responseText + ? JSON.parse(response.responseText) + : null; + locationLogger.debug("Unauthorized error details", { + errorBody, + errorType: errorBody?.error?.type, + errorMessage: errorBody?.error?.message, + errorPath: errorBody?.error?.errors?.[0]?.path, + }); + + Sentry.addBreadcrumb({ + message: "Unauthorized - refreshing token", + category: "geolocation-auth", + level: "warning", + data: { + errorType: errorBody?.error?.type, + errorMessage: errorBody?.error?.message, + }, + }); + } catch (e) { + locationLogger.debug("Failed to parse error response", { + error: e.message, + responseText: response?.responseText, + }); + } + + reloadAuth(); + break; + } }); try { diff --git a/src/stores/auth.js b/src/stores/auth.js index cc5e1fe..4a0e602 100644 --- a/src/stores/auth.js +++ b/src/stores/auth.js @@ -71,7 +71,6 @@ export default createAtom(({ get, merge, getActions }) => { await secureStore.setItemAsync("userToken", userToken); endLoading({ userToken, - authToken, }); sessionActions.loadSessionFromJWT(userToken); return { userToken }; @@ -111,7 +110,6 @@ export default createAtom(({ get, merge, getActions }) => { } else { endLoading({ userToken, - authToken, }); sessionActions.loadSessionFromJWT(jwtData); return; @@ -124,7 +122,6 @@ export default createAtom(({ get, merge, getActions }) => { authLogger.info("Successfully registered new user"); authToken = res.authToken; await secureStore.setItemAsync("authToken", authToken); - merge({ authToken }); } if (!userToken && authToken) { @@ -263,8 +260,6 @@ export default createAtom(({ get, merge, getActions }) => { ]); merge({ userOffMode: true, - authToken: null, - userToken: null, }); return; } @@ -308,7 +303,6 @@ export default createAtom(({ get, merge, getActions }) => { return { default: { userToken: null, - authToken: null, loading: true, initialized: false, onReload: false,