@oxyhq/services 5.5.7 → 5.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/core/index.js +13 -13
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +480 -87
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuthFetch.js +80 -45
- package/lib/commonjs/ui/hooks/useAuthFetch.js.map +1 -1
- package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/module/core/index.js +13 -6
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +480 -87
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useAuthFetch.js +80 -45
- package/lib/module/ui/hooks/useAuthFetch.js.map +1 -1
- package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/typescript/core/index.d.ts +7 -6
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +12 -7
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useAuthFetch.d.ts +10 -4
- package/lib/typescript/ui/hooks/useAuthFetch.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/backend-middleware.test.ts +209 -0
- package/src/__tests__/ui/hooks/authfetch-integration.test.ts +197 -0
- package/src/__tests__/ui/hooks/backward-compatibility.test.ts +159 -0
- package/src/__tests__/ui/hooks/real-world-scenarios.test.ts +224 -0
- package/src/__tests__/ui/hooks/url-resolution.test.ts +129 -0
- package/src/__tests__/ui/hooks/useAuthFetch-separation.test.ts +69 -0
- package/src/__tests__/ui/hooks/useAuthFetch.test.ts +70 -0
- package/src/core/index.ts +13 -7
- package/src/ui/context/OxyContext.tsx +536 -99
- package/src/ui/hooks/useAuthFetch.ts +81 -47
- package/src/ui/screens/SessionManagementScreen.tsx +9 -9
- package/lib/commonjs/core/AuthManager.js +0 -378
- package/lib/commonjs/core/AuthManager.js.map +0 -1
- package/lib/module/core/AuthManager.js +0 -373
- package/lib/module/core/AuthManager.js.map +0 -1
- package/lib/typescript/core/AuthManager.d.ts +0 -100
- package/lib/typescript/core/AuthManager.d.ts.map +0 -1
- package/src/core/AuthManager.ts +0 -389
|
@@ -1,44 +1,53 @@
|
|
|
1
1
|
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode, useMemo } from 'react';
|
|
2
2
|
import { OxyServices } from '../../core';
|
|
3
|
-
import { AuthManager, AuthState } from '../../core/AuthManager';
|
|
4
3
|
import { User } from '../../models/interfaces';
|
|
5
|
-
import { SecureClientSession } from '../../models/secureSession';
|
|
4
|
+
import { SecureLoginResponse, SecureClientSession, MinimalUserData } from '../../models/secureSession';
|
|
5
|
+
import { DeviceManager } from '../../utils/deviceManager';
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// Define the context shape
|
|
8
8
|
export interface OxyContextState {
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Authentication state
|
|
10
|
+
user: User | null; // Current active user (loaded from server)
|
|
11
|
+
minimalUser: MinimalUserData | null; // Minimal user data for UI
|
|
12
|
+
sessions: SecureClientSession[]; // All active sessions
|
|
13
|
+
activeSessionId: string | null;
|
|
14
|
+
isAuthenticated: boolean; // Single source of truth for authentication - use this instead of service methods
|
|
12
15
|
isLoading: boolean;
|
|
13
16
|
error: string | null;
|
|
14
17
|
|
|
15
|
-
// Session management
|
|
16
|
-
sessions: any[];
|
|
17
|
-
activeSessionId: string | null;
|
|
18
|
-
|
|
19
18
|
// Auth methods
|
|
20
19
|
login: (username: string, password: string, deviceName?: string) => Promise<User>;
|
|
21
20
|
logout: (targetSessionId?: string) => Promise<void>;
|
|
21
|
+
logoutAll: () => Promise<void>;
|
|
22
22
|
signUp: (username: string, email: string, password: string) => Promise<User>;
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
refreshSessions: () => Promise<void>;
|
|
24
|
+
// Multi-session methods
|
|
26
25
|
switchSession: (sessionId: string) => Promise<void>;
|
|
27
26
|
removeSession: (sessionId: string) => Promise<void>;
|
|
28
|
-
|
|
27
|
+
refreshSessions: () => Promise<void>;
|
|
29
28
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
// Device management methods
|
|
30
|
+
getDeviceSessions: () => Promise<any[]>;
|
|
31
|
+
logoutAllDeviceSessions: () => Promise<void>;
|
|
32
|
+
updateDeviceName: (deviceName: string) => Promise<void>;
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// Access to services
|
|
35
|
+
oxyServices: OxyServices;
|
|
35
36
|
bottomSheetRef?: React.RefObject<any>;
|
|
37
|
+
|
|
38
|
+
// API configuration
|
|
39
|
+
setApiUrl: (url: string) => void;
|
|
40
|
+
getAppBaseURL: () => string;
|
|
41
|
+
|
|
42
|
+
// Methods to directly control the bottom sheet
|
|
36
43
|
showBottomSheet?: (screenOrConfig?: string | { screen: string; props?: Record<string, any> }) => void;
|
|
37
44
|
hideBottomSheet?: () => void;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
// Create the context with default values
|
|
40
48
|
const OxyContext = createContext<OxyContextState | null>(null);
|
|
41
49
|
|
|
50
|
+
// Props for the OxyContextProvider
|
|
42
51
|
export interface OxyContextProviderProps {
|
|
43
52
|
children: ReactNode;
|
|
44
53
|
oxyServices: OxyServices;
|
|
@@ -47,7 +56,7 @@ export interface OxyContextProviderProps {
|
|
|
47
56
|
bottomSheetRef?: React.RefObject<any>;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
//
|
|
59
|
+
// Platform storage implementation
|
|
51
60
|
interface StorageInterface {
|
|
52
61
|
getItem: (key: string) => Promise<string | null>;
|
|
53
62
|
setItem: (key: string, value: string) => Promise<void>;
|
|
@@ -55,6 +64,7 @@ interface StorageInterface {
|
|
|
55
64
|
clear: () => Promise<void>;
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
// Web localStorage implementation
|
|
58
68
|
class WebStorage implements StorageInterface {
|
|
59
69
|
async getItem(key: string): Promise<string | null> {
|
|
60
70
|
return localStorage.getItem(key);
|
|
@@ -73,12 +83,15 @@ class WebStorage implements StorageInterface {
|
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
// React Native AsyncStorage implementation
|
|
76
87
|
let AsyncStorage: StorageInterface;
|
|
77
88
|
|
|
89
|
+
// Determine the platform and set up storage
|
|
78
90
|
const isReactNative = (): boolean => {
|
|
79
91
|
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
80
92
|
};
|
|
81
93
|
|
|
94
|
+
// Get appropriate storage for the platform
|
|
82
95
|
const getStorage = async (): Promise<StorageInterface> => {
|
|
83
96
|
if (isReactNative()) {
|
|
84
97
|
if (!AsyncStorage) {
|
|
@@ -92,9 +105,16 @@ const getStorage = async (): Promise<StorageInterface> => {
|
|
|
92
105
|
}
|
|
93
106
|
return AsyncStorage;
|
|
94
107
|
}
|
|
108
|
+
|
|
95
109
|
return new WebStorage();
|
|
96
110
|
};
|
|
97
111
|
|
|
112
|
+
// Storage keys for secure sessions
|
|
113
|
+
const getSecureStorageKeys = (prefix = 'oxy_secure') => ({
|
|
114
|
+
sessions: `${prefix}_sessions`, // Array of SecureClientSession objects
|
|
115
|
+
activeSessionId: `${prefix}_active_session_id`, // ID of currently active session
|
|
116
|
+
});
|
|
117
|
+
|
|
98
118
|
export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
99
119
|
children,
|
|
100
120
|
oxyServices,
|
|
@@ -102,29 +122,18 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
102
122
|
onAuthStateChange,
|
|
103
123
|
bottomSheetRef,
|
|
104
124
|
}) => {
|
|
125
|
+
// Authentication state
|
|
126
|
+
const [user, setUser] = useState<User | null>(null);
|
|
127
|
+
const [minimalUser, setMinimalUser] = useState<MinimalUserData | null>(null);
|
|
128
|
+
const [sessions, setSessions] = useState<SecureClientSession[]>([]);
|
|
129
|
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
130
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
131
|
+
const [error, setError] = useState<string | null>(null);
|
|
105
132
|
const [storage, setStorage] = useState<StorageInterface | null>(null);
|
|
106
|
-
const [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
activeSessionId: null,
|
|
111
|
-
sessions: [],
|
|
112
|
-
isLoading: true,
|
|
113
|
-
error: null,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Create AuthManager instance
|
|
117
|
-
const authManager = useMemo(() => {
|
|
118
|
-
return new AuthManager({
|
|
119
|
-
oxyServices,
|
|
120
|
-
onStateChange: (newState) => {
|
|
121
|
-
setAuthState(newState);
|
|
122
|
-
if (onAuthStateChange) {
|
|
123
|
-
onAuthStateChange(newState.user);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
}, [oxyServices, onAuthStateChange]);
|
|
133
|
+
const [appBaseURL, setAppBaseURL] = useState<string>(oxyServices.getBaseURL());
|
|
134
|
+
|
|
135
|
+
// Storage keys (memoized to prevent infinite loops)
|
|
136
|
+
const keys = useMemo(() => getSecureStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
|
|
128
137
|
|
|
129
138
|
// Initialize storage
|
|
130
139
|
useEffect(() => {
|
|
@@ -134,109 +143,499 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
134
143
|
setStorage(platformStorage);
|
|
135
144
|
} catch (error) {
|
|
136
145
|
console.error('Failed to initialize storage:', error);
|
|
137
|
-
|
|
146
|
+
setError('Failed to initialize storage');
|
|
138
147
|
}
|
|
139
148
|
};
|
|
149
|
+
|
|
140
150
|
initStorage();
|
|
141
151
|
}, []);
|
|
142
152
|
|
|
143
|
-
//
|
|
153
|
+
// Effect to initialize authentication state
|
|
144
154
|
useEffect(() => {
|
|
145
155
|
const initAuth = async () => {
|
|
146
156
|
if (!storage) return;
|
|
147
157
|
|
|
158
|
+
setIsLoading(true);
|
|
148
159
|
try {
|
|
149
|
-
|
|
150
|
-
await
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
// Load stored sessions
|
|
161
|
+
const sessionsData = await storage.getItem(keys.sessions);
|
|
162
|
+
const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
|
|
163
|
+
|
|
164
|
+
console.log('SecureAuth - sessionsData:', sessionsData);
|
|
165
|
+
console.log('SecureAuth - activeSessionId:', storedActiveSessionId);
|
|
166
|
+
|
|
167
|
+
if (sessionsData) {
|
|
168
|
+
const parsedSessions: SecureClientSession[] = JSON.parse(sessionsData);
|
|
169
|
+
|
|
170
|
+
// Migrate old session format to include user info
|
|
171
|
+
const migratedSessions: SecureClientSession[] = [];
|
|
172
|
+
let shouldUpdateStorage = false;
|
|
173
|
+
|
|
174
|
+
for (const session of parsedSessions) {
|
|
175
|
+
if (!session.userId || !session.username) {
|
|
176
|
+
// Session is missing user info, try to fetch it
|
|
177
|
+
try {
|
|
178
|
+
const sessionUser = await oxyServices.getUserBySession(session.sessionId);
|
|
179
|
+
migratedSessions.push({
|
|
180
|
+
...session,
|
|
181
|
+
userId: sessionUser.id,
|
|
182
|
+
username: sessionUser.username
|
|
183
|
+
});
|
|
184
|
+
shouldUpdateStorage = true;
|
|
185
|
+
console.log(`Migrated session ${session.sessionId} for user ${sessionUser.username}`);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Session might be invalid, skip it
|
|
188
|
+
console.log(`Removing invalid session ${session.sessionId}:`, error);
|
|
189
|
+
shouldUpdateStorage = true;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Session already has user info
|
|
193
|
+
migratedSessions.push(session);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update storage if we made changes
|
|
198
|
+
if (shouldUpdateStorage) {
|
|
199
|
+
await saveSessionsToStorage(migratedSessions);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setSessions(migratedSessions);
|
|
203
|
+
|
|
204
|
+
if (storedActiveSessionId && migratedSessions.length > 0) {
|
|
205
|
+
const activeSession = migratedSessions.find(s => s.sessionId === storedActiveSessionId);
|
|
206
|
+
|
|
207
|
+
if (activeSession) {
|
|
208
|
+
console.log('SecureAuth - activeSession found:', activeSession);
|
|
209
|
+
|
|
210
|
+
// Validate session
|
|
211
|
+
try {
|
|
212
|
+
const validation = await oxyServices.validateSession(activeSession.sessionId);
|
|
213
|
+
|
|
214
|
+
if (validation.valid) {
|
|
215
|
+
console.log('SecureAuth - session validated successfully');
|
|
216
|
+
setActiveSessionId(activeSession.sessionId);
|
|
217
|
+
|
|
218
|
+
// Get access token for API calls
|
|
219
|
+
await oxyServices.getTokenBySession(activeSession.sessionId);
|
|
220
|
+
|
|
221
|
+
// Load full user data
|
|
222
|
+
const fullUser = await oxyServices.getUserBySession(activeSession.sessionId);
|
|
223
|
+
setUser(fullUser);
|
|
224
|
+
setMinimalUser({
|
|
225
|
+
id: fullUser.id,
|
|
226
|
+
username: fullUser.username,
|
|
227
|
+
avatar: fullUser.avatar
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (onAuthStateChange) {
|
|
231
|
+
onAuthStateChange(fullUser);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
console.log('SecureAuth - session invalid, removing');
|
|
235
|
+
await removeInvalidSession(activeSession.sessionId);
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('SecureAuth - session validation error:', error);
|
|
239
|
+
await removeInvalidSession(activeSession.sessionId);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error('Secure auth initialization error:', err);
|
|
246
|
+
await clearAllStorage();
|
|
247
|
+
} finally {
|
|
248
|
+
setIsLoading(false);
|
|
154
249
|
}
|
|
155
250
|
};
|
|
156
251
|
|
|
157
252
|
if (storage) {
|
|
158
253
|
initAuth();
|
|
159
254
|
}
|
|
160
|
-
}, [storage,
|
|
255
|
+
}, [storage, oxyServices, keys, onAuthStateChange]);
|
|
256
|
+
|
|
257
|
+
// Remove invalid session
|
|
258
|
+
const removeInvalidSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
259
|
+
const filteredSessions = sessions.filter(s => s.sessionId !== sessionId);
|
|
260
|
+
setSessions(filteredSessions);
|
|
261
|
+
await saveSessionsToStorage(filteredSessions);
|
|
262
|
+
|
|
263
|
+
// If there are other sessions, switch to the first one
|
|
264
|
+
if (filteredSessions.length > 0) {
|
|
265
|
+
await switchToSession(filteredSessions[0].sessionId);
|
|
266
|
+
} else {
|
|
267
|
+
// No valid sessions left
|
|
268
|
+
setActiveSessionId(null);
|
|
269
|
+
setUser(null);
|
|
270
|
+
setMinimalUser(null);
|
|
271
|
+
await storage?.removeItem(keys.activeSessionId);
|
|
272
|
+
|
|
273
|
+
if (onAuthStateChange) {
|
|
274
|
+
onAuthStateChange(null);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}, [sessions, storage, keys, onAuthStateChange]);
|
|
278
|
+
|
|
279
|
+
// Save sessions to storage
|
|
280
|
+
const saveSessionsToStorage = useCallback(async (sessionsList: SecureClientSession[]): Promise<void> => {
|
|
281
|
+
if (!storage) return;
|
|
282
|
+
await storage.setItem(keys.sessions, JSON.stringify(sessionsList));
|
|
283
|
+
}, [storage, keys.sessions]);
|
|
284
|
+
|
|
285
|
+
// Save active session ID to storage
|
|
286
|
+
const saveActiveSessionId = useCallback(async (sessionId: string): Promise<void> => {
|
|
287
|
+
if (!storage) return;
|
|
288
|
+
await storage.setItem(keys.activeSessionId, sessionId);
|
|
289
|
+
}, [storage, keys.activeSessionId]);
|
|
290
|
+
|
|
291
|
+
// Clear all storage
|
|
292
|
+
const clearAllStorage = useCallback(async (): Promise<void> => {
|
|
293
|
+
if (!storage) return;
|
|
294
|
+
try {
|
|
295
|
+
await storage.removeItem(keys.sessions);
|
|
296
|
+
await storage.removeItem(keys.activeSessionId);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error('Clear secure storage error:', err);
|
|
299
|
+
}
|
|
300
|
+
}, [storage, keys]);
|
|
301
|
+
|
|
302
|
+
// Switch to a different session
|
|
303
|
+
const switchToSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
304
|
+
try {
|
|
305
|
+
setIsLoading(true);
|
|
306
|
+
|
|
307
|
+
// Get access token for this session
|
|
308
|
+
await oxyServices.getTokenBySession(sessionId);
|
|
309
|
+
|
|
310
|
+
// Load full user data
|
|
311
|
+
const fullUser = await oxyServices.getUserBySession(sessionId);
|
|
312
|
+
|
|
313
|
+
setActiveSessionId(sessionId);
|
|
314
|
+
setUser(fullUser);
|
|
315
|
+
setMinimalUser({
|
|
316
|
+
id: fullUser.id,
|
|
317
|
+
username: fullUser.username,
|
|
318
|
+
avatar: fullUser.avatar
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await saveActiveSessionId(sessionId);
|
|
322
|
+
|
|
323
|
+
if (onAuthStateChange) {
|
|
324
|
+
onAuthStateChange(fullUser);
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('Switch session error:', error);
|
|
328
|
+
setError('Failed to switch session');
|
|
329
|
+
} finally {
|
|
330
|
+
setIsLoading(false);
|
|
331
|
+
}
|
|
332
|
+
}, [oxyServices, onAuthStateChange, saveActiveSessionId]);
|
|
333
|
+
|
|
334
|
+
// Secure login method
|
|
335
|
+
const login = async (username: string, password: string, deviceName?: string): Promise<User> => {
|
|
336
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
337
|
+
|
|
338
|
+
setIsLoading(true);
|
|
339
|
+
setError(null);
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
// Get device fingerprint for enhanced device identification
|
|
343
|
+
const deviceFingerprint = DeviceManager.getDeviceFingerprint();
|
|
344
|
+
|
|
345
|
+
// Get or generate persistent device info
|
|
346
|
+
const deviceInfo = await DeviceManager.getDeviceInfo();
|
|
347
|
+
|
|
348
|
+
console.log('SecureAuth - Using device fingerprint:', deviceFingerprint);
|
|
349
|
+
console.log('SecureAuth - Using device ID:', deviceInfo.deviceId);
|
|
350
|
+
|
|
351
|
+
const response: SecureLoginResponse = await oxyServices.secureLogin(
|
|
352
|
+
username,
|
|
353
|
+
password,
|
|
354
|
+
deviceName || deviceInfo.deviceName || DeviceManager.getDefaultDeviceName(),
|
|
355
|
+
deviceFingerprint
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Create client session object with user info for duplicate detection
|
|
359
|
+
const clientSession: SecureClientSession = {
|
|
360
|
+
sessionId: response.sessionId,
|
|
361
|
+
deviceId: response.deviceId,
|
|
362
|
+
expiresAt: response.expiresAt,
|
|
363
|
+
lastActive: new Date().toISOString(),
|
|
364
|
+
userId: response.user.id,
|
|
365
|
+
username: response.user.username
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Check if this user already has a session (prevent duplicate accounts)
|
|
369
|
+
const existingUserSessionIndex = sessions.findIndex(s =>
|
|
370
|
+
s.userId === response.user.id || s.username === response.user.username
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
let updatedSessions: SecureClientSession[];
|
|
374
|
+
|
|
375
|
+
if (existingUserSessionIndex !== -1) {
|
|
376
|
+
// User already has a session - replace it with the new one (reused session scenario)
|
|
377
|
+
const existingSession = sessions[existingUserSessionIndex];
|
|
378
|
+
updatedSessions = [...sessions];
|
|
379
|
+
updatedSessions[existingUserSessionIndex] = clientSession;
|
|
380
|
+
|
|
381
|
+
console.log(`Reusing/updating existing session for user ${response.user.username}. Previous session: ${existingSession.sessionId}, New session: ${response.sessionId}`);
|
|
382
|
+
|
|
383
|
+
// If the replaced session was the active one, update active session
|
|
384
|
+
if (activeSessionId === existingSession.sessionId) {
|
|
385
|
+
setActiveSessionId(response.sessionId);
|
|
386
|
+
await saveActiveSessionId(response.sessionId);
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
// Add new session for new user
|
|
390
|
+
updatedSessions = [...sessions, clientSession];
|
|
391
|
+
console.log(`Added new session for user ${response.user.username} on device ${response.deviceId}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
setSessions(updatedSessions);
|
|
395
|
+
await saveSessionsToStorage(updatedSessions);
|
|
396
|
+
|
|
397
|
+
// Set as active session
|
|
398
|
+
setActiveSessionId(response.sessionId);
|
|
399
|
+
await saveActiveSessionId(response.sessionId);
|
|
400
|
+
|
|
401
|
+
// Get access token for API calls
|
|
402
|
+
await oxyServices.getTokenBySession(response.sessionId);
|
|
403
|
+
|
|
404
|
+
// Load full user data
|
|
405
|
+
const fullUser = await oxyServices.getUserBySession(response.sessionId);
|
|
406
|
+
setUser(fullUser);
|
|
407
|
+
setMinimalUser(response.user);
|
|
408
|
+
|
|
409
|
+
if (onAuthStateChange) {
|
|
410
|
+
onAuthStateChange(fullUser);
|
|
411
|
+
}
|
|
161
412
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
413
|
+
return fullUser;
|
|
414
|
+
} catch (error: any) {
|
|
415
|
+
setError(error.message || 'Login failed');
|
|
416
|
+
throw error;
|
|
417
|
+
} finally {
|
|
418
|
+
setIsLoading(false);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
165
421
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
422
|
+
// Logout method
|
|
423
|
+
const logout = async (targetSessionId?: string): Promise<void> => {
|
|
424
|
+
if (!activeSessionId) return;
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const sessionToLogout = targetSessionId || activeSessionId;
|
|
428
|
+
await oxyServices.logoutSecureSession(activeSessionId, sessionToLogout);
|
|
429
|
+
|
|
430
|
+
// Remove session from local storage
|
|
431
|
+
const filteredSessions = sessions.filter(s => s.sessionId !== sessionToLogout);
|
|
432
|
+
setSessions(filteredSessions);
|
|
433
|
+
await saveSessionsToStorage(filteredSessions);
|
|
434
|
+
|
|
435
|
+
// If logging out active session
|
|
436
|
+
if (sessionToLogout === activeSessionId) {
|
|
437
|
+
if (filteredSessions.length > 0) {
|
|
438
|
+
// Switch to another session
|
|
439
|
+
await switchToSession(filteredSessions[0].sessionId);
|
|
440
|
+
} else {
|
|
441
|
+
// No sessions left
|
|
442
|
+
setActiveSessionId(null);
|
|
443
|
+
setUser(null);
|
|
444
|
+
setMinimalUser(null);
|
|
445
|
+
await storage?.removeItem(keys.activeSessionId);
|
|
446
|
+
|
|
447
|
+
if (onAuthStateChange) {
|
|
448
|
+
onAuthStateChange(null);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error('Logout error:', error);
|
|
454
|
+
setError('Logout failed');
|
|
170
455
|
}
|
|
456
|
+
};
|
|
171
457
|
|
|
172
|
-
|
|
173
|
-
|
|
458
|
+
// Logout all sessions
|
|
459
|
+
const logoutAll = async (): Promise<void> => {
|
|
460
|
+
console.log('logoutAll called with activeSessionId:', activeSessionId);
|
|
174
461
|
|
|
175
|
-
|
|
176
|
-
|
|
462
|
+
if (!activeSessionId) {
|
|
463
|
+
console.error('No active session ID found, cannot logout all');
|
|
464
|
+
setError('No active session found');
|
|
465
|
+
throw new Error('No active session found');
|
|
466
|
+
}
|
|
177
467
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
468
|
+
if (!oxyServices) {
|
|
469
|
+
console.error('OxyServices not initialized');
|
|
470
|
+
setError('Service not available');
|
|
471
|
+
throw new Error('Service not available');
|
|
181
472
|
}
|
|
182
|
-
}, [authManager, storage, storageKeyPrefix]);
|
|
183
473
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
474
|
+
try {
|
|
475
|
+
console.log('Calling oxyServices.logoutAllSecureSessions with sessionId:', activeSessionId);
|
|
476
|
+
await oxyServices.logoutAllSecureSessions(activeSessionId);
|
|
477
|
+
console.log('logoutAllSecureSessions completed successfully');
|
|
478
|
+
|
|
479
|
+
// Clear all local data
|
|
480
|
+
setSessions([]);
|
|
481
|
+
setActiveSessionId(null);
|
|
482
|
+
setUser(null);
|
|
483
|
+
setMinimalUser(null);
|
|
484
|
+
await clearAllStorage();
|
|
485
|
+
console.log('Local storage cleared');
|
|
486
|
+
|
|
487
|
+
if (onAuthStateChange) {
|
|
488
|
+
onAuthStateChange(null);
|
|
489
|
+
console.log('Auth state change callback called');
|
|
490
|
+
}
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.error('Logout all error:', error);
|
|
493
|
+
setError(`Logout all failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
187
497
|
|
|
188
|
-
|
|
189
|
-
|
|
498
|
+
// Sign up method
|
|
499
|
+
const signUp = async (username: string, email: string, password: string): Promise<User> => {
|
|
500
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
190
501
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
502
|
+
setIsLoading(true);
|
|
503
|
+
setError(null);
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
// Create new account using the OxyServices signUp method
|
|
507
|
+
const response = await oxyServices.signUp(username, email, password);
|
|
508
|
+
|
|
509
|
+
console.log('SignUp successful:', response);
|
|
510
|
+
|
|
511
|
+
// Now log the user in securely to create a session
|
|
512
|
+
// This will handle the session creation and device registration
|
|
513
|
+
const user = await login(username, password);
|
|
514
|
+
|
|
515
|
+
return user;
|
|
516
|
+
} catch (error: any) {
|
|
517
|
+
setError(error.message || 'Sign up failed');
|
|
518
|
+
throw error;
|
|
519
|
+
} finally {
|
|
520
|
+
setIsLoading(false);
|
|
194
521
|
}
|
|
195
|
-
}
|
|
522
|
+
};
|
|
196
523
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
524
|
+
// Switch session method
|
|
525
|
+
const switchSession = async (sessionId: string): Promise<void> => {
|
|
526
|
+
await switchToSession(sessionId);
|
|
527
|
+
};
|
|
200
528
|
|
|
201
|
-
|
|
202
|
-
|
|
529
|
+
// Remove session method
|
|
530
|
+
const removeSession = async (sessionId: string): Promise<void> => {
|
|
531
|
+
await logout(sessionId);
|
|
532
|
+
};
|
|
203
533
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
534
|
+
// Refresh sessions method
|
|
535
|
+
const refreshSessions = async (): Promise<void> => {
|
|
536
|
+
if (!activeSessionId) return;
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
|
|
540
|
+
|
|
541
|
+
// Update local sessions with server data
|
|
542
|
+
const updatedSessions: SecureClientSession[] = serverSessions.map(serverSession => ({
|
|
543
|
+
sessionId: serverSession.sessionId,
|
|
544
|
+
deviceId: serverSession.deviceId,
|
|
545
|
+
expiresAt: new Date().toISOString(), // You might want to get this from server
|
|
546
|
+
lastActive: new Date().toISOString()
|
|
547
|
+
}));
|
|
548
|
+
|
|
549
|
+
setSessions(updatedSessions);
|
|
550
|
+
await saveSessionsToStorage(updatedSessions);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
console.error('Refresh sessions error:', error);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// Device management methods
|
|
557
|
+
const getDeviceSessions = async (): Promise<any[]> => {
|
|
558
|
+
if (!activeSessionId) throw new Error('No active session');
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
return await oxyServices.getDeviceSessions(activeSessionId);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.error('Get device sessions error:', error);
|
|
564
|
+
throw error;
|
|
207
565
|
}
|
|
208
|
-
}
|
|
566
|
+
};
|
|
209
567
|
|
|
210
|
-
const
|
|
211
|
-
|
|
568
|
+
const logoutAllDeviceSessions = async (): Promise<void> => {
|
|
569
|
+
if (!activeSessionId) throw new Error('No active session');
|
|
212
570
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
571
|
+
try {
|
|
572
|
+
await oxyServices.logoutAllDeviceSessions(activeSessionId);
|
|
573
|
+
|
|
574
|
+
// Clear all local sessions since we logged out from all devices
|
|
575
|
+
setSessions([]);
|
|
576
|
+
setActiveSessionId(null);
|
|
577
|
+
setUser(null);
|
|
578
|
+
setMinimalUser(null);
|
|
579
|
+
await clearAllStorage();
|
|
580
|
+
|
|
581
|
+
if (onAuthStateChange) {
|
|
582
|
+
onAuthStateChange(null);
|
|
583
|
+
}
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error('Logout all device sessions error:', error);
|
|
586
|
+
throw error;
|
|
217
587
|
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const updateDeviceName = async (deviceName: string): Promise<void> => {
|
|
591
|
+
if (!activeSessionId) throw new Error('No active session');
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
await oxyServices.updateDeviceName(activeSessionId, deviceName);
|
|
218
595
|
|
|
219
|
-
|
|
220
|
-
|
|
596
|
+
// Update local device info
|
|
597
|
+
await DeviceManager.updateDeviceName(deviceName);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error('Update device name error:', error);
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
221
603
|
|
|
222
|
-
// Bottom sheet
|
|
604
|
+
// Bottom sheet control methods
|
|
223
605
|
const showBottomSheet = useCallback((screenOrConfig?: string | { screen: string; props?: Record<string, any> }) => {
|
|
606
|
+
console.log('showBottomSheet called with:', screenOrConfig);
|
|
607
|
+
|
|
224
608
|
if (bottomSheetRef?.current) {
|
|
609
|
+
console.log('bottomSheetRef is available');
|
|
610
|
+
|
|
611
|
+
// First, show the bottom sheet
|
|
225
612
|
if (bottomSheetRef.current.expand) {
|
|
613
|
+
console.log('Expanding bottom sheet');
|
|
226
614
|
bottomSheetRef.current.expand();
|
|
227
615
|
} else if (bottomSheetRef.current.present) {
|
|
616
|
+
console.log('Presenting bottom sheet');
|
|
228
617
|
bottomSheetRef.current.present();
|
|
618
|
+
} else {
|
|
619
|
+
console.warn('No expand or present method available on bottomSheetRef');
|
|
229
620
|
}
|
|
230
621
|
|
|
622
|
+
// Then navigate to the specified screen if provided
|
|
231
623
|
if (screenOrConfig) {
|
|
624
|
+
// Add a small delay to ensure the bottom sheet is opened first
|
|
232
625
|
setTimeout(() => {
|
|
233
626
|
if (typeof screenOrConfig === 'string') {
|
|
627
|
+
// Simple screen name
|
|
628
|
+
console.log('Navigating to screen:', screenOrConfig);
|
|
234
629
|
bottomSheetRef.current?._navigateToScreen?.(screenOrConfig);
|
|
235
630
|
} else {
|
|
631
|
+
// Screen with props
|
|
632
|
+
console.log('Navigating to screen with props:', screenOrConfig.screen, screenOrConfig.props);
|
|
236
633
|
bottomSheetRef.current?._navigateToScreen?.(screenOrConfig.screen, screenOrConfig.props);
|
|
237
634
|
}
|
|
238
635
|
}, 100);
|
|
239
636
|
}
|
|
637
|
+
} else {
|
|
638
|
+
console.warn('bottomSheetRef is not available');
|
|
240
639
|
}
|
|
241
640
|
}, [bottomSheetRef]);
|
|
242
641
|
|
|
@@ -246,22 +645,59 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
246
645
|
}
|
|
247
646
|
}, [bottomSheetRef]);
|
|
248
647
|
|
|
648
|
+
// API URL configuration
|
|
649
|
+
const setApiUrl = useCallback((url: string) => {
|
|
650
|
+
try {
|
|
651
|
+
// Validate URL
|
|
652
|
+
if (!url) {
|
|
653
|
+
throw new Error('Base URL cannot be empty');
|
|
654
|
+
}
|
|
655
|
+
// Only update the app-specific base URL, not the OxyServices base URL
|
|
656
|
+
// This ensures internal module calls to Oxy API remain unaffected
|
|
657
|
+
setAppBaseURL(url);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
console.error('Failed to update API URL:', error);
|
|
660
|
+
setError(`Failed to update API URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
661
|
+
}
|
|
662
|
+
}, []);
|
|
663
|
+
|
|
664
|
+
// Get current app base URL
|
|
665
|
+
const getAppBaseURL = useCallback(() => {
|
|
666
|
+
return appBaseURL;
|
|
667
|
+
}, [appBaseURL]);
|
|
668
|
+
|
|
669
|
+
// Compute comprehensive authentication status
|
|
670
|
+
// This is the single source of truth for authentication across the entire app
|
|
671
|
+
const isAuthenticated = useMemo(() => {
|
|
672
|
+
// User is authenticated if:
|
|
673
|
+
// 1. We have a full user object loaded, OR
|
|
674
|
+
// 2. We have an active session (token will be fetched on-demand)
|
|
675
|
+
// This covers both the loaded state and the loading-but-authenticated state
|
|
676
|
+
return !!user || !!activeSessionId;
|
|
677
|
+
}, [user, activeSessionId]);
|
|
678
|
+
|
|
679
|
+
// Context value
|
|
249
680
|
const contextValue: OxyContextState = {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
681
|
+
user,
|
|
682
|
+
minimalUser,
|
|
683
|
+
sessions,
|
|
684
|
+
activeSessionId,
|
|
685
|
+
isAuthenticated,
|
|
686
|
+
isLoading,
|
|
687
|
+
error,
|
|
256
688
|
login,
|
|
257
689
|
logout,
|
|
690
|
+
logoutAll,
|
|
258
691
|
signUp,
|
|
259
|
-
refreshSessions,
|
|
260
692
|
switchSession,
|
|
261
693
|
removeSession,
|
|
262
|
-
|
|
694
|
+
refreshSessions,
|
|
695
|
+
getDeviceSessions,
|
|
696
|
+
logoutAllDeviceSessions,
|
|
697
|
+
updateDeviceName,
|
|
263
698
|
oxyServices,
|
|
264
|
-
|
|
699
|
+
setApiUrl,
|
|
700
|
+
getAppBaseURL,
|
|
265
701
|
bottomSheetRef,
|
|
266
702
|
showBottomSheet,
|
|
267
703
|
hideBottomSheet,
|
|
@@ -274,6 +710,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
274
710
|
);
|
|
275
711
|
};
|
|
276
712
|
|
|
713
|
+
// Hook to use the context
|
|
277
714
|
export const useOxy = (): OxyContextState => {
|
|
278
715
|
const context = useContext(OxyContext);
|
|
279
716
|
if (!context) {
|