@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
563 lines (512 loc) • 19.4 kB
JavaScript
;
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
import { DeviceManager } from '../../utils/deviceManager';
// Define the context shape
import { jsx as _jsx } from "react/jsx-runtime";
// Create the context with default values
const OxyContext = /*#__PURE__*/createContext(null);
// Props for the OxyContextProvider
// Platform storage implementation
// Web localStorage implementation
class WebStorage {
async getItem(key) {
return localStorage.getItem(key);
}
async setItem(key, value) {
localStorage.setItem(key, value);
}
async removeItem(key) {
localStorage.removeItem(key);
}
async clear() {
localStorage.clear();
}
}
// React Native AsyncStorage implementation
let AsyncStorage;
// Determine the platform and set up storage
const isReactNative = () => {
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
};
// Get appropriate storage for the platform
const getStorage = async () => {
if (isReactNative()) {
if (!AsyncStorage) {
try {
const asyncStorageModule = await import('@react-native-async-storage/async-storage');
AsyncStorage = asyncStorageModule.default;
} catch (error) {
console.error('Failed to import AsyncStorage:', error);
throw new Error('AsyncStorage is required in React Native environment');
}
}
return AsyncStorage;
}
return new WebStorage();
};
// Storage keys for secure sessions
const getSecureStorageKeys = (prefix = 'oxy_secure') => ({
sessions: `${prefix}_sessions`,
// Array of SecureClientSession objects
activeSessionId: `${prefix}_active_session_id` // ID of currently active session
});
export const OxyContextProvider = ({
children,
oxyServices,
storageKeyPrefix = 'oxy_secure',
onAuthStateChange,
bottomSheetRef
}) => {
// Authentication state
const [user, setUser] = useState(null);
const [minimalUser, setMinimalUser] = useState(null);
const [sessions, setSessions] = useState([]);
const [activeSessionId, setActiveSessionId] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [storage, setStorage] = useState(null);
// Storage keys (memoized to prevent infinite loops)
const keys = useMemo(() => getSecureStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
// Initialize storage
useEffect(() => {
const initStorage = async () => {
try {
const platformStorage = await getStorage();
setStorage(platformStorage);
} catch (error) {
console.error('Failed to initialize storage:', error);
setError('Failed to initialize storage');
}
};
initStorage();
}, []);
// Effect to initialize authentication state
useEffect(() => {
const initAuth = async () => {
if (!storage) return;
setIsLoading(true);
try {
// Load stored sessions
const sessionsData = await storage.getItem(keys.sessions);
const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
console.log('SecureAuth - sessionsData:', sessionsData);
console.log('SecureAuth - activeSessionId:', storedActiveSessionId);
if (sessionsData) {
const parsedSessions = JSON.parse(sessionsData);
// Migrate old session format to include user info
const migratedSessions = [];
let shouldUpdateStorage = false;
for (const session of parsedSessions) {
if (!session.userId || !session.username) {
// Session is missing user info, try to fetch it
try {
const sessionUser = await oxyServices.getUserBySession(session.sessionId);
migratedSessions.push({
...session,
userId: sessionUser.id,
username: sessionUser.username
});
shouldUpdateStorage = true;
console.log(`Migrated session ${session.sessionId} for user ${sessionUser.username}`);
} catch (error) {
// Session might be invalid, skip it
console.log(`Removing invalid session ${session.sessionId}:`, error);
shouldUpdateStorage = true;
}
} else {
// Session already has user info
migratedSessions.push(session);
}
}
// Update storage if we made changes
if (shouldUpdateStorage) {
await saveSessionsToStorage(migratedSessions);
}
setSessions(migratedSessions);
if (storedActiveSessionId && migratedSessions.length > 0) {
const activeSession = migratedSessions.find(s => s.sessionId === storedActiveSessionId);
if (activeSession) {
console.log('SecureAuth - activeSession found:', activeSession);
// Validate session
try {
const validation = await oxyServices.validateSession(activeSession.sessionId);
if (validation.valid) {
console.log('SecureAuth - session validated successfully');
setActiveSessionId(activeSession.sessionId);
// Get access token for API calls
await oxyServices.getTokenBySession(activeSession.sessionId);
// Load full user data
const fullUser = await oxyServices.getUserBySession(activeSession.sessionId);
setUser(fullUser);
setMinimalUser({
id: fullUser.id,
username: fullUser.username,
avatar: fullUser.avatar
});
if (onAuthStateChange) {
onAuthStateChange(fullUser);
}
} else {
console.log('SecureAuth - session invalid, removing');
await removeInvalidSession(activeSession.sessionId);
}
} catch (error) {
console.error('SecureAuth - session validation error:', error);
await removeInvalidSession(activeSession.sessionId);
}
}
}
}
} catch (err) {
console.error('Secure auth initialization error:', err);
await clearAllStorage();
} finally {
setIsLoading(false);
}
};
if (storage) {
initAuth();
}
}, [storage, oxyServices, keys, onAuthStateChange]);
// Remove invalid session
const removeInvalidSession = useCallback(async sessionId => {
const filteredSessions = sessions.filter(s => s.sessionId !== sessionId);
setSessions(filteredSessions);
await saveSessionsToStorage(filteredSessions);
// If there are other sessions, switch to the first one
if (filteredSessions.length > 0) {
await switchToSession(filteredSessions[0].sessionId);
} else {
// No valid sessions left
setActiveSessionId(null);
setUser(null);
setMinimalUser(null);
await storage?.removeItem(keys.activeSessionId);
if (onAuthStateChange) {
onAuthStateChange(null);
}
}
}, [sessions, storage, keys, onAuthStateChange]);
// Save sessions to storage
const saveSessionsToStorage = useCallback(async sessionsList => {
if (!storage) return;
await storage.setItem(keys.sessions, JSON.stringify(sessionsList));
}, [storage, keys.sessions]);
// Save active session ID to storage
const saveActiveSessionId = useCallback(async sessionId => {
if (!storage) return;
await storage.setItem(keys.activeSessionId, sessionId);
}, [storage, keys.activeSessionId]);
// Clear all storage
const clearAllStorage = useCallback(async () => {
if (!storage) return;
try {
await storage.removeItem(keys.sessions);
await storage.removeItem(keys.activeSessionId);
} catch (err) {
console.error('Clear secure storage error:', err);
}
}, [storage, keys]);
// Switch to a different session
const switchToSession = useCallback(async sessionId => {
try {
setIsLoading(true);
// Get access token for this session
await oxyServices.getTokenBySession(sessionId);
// Load full user data
const fullUser = await oxyServices.getUserBySession(sessionId);
setActiveSessionId(sessionId);
setUser(fullUser);
setMinimalUser({
id: fullUser.id,
username: fullUser.username,
avatar: fullUser.avatar
});
await saveActiveSessionId(sessionId);
if (onAuthStateChange) {
onAuthStateChange(fullUser);
}
} catch (error) {
console.error('Switch session error:', error);
setError('Failed to switch session');
} finally {
setIsLoading(false);
}
}, [oxyServices, onAuthStateChange, saveActiveSessionId]);
// Secure login method
const login = async (username, password, deviceName) => {
if (!storage) throw new Error('Storage not initialized');
setIsLoading(true);
setError(null);
try {
// Get device fingerprint for enhanced device identification
const deviceFingerprint = DeviceManager.getDeviceFingerprint();
// Get or generate persistent device info
const deviceInfo = await DeviceManager.getDeviceInfo();
console.log('SecureAuth - Using device fingerprint:', deviceFingerprint);
console.log('SecureAuth - Using device ID:', deviceInfo.deviceId);
const response = await oxyServices.secureLogin(username, password, deviceName || deviceInfo.deviceName || DeviceManager.getDefaultDeviceName(), deviceFingerprint);
// Create client session object with user info for duplicate detection
const clientSession = {
sessionId: response.sessionId,
deviceId: response.deviceId,
expiresAt: response.expiresAt,
lastActive: new Date().toISOString(),
userId: response.user.id,
username: response.user.username
};
// Check if this user already has a session (prevent duplicate accounts)
const existingUserSessionIndex = sessions.findIndex(s => s.userId === response.user.id || s.username === response.user.username);
let updatedSessions;
if (existingUserSessionIndex !== -1) {
// User already has a session - replace it with the new one (reused session scenario)
const existingSession = sessions[existingUserSessionIndex];
updatedSessions = [...sessions];
updatedSessions[existingUserSessionIndex] = clientSession;
console.log(`Reusing/updating existing session for user ${response.user.username}. Previous session: ${existingSession.sessionId}, New session: ${response.sessionId}`);
// If the replaced session was the active one, update active session
if (activeSessionId === existingSession.sessionId) {
setActiveSessionId(response.sessionId);
await saveActiveSessionId(response.sessionId);
}
} else {
// Add new session for new user
updatedSessions = [...sessions, clientSession];
console.log(`Added new session for user ${response.user.username} on device ${response.deviceId}`);
}
setSessions(updatedSessions);
await saveSessionsToStorage(updatedSessions);
// Set as active session
setActiveSessionId(response.sessionId);
await saveActiveSessionId(response.sessionId);
// Get access token for API calls
await oxyServices.getTokenBySession(response.sessionId);
// Load full user data
const fullUser = await oxyServices.getUserBySession(response.sessionId);
setUser(fullUser);
setMinimalUser(response.user);
if (onAuthStateChange) {
onAuthStateChange(fullUser);
}
return fullUser;
} catch (error) {
setError(error.message || 'Login failed');
throw error;
} finally {
setIsLoading(false);
}
};
// Logout method
const logout = async targetSessionId => {
if (!activeSessionId) return;
try {
const sessionToLogout = targetSessionId || activeSessionId;
await oxyServices.logoutSecureSession(activeSessionId, sessionToLogout);
// Remove session from local storage
const filteredSessions = sessions.filter(s => s.sessionId !== sessionToLogout);
setSessions(filteredSessions);
await saveSessionsToStorage(filteredSessions);
// If logging out active session
if (sessionToLogout === activeSessionId) {
if (filteredSessions.length > 0) {
// Switch to another session
await switchToSession(filteredSessions[0].sessionId);
} else {
// No sessions left
setActiveSessionId(null);
setUser(null);
setMinimalUser(null);
await storage?.removeItem(keys.activeSessionId);
if (onAuthStateChange) {
onAuthStateChange(null);
}
}
}
} catch (error) {
console.error('Logout error:', error);
setError('Logout failed');
}
};
// Logout all sessions
const logoutAll = async () => {
console.log('logoutAll called with activeSessionId:', activeSessionId);
if (!activeSessionId) {
console.error('No active session ID found, cannot logout all');
setError('No active session found');
throw new Error('No active session found');
}
if (!oxyServices) {
console.error('OxyServices not initialized');
setError('Service not available');
throw new Error('Service not available');
}
try {
console.log('Calling oxyServices.logoutAllSecureSessions with sessionId:', activeSessionId);
await oxyServices.logoutAllSecureSessions(activeSessionId);
console.log('logoutAllSecureSessions completed successfully');
// Clear all local data
setSessions([]);
setActiveSessionId(null);
setUser(null);
setMinimalUser(null);
await clearAllStorage();
console.log('Local storage cleared');
if (onAuthStateChange) {
onAuthStateChange(null);
console.log('Auth state change callback called');
}
} catch (error) {
console.error('Logout all error:', error);
setError(`Logout all failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error;
}
};
// Sign up method (placeholder - you can implement based on your needs)
const signUp = async (username, email, password) => {
// Implement sign up logic similar to secureLogin
throw new Error('Sign up not implemented yet');
};
// Switch session method
const switchSession = async sessionId => {
await switchToSession(sessionId);
};
// Remove session method
const removeSession = async sessionId => {
await logout(sessionId);
};
// Refresh sessions method
const refreshSessions = async () => {
if (!activeSessionId) return;
try {
const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
// Update local sessions with server data
const updatedSessions = serverSessions.map(serverSession => ({
sessionId: serverSession.sessionId,
deviceId: serverSession.deviceId,
expiresAt: new Date().toISOString(),
// You might want to get this from server
lastActive: new Date().toISOString()
}));
setSessions(updatedSessions);
await saveSessionsToStorage(updatedSessions);
} catch (error) {
console.error('Refresh sessions error:', error);
}
};
// Device management methods
const getDeviceSessions = async () => {
if (!activeSessionId) throw new Error('No active session');
try {
return await oxyServices.getDeviceSessions(activeSessionId);
} catch (error) {
console.error('Get device sessions error:', error);
throw error;
}
};
const logoutAllDeviceSessions = async () => {
if (!activeSessionId) throw new Error('No active session');
try {
await oxyServices.logoutAllDeviceSessions(activeSessionId);
// Clear all local sessions since we logged out from all devices
setSessions([]);
setActiveSessionId(null);
setUser(null);
setMinimalUser(null);
await clearAllStorage();
if (onAuthStateChange) {
onAuthStateChange(null);
}
} catch (error) {
console.error('Logout all device sessions error:', error);
throw error;
}
};
const updateDeviceName = async deviceName => {
if (!activeSessionId) throw new Error('No active session');
try {
await oxyServices.updateDeviceName(activeSessionId, deviceName);
// Update local device info
await DeviceManager.updateDeviceName(deviceName);
} catch (error) {
console.error('Update device name error:', error);
throw error;
}
};
// Bottom sheet control methods
const showBottomSheet = useCallback(screenOrConfig => {
console.log('showBottomSheet called with:', screenOrConfig);
if (bottomSheetRef?.current) {
console.log('bottomSheetRef is available');
// First, show the bottom sheet
if (bottomSheetRef.current.expand) {
console.log('Expanding bottom sheet');
bottomSheetRef.current.expand();
} else if (bottomSheetRef.current.present) {
console.log('Presenting bottom sheet');
bottomSheetRef.current.present();
} else {
console.warn('No expand or present method available on bottomSheetRef');
}
// Then navigate to the specified screen if provided
if (screenOrConfig) {
// Add a small delay to ensure the bottom sheet is opened first
setTimeout(() => {
if (typeof screenOrConfig === 'string') {
// Simple screen name
console.log('Navigating to screen:', screenOrConfig);
bottomSheetRef.current?._navigateToScreen?.(screenOrConfig);
} else {
// Screen with props
console.log('Navigating to screen with props:', screenOrConfig.screen, screenOrConfig.props);
bottomSheetRef.current?._navigateToScreen?.(screenOrConfig.screen, screenOrConfig.props);
}
}, 100);
}
} else {
console.warn('bottomSheetRef is not available');
}
}, [bottomSheetRef]);
const hideBottomSheet = useCallback(() => {
if (bottomSheetRef?.current) {
bottomSheetRef.current.dismiss?.();
}
}, [bottomSheetRef]);
// Context value
const contextValue = {
user,
minimalUser,
sessions,
activeSessionId,
isAuthenticated: !!user,
isLoading,
error,
login,
logout,
logoutAll,
signUp,
switchSession,
removeSession,
refreshSessions,
getDeviceSessions,
logoutAllDeviceSessions,
updateDeviceName,
oxyServices,
bottomSheetRef,
showBottomSheet,
hideBottomSheet
};
return /*#__PURE__*/_jsx(OxyContext.Provider, {
value: contextValue,
children: children
});
};
// Hook to use the context
export const useOxy = () => {
const context = useContext(OxyContext);
if (!context) {
throw new Error('useOxy must be used within an OxyContextProvider');
}
return context;
};
export default OxyContext;
//# sourceMappingURL=OxyContext.js.map