@oxyhq/services 5.16.12 → 5.16.14
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/mixins/OxyServices.security.js +54 -0
- package/lib/commonjs/core/mixins/OxyServices.security.js.map +1 -0
- package/lib/commonjs/core/mixins/index.js +3 -2
- package/lib/commonjs/core/mixins/index.js.map +1 -1
- package/lib/commonjs/crypto/keyManager.js +87 -22
- package/lib/commonjs/crypto/keyManager.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/TextFieldFlat.js +3 -1
- package/lib/commonjs/ui/components/TextField/TextFieldFlat.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js +3 -1
- package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +28 -36
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useSessionManagement.js +1 -1
- package/lib/commonjs/ui/context/utils/errorHandlers.js +10 -0
- package/lib/commonjs/ui/context/utils/errorHandlers.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +16 -2
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/index.js +13 -0
- package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +6 -0
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useSecurityQueries.js +58 -0
- package/lib/commonjs/ui/hooks/queries/useSecurityQueries.js.map +1 -0
- package/lib/commonjs/ui/utils/avatarUtils.js +8 -5
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.security.js +50 -0
- package/lib/module/core/mixins/OxyServices.security.js.map +1 -0
- package/lib/module/core/mixins/index.js +2 -1
- package/lib/module/core/mixins/index.js.map +1 -1
- package/lib/module/crypto/keyManager.js +87 -22
- package/lib/module/crypto/keyManager.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/TextField/TextFieldFlat.js +3 -1
- package/lib/module/ui/components/TextField/TextFieldFlat.js.map +1 -1
- package/lib/module/ui/components/TextField/TextFieldOutlined.js +3 -1
- package/lib/module/ui/components/TextField/TextFieldOutlined.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +23 -30
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useSessionManagement.js +1 -1
- package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/context/utils/errorHandlers.js +10 -0
- package/lib/module/ui/context/utils/errorHandlers.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +16 -2
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/index.js +3 -0
- package/lib/module/ui/hooks/queries/index.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +6 -0
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/queries/useSecurityQueries.js +52 -0
- package/lib/module/ui/hooks/queries/useSecurityQueries.js.map +1 -0
- package/lib/module/ui/utils/avatarUtils.js +8 -5
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.security.d.ts +67 -0
- package/lib/typescript/core/mixins/OxyServices.security.d.ts.map +1 -0
- package/lib/typescript/core/mixins/index.d.ts +48 -0
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/crypto/keyManager.d.ts +9 -2
- package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +34 -4
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/utils/errorHandlers.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/index.d.ts +1 -0
- package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +5 -0
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useSecurityQueries.d.ts +15 -0
- package/lib/typescript/ui/hooks/queries/useSecurityQueries.d.ts.map +1 -0
- package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/core/mixins/OxyServices.security.ts +59 -0
- package/src/core/mixins/index.ts +14 -11
- package/src/crypto/keyManager.ts +92 -16
- package/src/index.ts +6 -1
- package/src/models/interfaces.ts +48 -5
- package/src/ui/components/TextField/TextFieldFlat.tsx +1 -1
- package/src/ui/components/TextField/TextFieldOutlined.tsx +1 -1
- package/src/ui/context/OxyContext.tsx +26 -28
- package/src/ui/context/hooks/useSessionManagement.ts +1 -1
- package/src/ui/context/utils/errorHandlers.ts +10 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +17 -2
- package/src/ui/hooks/queries/index.ts +6 -0
- package/src/ui/hooks/queries/queryKeys.ts +9 -0
- package/src/ui/hooks/queries/useSecurityQueries.ts +64 -0
- package/src/ui/utils/avatarUtils.ts +8 -5
package/src/crypto/keyManager.ts
CHANGED
|
@@ -28,7 +28,15 @@ const STORAGE_KEYS = {
|
|
|
28
28
|
*/
|
|
29
29
|
async function initSecureStore(): Promise<typeof import('expo-secure-store')> {
|
|
30
30
|
if (!SecureStore) {
|
|
31
|
-
|
|
31
|
+
try {
|
|
32
|
+
SecureStore = await import('expo-secure-store');
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
35
|
+
throw new Error(`Failed to load expo-secure-store: ${errorMessage}. Make sure expo-secure-store is installed and properly configured.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!SecureStore) {
|
|
39
|
+
throw new Error('expo-secure-store module is not available');
|
|
32
40
|
}
|
|
33
41
|
return SecureStore;
|
|
34
42
|
}
|
|
@@ -95,6 +103,19 @@ export interface KeyPair {
|
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
export class KeyManager {
|
|
106
|
+
// In-memory cache for identity state (invalidated on identity changes)
|
|
107
|
+
private static cachedPublicKey: string | null = null;
|
|
108
|
+
private static cachedHasIdentity: boolean | null = null;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Invalidate cached identity state
|
|
112
|
+
* Called internally when identity is created/deleted/imported
|
|
113
|
+
*/
|
|
114
|
+
private static invalidateCache(): void {
|
|
115
|
+
KeyManager.cachedPublicKey = null;
|
|
116
|
+
KeyManager.cachedHasIdentity = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
/**
|
|
99
120
|
* Generate a new ECDSA secp256k1 key pair
|
|
100
121
|
* Returns the keys in hexadecimal format
|
|
@@ -129,14 +150,16 @@ export class KeyManager {
|
|
|
129
150
|
const store = await initSecureStore();
|
|
130
151
|
const { privateKey, publicKey } = await KeyManager.generateKeyPair();
|
|
131
152
|
|
|
132
|
-
// Store private key securely
|
|
133
153
|
await store.setItemAsync(STORAGE_KEYS.PRIVATE_KEY, privateKey, {
|
|
134
154
|
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
135
155
|
});
|
|
136
156
|
|
|
137
|
-
// Store public key (for quick access without deriving)
|
|
138
157
|
await store.setItemAsync(STORAGE_KEYS.PUBLIC_KEY, publicKey);
|
|
139
158
|
|
|
159
|
+
// Update cache
|
|
160
|
+
KeyManager.cachedPublicKey = publicKey;
|
|
161
|
+
KeyManager.cachedHasIdentity = true;
|
|
162
|
+
|
|
140
163
|
return publicKey;
|
|
141
164
|
}
|
|
142
165
|
|
|
@@ -146,16 +169,18 @@ export class KeyManager {
|
|
|
146
169
|
static async importKeyPair(privateKey: string): Promise<string> {
|
|
147
170
|
const store = await initSecureStore();
|
|
148
171
|
|
|
149
|
-
// Derive public key from private key
|
|
150
172
|
const keyPair = ec.keyFromPrivate(privateKey);
|
|
151
173
|
const publicKey = keyPair.getPublic('hex');
|
|
152
174
|
|
|
153
|
-
// Store both keys
|
|
154
175
|
await store.setItemAsync(STORAGE_KEYS.PRIVATE_KEY, privateKey, {
|
|
155
176
|
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
156
177
|
});
|
|
157
178
|
await store.setItemAsync(STORAGE_KEYS.PUBLIC_KEY, publicKey);
|
|
158
179
|
|
|
180
|
+
// Update cache
|
|
181
|
+
KeyManager.cachedPublicKey = publicKey;
|
|
182
|
+
KeyManager.cachedHasIdentity = true;
|
|
183
|
+
|
|
159
184
|
return publicKey;
|
|
160
185
|
}
|
|
161
186
|
|
|
@@ -164,24 +189,71 @@ export class KeyManager {
|
|
|
164
189
|
* WARNING: Only use this for signing operations within the app
|
|
165
190
|
*/
|
|
166
191
|
static async getPrivateKey(): Promise<string | null> {
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
try {
|
|
193
|
+
const store = await initSecureStore();
|
|
194
|
+
return await store.getItemAsync(STORAGE_KEYS.PRIVATE_KEY);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// If secure store is not available, return null (no identity)
|
|
197
|
+
// This allows the app to continue functioning even if secure store fails to load
|
|
198
|
+
if (__DEV__) {
|
|
199
|
+
console.warn('[KeyManager] Failed to access secure store:', error);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
169
203
|
}
|
|
170
204
|
|
|
171
205
|
/**
|
|
172
|
-
* Get the stored public key
|
|
206
|
+
* Get the stored public key (cached for performance)
|
|
173
207
|
*/
|
|
174
208
|
static async getPublicKey(): Promise<string | null> {
|
|
175
|
-
|
|
176
|
-
|
|
209
|
+
if (KeyManager.cachedPublicKey !== null) {
|
|
210
|
+
return KeyManager.cachedPublicKey;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const store = await initSecureStore();
|
|
215
|
+
const publicKey = await store.getItemAsync(STORAGE_KEYS.PUBLIC_KEY);
|
|
216
|
+
|
|
217
|
+
// Cache result (null is a valid cache value meaning no identity)
|
|
218
|
+
KeyManager.cachedPublicKey = publicKey;
|
|
219
|
+
|
|
220
|
+
return publicKey;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// If secure store is not available, return null (no identity)
|
|
223
|
+
// Cache null to avoid repeated failed attempts
|
|
224
|
+
KeyManager.cachedPublicKey = null;
|
|
225
|
+
if (__DEV__) {
|
|
226
|
+
console.warn('[KeyManager] Failed to access secure store:', error);
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
177
230
|
}
|
|
178
231
|
|
|
179
232
|
/**
|
|
180
|
-
* Check if an identity (key pair) exists on this device
|
|
233
|
+
* Check if an identity (key pair) exists on this device (cached for performance)
|
|
181
234
|
*/
|
|
182
235
|
static async hasIdentity(): Promise<boolean> {
|
|
183
|
-
|
|
184
|
-
|
|
236
|
+
if (KeyManager.cachedHasIdentity !== null) {
|
|
237
|
+
return KeyManager.cachedHasIdentity;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const privateKey = await KeyManager.getPrivateKey();
|
|
242
|
+
const hasIdentity = privateKey !== null;
|
|
243
|
+
|
|
244
|
+
// Cache result
|
|
245
|
+
KeyManager.cachedHasIdentity = hasIdentity;
|
|
246
|
+
|
|
247
|
+
return hasIdentity;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// If we can't check, assume no identity (safer default)
|
|
250
|
+
// Cache false to avoid repeated failed attempts
|
|
251
|
+
KeyManager.cachedHasIdentity = false;
|
|
252
|
+
if (__DEV__) {
|
|
253
|
+
console.warn('[KeyManager] Failed to check identity:', error);
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
185
257
|
}
|
|
186
258
|
|
|
187
259
|
/**
|
|
@@ -228,6 +300,9 @@ export class KeyManager {
|
|
|
228
300
|
await store.deleteItemAsync(STORAGE_KEYS.PRIVATE_KEY);
|
|
229
301
|
await store.deleteItemAsync(STORAGE_KEYS.PUBLIC_KEY);
|
|
230
302
|
|
|
303
|
+
// Invalidate cache
|
|
304
|
+
KeyManager.invalidateCache();
|
|
305
|
+
|
|
231
306
|
// Also clear backup if force deletion
|
|
232
307
|
if (force) {
|
|
233
308
|
try {
|
|
@@ -343,16 +418,17 @@ export class KeyManager {
|
|
|
343
418
|
return false; // Backup keys don't match
|
|
344
419
|
}
|
|
345
420
|
|
|
346
|
-
// Restore from backup
|
|
347
421
|
await store.setItemAsync(STORAGE_KEYS.PRIVATE_KEY, backupPrivateKey, {
|
|
348
422
|
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
349
423
|
});
|
|
350
424
|
await store.setItemAsync(STORAGE_KEYS.PUBLIC_KEY, backupPublicKey);
|
|
351
425
|
|
|
352
|
-
// Verify restoration was successful
|
|
353
426
|
const restored = await KeyManager.verifyIdentityIntegrity();
|
|
354
427
|
if (restored) {
|
|
355
|
-
// Update
|
|
428
|
+
// Update cache
|
|
429
|
+
KeyManager.cachedPublicKey = backupPublicKey;
|
|
430
|
+
KeyManager.cachedHasIdentity = true;
|
|
431
|
+
|
|
356
432
|
await store.setItemAsync(STORAGE_KEYS.BACKUP_TIMESTAMP, Date.now().toString());
|
|
357
433
|
return true;
|
|
358
434
|
}
|
package/src/index.ts
CHANGED
|
@@ -103,7 +103,12 @@ export type {
|
|
|
103
103
|
AssetUpdateVisibilityResponse,
|
|
104
104
|
// Account storage usage
|
|
105
105
|
AccountStorageCategoryUsage,
|
|
106
|
-
AccountStorageUsageResponse
|
|
106
|
+
AccountStorageUsageResponse,
|
|
107
|
+
// Security activity
|
|
108
|
+
SecurityEventType,
|
|
109
|
+
SecurityEventSeverity,
|
|
110
|
+
SecurityActivity,
|
|
111
|
+
SecurityActivityResponse
|
|
107
112
|
} from './models/interfaces';
|
|
108
113
|
|
|
109
114
|
export type {
|
package/src/models/interfaces.ts
CHANGED
|
@@ -50,15 +50,11 @@ export interface User {
|
|
|
50
50
|
image?: string;
|
|
51
51
|
link: string;
|
|
52
52
|
}>;
|
|
53
|
-
// Social counts
|
|
53
|
+
// Social counts
|
|
54
54
|
_count?: {
|
|
55
55
|
followers?: number;
|
|
56
56
|
following?: number;
|
|
57
57
|
};
|
|
58
|
-
stats?: {
|
|
59
|
-
followers?: number;
|
|
60
|
-
following?: number;
|
|
61
|
-
};
|
|
62
58
|
accountExpiresAfterInactivityDays?: number | null; // Days of inactivity before account expires (null = never expire)
|
|
63
59
|
[key: string]: unknown;
|
|
64
60
|
}
|
|
@@ -407,6 +403,53 @@ export interface AccountStorageUsageResponse {
|
|
|
407
403
|
updatedAt: string;
|
|
408
404
|
}
|
|
409
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Security activity event types
|
|
408
|
+
*/
|
|
409
|
+
export type SecurityEventType =
|
|
410
|
+
| 'sign_in'
|
|
411
|
+
| 'sign_out'
|
|
412
|
+
| 'email_changed'
|
|
413
|
+
| 'profile_updated'
|
|
414
|
+
| 'device_added'
|
|
415
|
+
| 'device_removed'
|
|
416
|
+
| 'account_recovery'
|
|
417
|
+
| 'security_settings_changed'
|
|
418
|
+
| 'suspicious_activity';
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Security event severity levels
|
|
422
|
+
*/
|
|
423
|
+
export type SecurityEventSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Security activity event
|
|
427
|
+
*/
|
|
428
|
+
export interface SecurityActivity {
|
|
429
|
+
id: string;
|
|
430
|
+
userId: string;
|
|
431
|
+
eventType: SecurityEventType;
|
|
432
|
+
eventDescription: string;
|
|
433
|
+
metadata?: Record<string, any>;
|
|
434
|
+
ipAddress?: string;
|
|
435
|
+
userAgent?: string;
|
|
436
|
+
deviceId?: string;
|
|
437
|
+
timestamp: string;
|
|
438
|
+
severity: SecurityEventSeverity;
|
|
439
|
+
createdAt: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Security activity response with pagination
|
|
444
|
+
*/
|
|
445
|
+
export interface SecurityActivityResponse {
|
|
446
|
+
data: SecurityActivity[];
|
|
447
|
+
total: number;
|
|
448
|
+
limit: number;
|
|
449
|
+
offset: number;
|
|
450
|
+
hasMore: boolean;
|
|
451
|
+
}
|
|
452
|
+
|
|
410
453
|
export interface AssetUploadProgress {
|
|
411
454
|
fileId: string;
|
|
412
455
|
uploaded: number;
|
|
@@ -438,7 +438,7 @@ const TextInputFlat = ({
|
|
|
438
438
|
flexShrink: 1,
|
|
439
439
|
minWidth: 0,
|
|
440
440
|
},
|
|
441
|
-
Platform.OS === 'web' ? { outline: 'none' } : undefined,
|
|
441
|
+
Platform.OS === 'web' ? { outline: 'none', outlineWidth: 0, outlineStyle: 'none' } : undefined,
|
|
442
442
|
adornmentStyleAdjustmentForNativeInput,
|
|
443
443
|
contentStyle,
|
|
444
444
|
],
|
|
@@ -427,7 +427,7 @@ const TextInputOutlined = ({
|
|
|
427
427
|
flexShrink: 1,
|
|
428
428
|
minWidth: 0,
|
|
429
429
|
},
|
|
430
|
-
Platform.OS === 'web' ? { outline: 'none' } : undefined,
|
|
430
|
+
Platform.OS === 'web' ? { outline: 'none', outlineWidth: 0, outlineStyle: 'none' } : undefined,
|
|
431
431
|
adornmentStyleAdjustmentForNativeInput,
|
|
432
432
|
contentStyle,
|
|
433
433
|
],
|
|
@@ -201,7 +201,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
201
201
|
|
|
202
202
|
const checkAndRestoreIdentity = async () => {
|
|
203
203
|
try {
|
|
204
|
-
const { KeyManager } = await import('../../crypto/index.js');
|
|
205
204
|
// Check if identity exists and verify integrity
|
|
206
205
|
const hasIdentity = await KeyManager.hasIdentity();
|
|
207
206
|
if (hasIdentity) {
|
|
@@ -209,14 +208,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
209
208
|
if (!isValid) {
|
|
210
209
|
// Try to restore from backup
|
|
211
210
|
const restored = await KeyManager.restoreIdentityFromBackup();
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (__DEV__) {
|
|
218
|
-
logger('Identity integrity check failed - user may need to restore from recovery phrase');
|
|
219
|
-
}
|
|
211
|
+
if (__DEV__) {
|
|
212
|
+
logger(restored
|
|
213
|
+
? 'Identity restored from backup successfully'
|
|
214
|
+
: 'Identity integrity check failed - user may need to restore from recovery phrase'
|
|
215
|
+
);
|
|
220
216
|
}
|
|
221
217
|
} else {
|
|
222
218
|
// Identity is valid - ensure backup is up to date
|
|
@@ -317,9 +313,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
317
313
|
});
|
|
318
314
|
|
|
319
315
|
// syncIdentity - TanStack Query handles offline mutations automatically
|
|
320
|
-
const syncIdentity = useCallback(
|
|
321
|
-
return await syncIdentityBase();
|
|
322
|
-
}, [syncIdentityBase]);
|
|
316
|
+
const syncIdentity = useCallback(() => syncIdentityBase(), [syncIdentityBase]);
|
|
323
317
|
|
|
324
318
|
// Clear all account data when identity is lost (for accounts app)
|
|
325
319
|
// In accounts app, identity = account, so losing identity means losing everything
|
|
@@ -332,9 +326,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
332
326
|
try {
|
|
333
327
|
await clearQueryCache(storage);
|
|
334
328
|
} catch (error) {
|
|
335
|
-
|
|
336
|
-
logger('Failed to clear persisted query cache', error);
|
|
337
|
-
}
|
|
329
|
+
logger('Failed to clear persisted query cache', error);
|
|
338
330
|
}
|
|
339
331
|
}
|
|
340
332
|
|
|
@@ -346,9 +338,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
346
338
|
try {
|
|
347
339
|
await storage.removeItem('oxy_identity_synced');
|
|
348
340
|
} catch (error) {
|
|
349
|
-
|
|
350
|
-
logger('Failed to clear identity sync state', error);
|
|
351
|
-
}
|
|
341
|
+
logger('Failed to clear identity sync state', error);
|
|
352
342
|
}
|
|
353
343
|
}
|
|
354
344
|
|
|
@@ -419,20 +409,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
419
409
|
|
|
420
410
|
// If we were offline and now we're online, sync identity if needed
|
|
421
411
|
if (wasOffline) {
|
|
422
|
-
|
|
423
|
-
logger('Network reconnected, checking identity sync...');
|
|
424
|
-
}
|
|
412
|
+
logger('Network reconnected, checking identity sync...');
|
|
425
413
|
|
|
426
414
|
// Sync identity first (if not synced)
|
|
427
415
|
try {
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
430
|
-
|
|
416
|
+
const hasIdentityValue = await hasIdentity();
|
|
417
|
+
if (hasIdentityValue) {
|
|
418
|
+
// Check sync status directly - sync if not explicitly 'true'
|
|
419
|
+
// undefined = not synced yet, 'false' = explicitly not synced, 'true' = synced
|
|
420
|
+
const syncStatus = await storage.getItem('oxy_identity_synced');
|
|
421
|
+
if (syncStatus !== 'true') {
|
|
422
|
+
await syncIdentity();
|
|
423
|
+
}
|
|
431
424
|
}
|
|
432
425
|
} catch (syncError) {
|
|
433
|
-
|
|
434
|
-
logger('Error syncing identity on reconnect', syncError);
|
|
435
|
-
}
|
|
426
|
+
logger('Error syncing identity on reconnect', syncError);
|
|
436
427
|
}
|
|
437
428
|
|
|
438
429
|
// TanStack Query will automatically retry pending mutations
|
|
@@ -515,7 +506,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
515
506
|
});
|
|
516
507
|
}
|
|
517
508
|
} catch (validationError) {
|
|
518
|
-
|
|
509
|
+
// Silently handle expected 401 errors (expired/invalid sessions) during restoration
|
|
510
|
+
// Only log unexpected errors
|
|
511
|
+
if (!isInvalidSessionError(validationError)) {
|
|
512
|
+
logger('Session validation failed during init', validationError);
|
|
513
|
+
}
|
|
519
514
|
}
|
|
520
515
|
}
|
|
521
516
|
|
|
@@ -528,13 +523,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
528
523
|
try {
|
|
529
524
|
await switchSession(storedActiveSessionId);
|
|
530
525
|
} catch (switchError) {
|
|
526
|
+
// Silently handle expected 401 errors (expired/invalid active session)
|
|
531
527
|
if (isInvalidSessionError(switchError)) {
|
|
532
528
|
await storage.removeItem(storageKeys.activeSessionId);
|
|
533
529
|
updateSessions(
|
|
534
530
|
validSessions.filter((session) => session.sessionId !== storedActiveSessionId),
|
|
535
531
|
{ merge: false },
|
|
536
532
|
);
|
|
533
|
+
// Don't log expected session errors during restoration
|
|
537
534
|
} else {
|
|
535
|
+
// Only log unexpected errors
|
|
538
536
|
logger('Active session validation error', switchError);
|
|
539
537
|
}
|
|
540
538
|
}
|
|
@@ -7,7 +7,7 @@ import { getStorageKeys, type StorageInterface } from '../utils/storageHelpers';
|
|
|
7
7
|
import { handleAuthError, isInvalidSessionError } from '../utils/errorHandlers';
|
|
8
8
|
import type { OxyServices } from '../../../core';
|
|
9
9
|
import type { QueryClient } from '@tanstack/react-query';
|
|
10
|
-
import { clearQueryCache } from '../../hooks/queryClient
|
|
10
|
+
import { clearQueryCache } from '../../hooks/queryClient';
|
|
11
11
|
|
|
12
12
|
export interface UseSessionManagementOptions {
|
|
13
13
|
oxyServices: OxyServices;
|
|
@@ -53,11 +53,21 @@ export const isInvalidSessionError = (error: unknown): boolean => {
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Check error.status directly (HttpService sets this)
|
|
57
|
+
if ((error as any).status === 401) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
const normalizedMessage = extractErrorMessage(error)?.toLowerCase();
|
|
57
62
|
if (!normalizedMessage) {
|
|
58
63
|
return false;
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
// Check for HTTP 401 in message (HttpService creates errors with "HTTP 401:" format)
|
|
67
|
+
if (normalizedMessage.includes('http 401') || normalizedMessage.includes('401')) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
return DEFAULT_INVALID_SESSION_MESSAGES.some((msg) =>
|
|
62
72
|
normalizedMessage.includes(msg.toLowerCase()),
|
|
63
73
|
);
|
|
@@ -4,6 +4,7 @@ import { queryKeys, invalidateAccountQueries, invalidateUserQueries } from '../q
|
|
|
4
4
|
import { useOxy } from '../../context/OxyContext';
|
|
5
5
|
import { toast } from '../../../lib/sonner';
|
|
6
6
|
import { refreshAvatarInStore } from '../../utils/avatarUtils';
|
|
7
|
+
import { useAuthStore } from '../../stores/authStore';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Update user profile with optimistic updates and offline queue support
|
|
@@ -107,6 +108,9 @@ export const useUpdateProfile = () => {
|
|
|
107
108
|
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// Update authStore so frontend components see the changes immediately
|
|
112
|
+
useAuthStore.getState().setUser(data);
|
|
113
|
+
|
|
110
114
|
// If avatar was updated, refresh accountStore with cache-busted URL
|
|
111
115
|
if (updates.avatar && activeSessionId && oxyServices) {
|
|
112
116
|
refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
|
|
@@ -220,6 +224,9 @@ export const useUploadAvatar = () => {
|
|
|
220
224
|
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
|
|
221
225
|
}
|
|
222
226
|
|
|
227
|
+
// Update authStore so frontend components see the changes immediately
|
|
228
|
+
useAuthStore.getState().setUser(data);
|
|
229
|
+
|
|
223
230
|
// Refresh accountStore with cache-busted URL if avatar was updated
|
|
224
231
|
if (data?.avatar && activeSessionId && oxyServices) {
|
|
225
232
|
refreshAvatarInStore(activeSessionId, data.avatar, oxyServices);
|
|
@@ -268,6 +275,10 @@ export const useUpdateAccountSettings = () => {
|
|
|
268
275
|
},
|
|
269
276
|
onSuccess: (data) => {
|
|
270
277
|
queryClient.setQueryData(queryKeys.accounts.current(), data);
|
|
278
|
+
|
|
279
|
+
// Update authStore so frontend components see the changes immediately
|
|
280
|
+
useAuthStore.getState().setUser(data);
|
|
281
|
+
|
|
271
282
|
invalidateAccountQueries(queryClient);
|
|
272
283
|
toast.success('Settings updated successfully');
|
|
273
284
|
},
|
|
@@ -394,10 +405,14 @@ export const useUpdatePrivacySettings = () => {
|
|
|
394
405
|
// Also update account query if it contains privacy settings
|
|
395
406
|
const currentUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
|
|
396
407
|
if (currentUser) {
|
|
397
|
-
|
|
408
|
+
const updatedUser = {
|
|
398
409
|
...currentUser,
|
|
399
410
|
privacySettings: data,
|
|
400
|
-
}
|
|
411
|
+
};
|
|
412
|
+
queryClient.setQueryData<User>(queryKeys.accounts.current(), updatedUser);
|
|
413
|
+
|
|
414
|
+
// Update authStore so frontend components see the changes immediately
|
|
415
|
+
useAuthStore.getState().setUser(updatedUser);
|
|
401
416
|
}
|
|
402
417
|
invalidateAccountQueries(queryClient);
|
|
403
418
|
},
|
|
@@ -25,6 +25,12 @@ export {
|
|
|
25
25
|
useSecurityInfo,
|
|
26
26
|
} from './useServicesQueries';
|
|
27
27
|
|
|
28
|
+
// Security activity query hooks
|
|
29
|
+
export {
|
|
30
|
+
useSecurityActivity,
|
|
31
|
+
useRecentSecurityActivity,
|
|
32
|
+
} from './useSecurityQueries';
|
|
33
|
+
|
|
28
34
|
// Query keys and invalidation helpers (for advanced usage)
|
|
29
35
|
export { queryKeys, invalidateAccountQueries, invalidateUserQueries, invalidateSessionQueries } from './queryKeys';
|
|
30
36
|
|
|
@@ -54,6 +54,15 @@ export const queryKeys = {
|
|
|
54
54
|
all: ['privacy'] as const,
|
|
55
55
|
settings: (userId?: string) => [...queryKeys.privacy.all, 'settings', userId || 'current'] as const,
|
|
56
56
|
},
|
|
57
|
+
|
|
58
|
+
// Security activity queries
|
|
59
|
+
security: {
|
|
60
|
+
all: ['security'] as const,
|
|
61
|
+
activity: (limit?: number, offset?: number, eventType?: string) =>
|
|
62
|
+
[...queryKeys.security.all, 'activity', limit, offset, eventType] as const,
|
|
63
|
+
recent: (limit: number) =>
|
|
64
|
+
[...queryKeys.security.all, 'recent', limit] as const,
|
|
65
|
+
},
|
|
57
66
|
} as const;
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { queryKeys } from './queryKeys';
|
|
3
|
+
import { useOxy } from '../../context/OxyContext';
|
|
4
|
+
import type { SecurityActivity, SecurityEventType } from '../../../models/interfaces';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get user's security activity with pagination
|
|
8
|
+
*/
|
|
9
|
+
export const useSecurityActivity = (
|
|
10
|
+
options?: {
|
|
11
|
+
limit?: number;
|
|
12
|
+
offset?: number;
|
|
13
|
+
eventType?: SecurityEventType;
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
) => {
|
|
17
|
+
const { oxyServices, activeSessionId } = useOxy();
|
|
18
|
+
|
|
19
|
+
return useQuery({
|
|
20
|
+
queryKey: queryKeys.security.activity(
|
|
21
|
+
options?.limit,
|
|
22
|
+
options?.offset,
|
|
23
|
+
options?.eventType
|
|
24
|
+
),
|
|
25
|
+
queryFn: async () => {
|
|
26
|
+
if (!activeSessionId) {
|
|
27
|
+
throw new Error('No active session');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const response = await oxyServices.getSecurityActivity(
|
|
31
|
+
options?.limit,
|
|
32
|
+
options?.offset,
|
|
33
|
+
options?.eventType
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return response;
|
|
37
|
+
},
|
|
38
|
+
enabled: (options?.enabled !== false) && !!activeSessionId,
|
|
39
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
40
|
+
gcTime: 10 * 60 * 1000, // 10 minutes
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get recent security activity (convenience hook)
|
|
46
|
+
*/
|
|
47
|
+
export const useRecentSecurityActivity = (limit: number = 10) => {
|
|
48
|
+
const { oxyServices, activeSessionId } = useOxy();
|
|
49
|
+
|
|
50
|
+
return useQuery<SecurityActivity[]>({
|
|
51
|
+
queryKey: queryKeys.security.recent(limit),
|
|
52
|
+
queryFn: async () => {
|
|
53
|
+
if (!activeSessionId) {
|
|
54
|
+
throw new Error('No active session');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return await oxyServices.getRecentSecurityActivity(limit);
|
|
58
|
+
},
|
|
59
|
+
enabled: !!activeSessionId,
|
|
60
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
61
|
+
gcTime: 10 * 60 * 1000, // 10 minutes
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OxyServices } from '../../core';
|
|
2
2
|
import type { User } from '../../models/interfaces';
|
|
3
3
|
import { useAccountStore } from '../stores/accountStore';
|
|
4
|
+
import { useAuthStore } from '../stores/authStore';
|
|
4
5
|
import { QueryClient } from '@tanstack/react-query';
|
|
5
6
|
import { queryKeys, invalidateUserQueries, invalidateAccountQueries } from '../hooks/queries/queryKeys';
|
|
6
7
|
|
|
@@ -25,12 +26,11 @@ export async function updateAvatarVisibility(
|
|
|
25
26
|
|
|
26
27
|
try {
|
|
27
28
|
await oxyServices.assetUpdateVisibility(fileId, 'public');
|
|
28
|
-
|
|
29
|
+
// Visibility update is logged by the API
|
|
29
30
|
} catch (visError: any) {
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
31
|
+
// Silently handle errors - 404 means asset doesn't exist yet (which is OK)
|
|
32
|
+
// Other errors are logged by the API, so no need to log here
|
|
33
|
+
// Function continues gracefully regardless of visibility update success
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -104,6 +104,9 @@ export async function updateProfileWithAvatar(
|
|
|
104
104
|
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Update authStore so frontend components see the changes immediately
|
|
108
|
+
useAuthStore.getState().setUser(data);
|
|
109
|
+
|
|
107
110
|
// If avatar was updated, refresh accountStore with cache-busted URL
|
|
108
111
|
if (updates.avatar && activeSessionId) {
|
|
109
112
|
refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
|