@oxyhq/services 5.1.33 → 5.1.34
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/README.md +67 -2
- package/lib/commonjs/core/index.js +56 -1
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +351 -41
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/index.js +8 -0
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/navigation/OxyRouter.js +10 -0
- package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +25 -2
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +382 -0
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/SessionManagementScreen.js +448 -0
- package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -0
- package/lib/module/core/index.js +56 -1
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +351 -40
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/index.js +1 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/navigation/OxyRouter.js +10 -0
- package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
- package/lib/module/ui/screens/AccountCenterScreen.js +25 -2
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +377 -0
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -0
- package/lib/module/ui/screens/SessionManagementScreen.js +443 -0
- package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -0
- package/lib/typescript/core/index.d.ts +31 -1
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +12 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/index.d.ts +1 -0
- package/lib/typescript/ui/index.d.ts.map +1 -1
- package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/SessionManagementScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/index.ts +56 -1
- package/src/index.ts +2 -0
- package/src/ui/context/OxyContext.tsx +397 -46
- package/src/ui/index.ts +1 -0
- package/src/ui/navigation/OxyRouter.tsx +10 -0
- package/src/ui/screens/AccountCenterScreen.tsx +20 -1
- package/src/ui/screens/AccountSwitcherScreen.tsx +380 -0
- package/src/ui/screens/SessionManagementScreen.tsx +479 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionManagementScreen.d.ts","sourceRoot":"","sources":["../../../../src/ui/screens/SessionManagementScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAYnD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAiBtD,QAAA,MAAM,uBAAuB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAgUtD,CAAC;AAiIF,eAAe,uBAAuB,CAAC"}
|
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from '../models/interfaces';
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Default cloud URL for Oxy services
|
|
44
|
+
* Default cloud URL for Oxy services, cloud is where the user files are. (e.g. images, videos, etc.). Not the API.
|
|
45
45
|
*/
|
|
46
46
|
export const OXY_CLOUD_URL = 'https://cloud.oxy.so';
|
|
47
47
|
|
|
@@ -277,6 +277,61 @@ export class OxyServices {
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/* Session Management Methods */
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get active sessions for the authenticated user
|
|
284
|
+
* @returns Array of active session objects
|
|
285
|
+
*/
|
|
286
|
+
async getUserSessions(): Promise<any[]> {
|
|
287
|
+
try {
|
|
288
|
+
const res = await this.client.get('/sessions');
|
|
289
|
+
return res.data;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw this.handleError(error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Logout from a specific session
|
|
297
|
+
* @param sessionId - The session ID to logout from
|
|
298
|
+
* @returns Success status
|
|
299
|
+
*/
|
|
300
|
+
async logoutSession(sessionId: string): Promise<{ success: boolean; message: string }> {
|
|
301
|
+
try {
|
|
302
|
+
const res = await this.client.delete(`/sessions/${sessionId}`);
|
|
303
|
+
return res.data;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw this.handleError(error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Logout from all other sessions (keep current session active)
|
|
311
|
+
* @returns Success status
|
|
312
|
+
*/
|
|
313
|
+
async logoutOtherSessions(): Promise<{ success: boolean; message: string }> {
|
|
314
|
+
try {
|
|
315
|
+
const res = await this.client.post('/sessions/logout-others');
|
|
316
|
+
return res.data;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
throw this.handleError(error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Logout from all sessions
|
|
324
|
+
* @returns Success status
|
|
325
|
+
*/
|
|
326
|
+
async logoutAllSessions(): Promise<{ success: boolean; message: string }> {
|
|
327
|
+
try {
|
|
328
|
+
const res = await this.client.post('/sessions/logout-all');
|
|
329
|
+
return res.data;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw this.handleError(error);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
280
335
|
/* Profile Methods */
|
|
281
336
|
|
|
282
337
|
/**
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SignInScreen,
|
|
18
18
|
SignUpScreen,
|
|
19
19
|
AccountCenterScreen,
|
|
20
|
+
SessionManagementScreen,
|
|
20
21
|
|
|
21
22
|
// Components
|
|
22
23
|
OxySignInButton,
|
|
@@ -50,6 +51,7 @@ export {
|
|
|
50
51
|
SignInScreen,
|
|
51
52
|
SignUpScreen,
|
|
52
53
|
AccountCenterScreen,
|
|
54
|
+
SessionManagementScreen,
|
|
53
55
|
|
|
54
56
|
// Components
|
|
55
57
|
OxySignInButton,
|
|
@@ -2,19 +2,34 @@ import React, { createContext, useContext, useState, useEffect, useCallback, Rea
|
|
|
2
2
|
import { OxyServices } from '../../core';
|
|
3
3
|
import { User, LoginResponse } from '../../models/interfaces';
|
|
4
4
|
|
|
5
|
+
// Define authenticated user with tokens
|
|
6
|
+
export interface AuthenticatedUser extends User {
|
|
7
|
+
accessToken: string;
|
|
8
|
+
refreshToken?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
// Define the context shape
|
|
6
13
|
export interface OxyContextState {
|
|
7
|
-
//
|
|
8
|
-
user: User | null;
|
|
14
|
+
// Multi-user authentication state
|
|
15
|
+
user: User | null; // Current active user
|
|
16
|
+
users: AuthenticatedUser[]; // All authenticated users
|
|
9
17
|
isAuthenticated: boolean;
|
|
10
18
|
isLoading: boolean;
|
|
11
19
|
error: string | null;
|
|
12
20
|
|
|
13
21
|
// Auth methods
|
|
14
22
|
login: (username: string, password: string) => Promise<User>;
|
|
15
|
-
logout: () => Promise<void>;
|
|
23
|
+
logout: (userId?: string) => Promise<void>; // Optional userId for multi-user logout
|
|
24
|
+
logoutAll: () => Promise<void>; // Logout all users
|
|
16
25
|
signUp: (username: string, email: string, password: string) => Promise<User>;
|
|
17
26
|
|
|
27
|
+
// Multi-user methods
|
|
28
|
+
switchUser: (userId: string) => Promise<void>;
|
|
29
|
+
removeUser: (userId: string) => Promise<void>;
|
|
30
|
+
getUserSessions: (userId?: string) => Promise<any[]>; // Get sessions for user
|
|
31
|
+
logoutSession: (sessionId: string, userId?: string) => Promise<void>; // Logout specific session
|
|
32
|
+
|
|
18
33
|
// Access to services
|
|
19
34
|
oxyServices: OxyServices;
|
|
20
35
|
bottomSheetRef?: React.RefObject<any>;
|
|
@@ -94,6 +109,9 @@ const getStorage = async (): Promise<StorageInterface> => {
|
|
|
94
109
|
|
|
95
110
|
// Storage keys
|
|
96
111
|
const getStorageKeys = (prefix = 'oxy') => ({
|
|
112
|
+
users: `${prefix}_users`, // Array of authenticated users with tokens
|
|
113
|
+
activeUserId: `${prefix}_active_user_id`, // ID of currently active user
|
|
114
|
+
// Legacy keys for migration
|
|
97
115
|
accessToken: `${prefix}_access_token`,
|
|
98
116
|
refreshToken: `${prefix}_refresh_token`,
|
|
99
117
|
user: `${prefix}_user`,
|
|
@@ -108,6 +126,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
108
126
|
}) => {
|
|
109
127
|
// Authentication state
|
|
110
128
|
const [user, setUser] = useState<User | null>(null);
|
|
129
|
+
const [users, setUsers] = useState<AuthenticatedUser[]>([]);
|
|
111
130
|
const [isLoading, setIsLoading] = useState(true);
|
|
112
131
|
const [error, setError] = useState<string | null>(null);
|
|
113
132
|
const [storage, setStorage] = useState<StorageInterface | null>(null);
|
|
@@ -137,41 +156,41 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
137
156
|
|
|
138
157
|
setIsLoading(true);
|
|
139
158
|
try {
|
|
140
|
-
//
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
// Check for multi-user data first
|
|
160
|
+
const usersData = await storage.getItem(keys.users);
|
|
161
|
+
const activeUserId = await storage.getItem(keys.activeUserId);
|
|
162
|
+
|
|
163
|
+
if (usersData) {
|
|
164
|
+
// Multi-user setup exists
|
|
165
|
+
const parsedUsers: AuthenticatedUser[] = JSON.parse(usersData);
|
|
166
|
+
setUsers(parsedUsers);
|
|
167
|
+
|
|
168
|
+
if (activeUserId && parsedUsers.length > 0) {
|
|
169
|
+
const activeUser = parsedUsers.find(u => u._id === activeUserId);
|
|
170
|
+
if (activeUser) {
|
|
171
|
+
setUser(activeUser);
|
|
172
|
+
oxyServices.setTokens(activeUser.accessToken, activeUser.refreshToken || activeUser.accessToken);
|
|
173
|
+
|
|
174
|
+
// Validate the tokens
|
|
175
|
+
const isValid = await oxyServices.validate();
|
|
176
|
+
if (!isValid) {
|
|
177
|
+
// Remove invalid user
|
|
178
|
+
await removeUser(activeUser._id);
|
|
179
|
+
} else {
|
|
180
|
+
// Notify about auth state change
|
|
181
|
+
if (onAuthStateChange) {
|
|
182
|
+
onAuthStateChange(activeUser);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
165
185
|
}
|
|
166
|
-
} else {
|
|
167
|
-
// Tokens are invalid, clear everything
|
|
168
|
-
await clearStorage();
|
|
169
|
-
oxyServices.clearTokens();
|
|
170
186
|
}
|
|
187
|
+
} else {
|
|
188
|
+
// Check for legacy single-user data and migrate
|
|
189
|
+
await migrateLegacyAuth();
|
|
171
190
|
}
|
|
172
191
|
} catch (err) {
|
|
173
192
|
console.error('Auth initialization error:', err);
|
|
174
|
-
await
|
|
193
|
+
await clearAllStorage();
|
|
175
194
|
oxyServices.clearTokens();
|
|
176
195
|
} finally {
|
|
177
196
|
setIsLoading(false);
|
|
@@ -181,9 +200,57 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
181
200
|
if (storage) {
|
|
182
201
|
initAuth();
|
|
183
202
|
}
|
|
184
|
-
}, [storage, oxyServices, keys.
|
|
203
|
+
}, [storage, oxyServices, keys.users, keys.activeUserId, onAuthStateChange]);
|
|
204
|
+
|
|
205
|
+
// Migrate legacy single-user authentication to multi-user
|
|
206
|
+
const migrateLegacyAuth = async (): Promise<void> => {
|
|
207
|
+
if (!storage) return;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const accessToken = await storage.getItem(keys.accessToken);
|
|
211
|
+
const refreshToken = await storage.getItem(keys.refreshToken);
|
|
212
|
+
const storedUser = await storage.getItem(keys.user);
|
|
213
|
+
|
|
214
|
+
if (accessToken && storedUser) {
|
|
215
|
+
// Set tokens in OxyServices
|
|
216
|
+
oxyServices.setTokens(accessToken, refreshToken || accessToken);
|
|
217
|
+
|
|
218
|
+
// Validate the tokens
|
|
219
|
+
const isValid = await oxyServices.validate();
|
|
220
|
+
|
|
221
|
+
if (isValid) {
|
|
222
|
+
const parsedUser = JSON.parse(storedUser);
|
|
223
|
+
const authenticatedUser: AuthenticatedUser = {
|
|
224
|
+
...parsedUser,
|
|
225
|
+
accessToken,
|
|
226
|
+
refreshToken: refreshToken || undefined,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Store in new multi-user format
|
|
230
|
+
await storage.setItem(keys.users, JSON.stringify([authenticatedUser]));
|
|
231
|
+
await storage.setItem(keys.activeUserId, authenticatedUser._id);
|
|
232
|
+
|
|
233
|
+
// Set state
|
|
234
|
+
setUsers([authenticatedUser]);
|
|
235
|
+
setUser(authenticatedUser);
|
|
236
|
+
|
|
237
|
+
// Notify about auth state change
|
|
238
|
+
if (onAuthStateChange) {
|
|
239
|
+
onAuthStateChange(authenticatedUser);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
185
242
|
|
|
186
|
-
|
|
243
|
+
// Clear legacy storage
|
|
244
|
+
await storage.removeItem(keys.accessToken);
|
|
245
|
+
await storage.removeItem(keys.refreshToken);
|
|
246
|
+
await storage.removeItem(keys.user);
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('Migration error:', err);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Helper to clear legacy storage
|
|
187
254
|
const clearStorage = async (): Promise<void> => {
|
|
188
255
|
if (!storage) return;
|
|
189
256
|
|
|
@@ -196,6 +263,32 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
196
263
|
}
|
|
197
264
|
};
|
|
198
265
|
|
|
266
|
+
// Helper to clear all storage (multi-user)
|
|
267
|
+
const clearAllStorage = async (): Promise<void> => {
|
|
268
|
+
if (!storage) return;
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await storage.removeItem(keys.users);
|
|
272
|
+
await storage.removeItem(keys.activeUserId);
|
|
273
|
+
// Also clear legacy keys
|
|
274
|
+
await clearStorage();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('Clear all storage error:', err);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Save users to storage
|
|
281
|
+
const saveUsersToStorage = async (usersList: AuthenticatedUser[]): Promise<void> => {
|
|
282
|
+
if (!storage) return;
|
|
283
|
+
await storage.setItem(keys.users, JSON.stringify(usersList));
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Save active user ID to storage
|
|
287
|
+
const saveActiveUserId = async (userId: string): Promise<void> => {
|
|
288
|
+
if (!storage) return;
|
|
289
|
+
await storage.setItem(keys.activeUserId, userId);
|
|
290
|
+
};
|
|
291
|
+
|
|
199
292
|
// Utility function to handle different token response formats
|
|
200
293
|
const storeTokens = async (response: any) => {
|
|
201
294
|
// Store token and user data
|
|
@@ -211,7 +304,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
211
304
|
await storage?.setItem(keys.user, JSON.stringify(response.user));
|
|
212
305
|
};
|
|
213
306
|
|
|
214
|
-
// Login method
|
|
307
|
+
// Login method (updated for multi-user)
|
|
215
308
|
const login = async (username: string, password: string): Promise<User> => {
|
|
216
309
|
if (!storage) throw new Error('Storage not initialized');
|
|
217
310
|
|
|
@@ -220,17 +313,46 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
220
313
|
|
|
221
314
|
try {
|
|
222
315
|
const response = await oxyServices.login(username, password);
|
|
223
|
-
|
|
316
|
+
const accessToken = response.accessToken || (response as any).token;
|
|
317
|
+
|
|
318
|
+
if (!accessToken) {
|
|
319
|
+
throw new Error('No access token received from login');
|
|
320
|
+
}
|
|
224
321
|
|
|
225
|
-
|
|
226
|
-
|
|
322
|
+
const newUser: AuthenticatedUser = {
|
|
323
|
+
...response.user,
|
|
324
|
+
accessToken,
|
|
325
|
+
refreshToken: response.refreshToken,
|
|
326
|
+
// sessionId will be set by backend, but we don't get it in response yet
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Check if user already exists
|
|
330
|
+
const existingUserIndex = users.findIndex(u => u._id === newUser._id);
|
|
331
|
+
let updatedUsers: AuthenticatedUser[];
|
|
332
|
+
|
|
333
|
+
if (existingUserIndex >= 0) {
|
|
334
|
+
// Update existing user
|
|
335
|
+
updatedUsers = [...users];
|
|
336
|
+
updatedUsers[existingUserIndex] = newUser;
|
|
337
|
+
} else {
|
|
338
|
+
// Add new user
|
|
339
|
+
updatedUsers = [...users, newUser];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Update state
|
|
343
|
+
setUsers(updatedUsers);
|
|
344
|
+
setUser(newUser);
|
|
345
|
+
|
|
346
|
+
// Save to storage
|
|
347
|
+
await saveUsersToStorage(updatedUsers);
|
|
348
|
+
await saveActiveUserId(newUser._id);
|
|
227
349
|
|
|
228
350
|
// Notify about auth state change
|
|
229
351
|
if (onAuthStateChange) {
|
|
230
|
-
onAuthStateChange(
|
|
352
|
+
onAuthStateChange(newUser);
|
|
231
353
|
}
|
|
232
354
|
|
|
233
|
-
return
|
|
355
|
+
return newUser;
|
|
234
356
|
} catch (err: any) {
|
|
235
357
|
setError(err.message || 'Login failed');
|
|
236
358
|
throw err;
|
|
@@ -239,30 +361,242 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
239
361
|
}
|
|
240
362
|
};
|
|
241
363
|
|
|
242
|
-
// Logout method
|
|
243
|
-
const logout = async (): Promise<void> => {
|
|
364
|
+
// Logout method (supports multi-user)
|
|
365
|
+
const logout = async (userId?: string): Promise<void> => {
|
|
244
366
|
if (!storage) throw new Error('Storage not initialized');
|
|
245
367
|
|
|
246
368
|
setIsLoading(true);
|
|
247
369
|
setError(null);
|
|
248
370
|
|
|
249
371
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
372
|
+
const targetUserId = userId || user?._id;
|
|
373
|
+
if (!targetUserId) return;
|
|
374
|
+
|
|
375
|
+
const targetUser = users.find(u => u._id === targetUserId);
|
|
376
|
+
if (targetUser) {
|
|
377
|
+
// Set the target user's tokens to logout
|
|
378
|
+
oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
await oxyServices.logout();
|
|
382
|
+
} catch (logoutError) {
|
|
383
|
+
console.warn('Logout API call failed:', logoutError);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Remove user from list
|
|
387
|
+
const updatedUsers = users.filter(u => u._id !== targetUserId);
|
|
388
|
+
setUsers(updatedUsers);
|
|
389
|
+
|
|
390
|
+
// If logging out current user, switch to another user or clear
|
|
391
|
+
if (targetUserId === user?._id) {
|
|
392
|
+
if (updatedUsers.length > 0) {
|
|
393
|
+
// Switch to first available user
|
|
394
|
+
const nextUser = updatedUsers[0];
|
|
395
|
+
setUser(nextUser);
|
|
396
|
+
oxyServices.setTokens(nextUser.accessToken, nextUser.refreshToken || nextUser.accessToken);
|
|
397
|
+
await saveActiveUserId(nextUser._id);
|
|
398
|
+
|
|
399
|
+
if (onAuthStateChange) {
|
|
400
|
+
onAuthStateChange(nextUser);
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
// No users left
|
|
404
|
+
setUser(null);
|
|
405
|
+
oxyServices.clearTokens();
|
|
406
|
+
await storage.removeItem(keys.activeUserId);
|
|
407
|
+
|
|
408
|
+
if (onAuthStateChange) {
|
|
409
|
+
onAuthStateChange(null);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Save updated users list
|
|
415
|
+
await saveUsersToStorage(updatedUsers);
|
|
416
|
+
}
|
|
417
|
+
} catch (err: any) {
|
|
418
|
+
setError(err.message || 'Logout failed');
|
|
419
|
+
throw err;
|
|
420
|
+
} finally {
|
|
421
|
+
setIsLoading(false);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Logout all users
|
|
426
|
+
const logoutAll = async (): Promise<void> => {
|
|
427
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
428
|
+
|
|
429
|
+
setIsLoading(true);
|
|
430
|
+
setError(null);
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
// Logout each user
|
|
434
|
+
for (const userItem of users) {
|
|
435
|
+
try {
|
|
436
|
+
oxyServices.setTokens(userItem.accessToken, userItem.refreshToken || userItem.accessToken);
|
|
437
|
+
await oxyServices.logout();
|
|
438
|
+
} catch (logoutError) {
|
|
439
|
+
console.warn(`Logout failed for user ${userItem._id}:`, logoutError);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Clear all state and storage
|
|
444
|
+
setUsers([]);
|
|
252
445
|
setUser(null);
|
|
446
|
+
oxyServices.clearTokens();
|
|
447
|
+
await clearAllStorage();
|
|
253
448
|
|
|
254
449
|
// Notify about auth state change
|
|
255
450
|
if (onAuthStateChange) {
|
|
256
451
|
onAuthStateChange(null);
|
|
257
452
|
}
|
|
258
453
|
} catch (err: any) {
|
|
259
|
-
setError(err.message || 'Logout failed');
|
|
454
|
+
setError(err.message || 'Logout all failed');
|
|
260
455
|
throw err;
|
|
261
456
|
} finally {
|
|
262
457
|
setIsLoading(false);
|
|
263
458
|
}
|
|
264
459
|
};
|
|
265
460
|
|
|
461
|
+
// Switch user
|
|
462
|
+
const switchUser = async (userId: string): Promise<void> => {
|
|
463
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
464
|
+
|
|
465
|
+
setError(null);
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const targetUser = users.find(u => u._id === userId);
|
|
469
|
+
if (!targetUser) {
|
|
470
|
+
throw new Error('User not found');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Validate tokens before switching
|
|
474
|
+
oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
|
|
475
|
+
const isValid = await oxyServices.validate();
|
|
476
|
+
|
|
477
|
+
if (!isValid) {
|
|
478
|
+
// Remove invalid user
|
|
479
|
+
await removeUser(userId);
|
|
480
|
+
throw new Error('User session is invalid');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Switch to the user
|
|
484
|
+
setUser(targetUser);
|
|
485
|
+
await saveActiveUserId(userId);
|
|
486
|
+
|
|
487
|
+
// Notify about auth state change
|
|
488
|
+
if (onAuthStateChange) {
|
|
489
|
+
onAuthStateChange(targetUser);
|
|
490
|
+
}
|
|
491
|
+
} catch (err: any) {
|
|
492
|
+
setError(err.message || 'Switch user failed');
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Remove user
|
|
498
|
+
const removeUser = async (userId: string): Promise<void> => {
|
|
499
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const updatedUsers = users.filter(u => u._id !== userId);
|
|
503
|
+
setUsers(updatedUsers);
|
|
504
|
+
|
|
505
|
+
// If removing current user, switch to another or clear
|
|
506
|
+
if (userId === user?._id) {
|
|
507
|
+
if (updatedUsers.length > 0) {
|
|
508
|
+
await switchUser(updatedUsers[0]._id);
|
|
509
|
+
} else {
|
|
510
|
+
setUser(null);
|
|
511
|
+
oxyServices.clearTokens();
|
|
512
|
+
await storage.removeItem(keys.activeUserId);
|
|
513
|
+
|
|
514
|
+
if (onAuthStateChange) {
|
|
515
|
+
onAuthStateChange(null);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Save updated users list
|
|
521
|
+
await saveUsersToStorage(updatedUsers);
|
|
522
|
+
} catch (err: any) {
|
|
523
|
+
setError(err.message || 'Remove user failed');
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Get user sessions
|
|
529
|
+
const getUserSessions = async (userId?: string): Promise<any[]> => {
|
|
530
|
+
try {
|
|
531
|
+
const targetUserId = userId || user?._id;
|
|
532
|
+
if (!targetUserId) return [];
|
|
533
|
+
|
|
534
|
+
const targetUser = users.find(u => u._id === targetUserId);
|
|
535
|
+
if (!targetUser) return [];
|
|
536
|
+
|
|
537
|
+
// Store current tokens to restore later
|
|
538
|
+
const currentUser = user;
|
|
539
|
+
const wasCurrentUser = targetUserId === user?._id;
|
|
540
|
+
|
|
541
|
+
if (!wasCurrentUser) {
|
|
542
|
+
// Temporarily switch to target user's tokens
|
|
543
|
+
oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
// Use the new OxyServices method
|
|
548
|
+
const sessions = await oxyServices.getUserSessions();
|
|
549
|
+
return sessions;
|
|
550
|
+
} finally {
|
|
551
|
+
if (!wasCurrentUser && currentUser) {
|
|
552
|
+
// Restore original tokens
|
|
553
|
+
oxyServices.setTokens(currentUser.accessToken, currentUser.refreshToken || currentUser.accessToken);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
} catch (err: any) {
|
|
557
|
+
console.error('Get user sessions failed:', err);
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Logout specific session
|
|
563
|
+
const logoutSession = async (sessionId: string, userId?: string): Promise<void> => {
|
|
564
|
+
try {
|
|
565
|
+
const targetUserId = userId || user?._id;
|
|
566
|
+
if (!targetUserId) return;
|
|
567
|
+
|
|
568
|
+
const targetUser = users.find(u => u._id === targetUserId);
|
|
569
|
+
if (!targetUser) return;
|
|
570
|
+
|
|
571
|
+
// Store current tokens to restore later
|
|
572
|
+
const currentUser = user;
|
|
573
|
+
const wasCurrentUser = targetUserId === user?._id;
|
|
574
|
+
|
|
575
|
+
if (!wasCurrentUser) {
|
|
576
|
+
// Temporarily switch to target user's tokens
|
|
577
|
+
oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
// Use the new OxyServices method
|
|
582
|
+
await oxyServices.logoutSession(sessionId);
|
|
583
|
+
|
|
584
|
+
// If this is the current user's session, remove them from local state
|
|
585
|
+
if (wasCurrentUser && sessionId === targetUser.sessionId) {
|
|
586
|
+
await removeUser(targetUserId);
|
|
587
|
+
}
|
|
588
|
+
} finally {
|
|
589
|
+
if (!wasCurrentUser && currentUser) {
|
|
590
|
+
// Restore original tokens
|
|
591
|
+
oxyServices.setTokens(currentUser.accessToken, currentUser.refreshToken || currentUser.accessToken);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} catch (err: any) {
|
|
595
|
+
console.error('Logout session failed:', err);
|
|
596
|
+
throw err;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
266
600
|
// Sign up method
|
|
267
601
|
const signUp = async (username: string, email: string, password: string): Promise<User> => {
|
|
268
602
|
if (!storage) throw new Error('Storage not initialized');
|
|
@@ -322,14 +656,31 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
322
656
|
|
|
323
657
|
// Build context value
|
|
324
658
|
const contextValue: OxyContextState = {
|
|
659
|
+
// Single user state (current active user)
|
|
325
660
|
user,
|
|
326
661
|
isAuthenticated: !!user,
|
|
327
662
|
isLoading,
|
|
328
663
|
error,
|
|
664
|
+
|
|
665
|
+
// Multi-user state
|
|
666
|
+
users,
|
|
667
|
+
|
|
668
|
+
// Auth methods
|
|
329
669
|
login,
|
|
330
670
|
logout,
|
|
671
|
+
logoutAll,
|
|
331
672
|
signUp,
|
|
673
|
+
|
|
674
|
+
// Multi-user methods
|
|
675
|
+
switchUser,
|
|
676
|
+
removeUser,
|
|
677
|
+
getUserSessions,
|
|
678
|
+
logoutSession,
|
|
679
|
+
|
|
680
|
+
// OxyServices instance
|
|
332
681
|
oxyServices,
|
|
682
|
+
|
|
683
|
+
// Bottom sheet methods
|
|
333
684
|
bottomSheetRef,
|
|
334
685
|
showBottomSheet,
|
|
335
686
|
hideBottomSheet,
|
package/src/ui/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { fontFamilies, fontStyles } from './styles/fonts';
|
|
|
23
23
|
export { default as SignInScreen } from './screens/SignInScreen';
|
|
24
24
|
export { default as SignUpScreen } from './screens/SignUpScreen';
|
|
25
25
|
export { default as AccountCenterScreen } from './screens/AccountCenterScreen';
|
|
26
|
+
export { default as SessionManagementScreen } from './screens/SessionManagementScreen';
|
|
26
27
|
export { default as AccountOverviewScreen } from './screens/AccountOverviewScreen';
|
|
27
28
|
export { default as AccountSettingsScreen } from './screens/AccountSettingsScreen';
|
|
28
29
|
|