@oxyhq/services 5.16.29 → 5.16.31
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/services/SessionService.js +2 -1
- package/lib/commonjs/core/services/SessionService.js.map +1 -1
- package/lib/commonjs/core/services/TokenService.js +17 -9
- package/lib/commonjs/core/services/TokenService.js.map +1 -1
- package/lib/commonjs/index.js +64 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/models/interfaces.js +9 -8
- package/lib/commonjs/models/interfaces.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +40 -4
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +25 -14
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/auth/index.js +37 -0
- package/lib/commonjs/ui/hooks/auth/index.js.map +1 -0
- package/lib/commonjs/ui/hooks/auth/useUsernameValidation.js +171 -0
- package/lib/commonjs/ui/hooks/auth/useUsernameValidation.js.map +1 -0
- package/lib/commonjs/ui/hooks/index.js +20 -0
- package/lib/commonjs/ui/hooks/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/index.js +12 -0
- package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +45 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/index.js +12 -0
- package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +3 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +43 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +12 -4
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +8 -0
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +76 -97
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/sessionHelpers.js +26 -11
- package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/commonjs/utils/sessionUtils.js +8 -1
- package/lib/commonjs/utils/sessionUtils.js.map +1 -1
- package/lib/module/core/services/SessionService.js +2 -1
- package/lib/module/core/services/SessionService.js.map +1 -1
- package/lib/module/core/services/TokenService.js +17 -9
- package/lib/module/core/services/TokenService.js.map +1 -1
- package/lib/module/index.js +3 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/models/interfaces.js +9 -8
- package/lib/module/models/interfaces.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +40 -4
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +25 -14
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/auth/index.js +7 -0
- package/lib/module/ui/hooks/auth/index.js.map +1 -0
- package/lib/module/ui/hooks/auth/useUsernameValidation.js +167 -0
- package/lib/module/ui/hooks/auth/useUsernameValidation.js.map +1 -0
- package/lib/module/ui/hooks/index.js +1 -0
- package/lib/module/ui/hooks/index.js.map +1 -1
- package/lib/module/ui/hooks/mutations/index.js +1 -1
- package/lib/module/ui/hooks/mutations/index.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +42 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/index.js +1 -1
- package/lib/module/ui/hooks/queries/index.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +3 -1
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +40 -0
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/queries/useServicesQueries.js +13 -5
- package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +8 -0
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/screens/PrivacySettingsScreen.js +77 -98
- package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/module/ui/utils/sessionHelpers.js +26 -11
- package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/module/utils/sessionUtils.js +8 -1
- package/lib/module/utils/sessionUtils.js.map +1 -1
- package/lib/typescript/core/services/SessionService.d.ts +4 -2
- package/lib/typescript/core/services/SessionService.d.ts.map +1 -1
- package/lib/typescript/core/services/TokenService.d.ts +8 -3
- package/lib/typescript/core/services/TokenService.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +4 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +9 -8
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/models/session.d.ts +4 -2
- package/lib/typescript/models/session.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/auth/index.d.ts +6 -0
- package/lib/typescript/ui/hooks/auth/index.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/auth/useUsernameValidation.d.ts +32 -0
- package/lib/typescript/ui/hooks/auth/useUsernameValidation.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/index.d.ts +1 -0
- package/lib/typescript/ui/hooks/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/index.d.ts +1 -1
- package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +12 -0
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/index.d.ts +1 -1
- package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +2 -0
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +12 -0
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/utils/sessionHelpers.d.ts +6 -2
- package/lib/typescript/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/utils/sessionUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/services/SessionService.ts +4 -2
- package/src/core/services/TokenService.ts +18 -10
- package/src/index.ts +6 -0
- package/src/models/interfaces.ts +11 -10
- package/src/models/session.ts +5 -3
- package/src/ui/context/OxyContext.tsx +56 -20
- package/src/ui/context/hooks/useAuthOperations.ts +23 -15
- package/src/ui/hooks/auth/index.ts +7 -0
- package/src/ui/hooks/auth/useUsernameValidation.ts +177 -0
- package/src/ui/hooks/index.ts +2 -1
- package/src/ui/hooks/mutations/index.ts +2 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +36 -0
- package/src/ui/hooks/queries/index.ts +2 -0
- package/src/ui/hooks/queries/queryKeys.ts +2 -0
- package/src/ui/hooks/queries/useAccountQueries.ts +34 -0
- package/src/ui/hooks/queries/useServicesQueries.ts +8 -3
- package/src/ui/hooks/useSessionManagement.ts +8 -1
- package/src/ui/screens/PrivacySettingsScreen.tsx +67 -101
- package/src/ui/utils/sessionHelpers.ts +32 -15
- package/src/utils/sessionUtils.ts +8 -1
- package/lib/commonjs/ui/context/hooks/useSessionManagement.js +0 -281
- package/lib/commonjs/ui/context/hooks/useSessionManagement.js.map +0 -1
- package/lib/commonjs/ui/context/hooks/useStorage.js +0 -79
- package/lib/commonjs/ui/context/hooks/useStorage.js.map +0 -1
- package/lib/module/ui/context/hooks/useSessionManagement.js +0 -276
- package/lib/module/ui/context/hooks/useSessionManagement.js.map +0 -1
- package/lib/module/ui/context/hooks/useStorage.js +0 -74
- package/lib/module/ui/context/hooks/useStorage.js.map +0 -1
- package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts +0 -41
- package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts.map +0 -1
- package/lib/typescript/ui/context/hooks/useStorage.d.ts +0 -22
- package/lib/typescript/ui/context/hooks/useStorage.d.ts.map +0 -1
- package/src/ui/context/hooks/useSessionManagement.ts +0 -401
- package/src/ui/context/hooks/useStorage.ts +0 -104
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
* - Single storage location (no duplication)
|
|
9
9
|
* - Automatic token refresh when expiring soon
|
|
10
10
|
* - Type-safe token payload handling
|
|
11
|
-
* -
|
|
11
|
+
* - publicKey is the canonical user identity
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { jwtDecode } from 'jwt-decode';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* AccessTokenPayload - Matches the token payload structure from API
|
|
18
|
-
*
|
|
18
|
+
* publicKey is the canonical user identity (ECDSA secp256k1 public key)
|
|
19
19
|
*/
|
|
20
20
|
interface AccessTokenPayload {
|
|
21
|
-
|
|
21
|
+
publicKey: string; // User's public key - CANONICAL USER IDENTITY
|
|
22
22
|
sessionId: string; // Session UUID
|
|
23
23
|
deviceId: string; // Device identifier
|
|
24
24
|
type: 'access';
|
|
@@ -116,21 +116,29 @@ class TokenService {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Get
|
|
120
|
-
* Returns
|
|
119
|
+
* Get publicKey from current access token
|
|
120
|
+
* Returns the user's public key (canonical user identity)
|
|
121
121
|
*/
|
|
122
|
-
|
|
122
|
+
getPublicKeyFromToken(): string | null {
|
|
123
123
|
const token = this.tokenStore.accessToken;
|
|
124
124
|
if (!token) return null;
|
|
125
125
|
|
|
126
126
|
try {
|
|
127
127
|
const decoded = jwtDecode<AccessTokenPayload>(token);
|
|
128
|
-
return decoded.
|
|
128
|
+
return decoded.publicKey || null;
|
|
129
129
|
} catch {
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* @deprecated Use getPublicKeyFromToken() instead. This method is kept for backward compatibility.
|
|
136
|
+
* Get userId from current access token (returns publicKey, not MongoDB ObjectId)
|
|
137
|
+
*/
|
|
138
|
+
getUserIdFromToken(): string | null {
|
|
139
|
+
return this.getPublicKeyFromToken();
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
/**
|
|
135
143
|
* Refresh access token if expiring soon
|
|
136
144
|
* Returns promise that resolves when token is refreshed (or already valid)
|
|
@@ -193,10 +201,10 @@ class TokenService {
|
|
|
193
201
|
throw new Error('No access token in refresh response');
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
// Validate new token has
|
|
204
|
+
// Validate new token has publicKey (canonical user identity)
|
|
197
205
|
const newDecoded = jwtDecode<AccessTokenPayload>(newToken);
|
|
198
|
-
if (newDecoded.
|
|
199
|
-
throw new Error(
|
|
206
|
+
if (!newDecoded.publicKey) {
|
|
207
|
+
throw new Error('Token missing publicKey after refresh');
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
this.setTokens(newToken);
|
package/src/index.ts
CHANGED
|
@@ -137,6 +137,8 @@ export {
|
|
|
137
137
|
export { useSessionSocket } from './ui/hooks/useSessionSocket';
|
|
138
138
|
export { useAssets, setOxyAssetInstance } from './ui/hooks/useAssets';
|
|
139
139
|
export { useFileDownloadUrl, setOxyFileUrlInstance } from './ui/hooks/useFileDownloadUrl';
|
|
140
|
+
export { useUsernameValidation, USERNAME_MIN_LENGTH, USERNAME_REGEX, USERNAME_FORMAT_ERROR, USERNAME_DEBOUNCE_MS } from './ui/hooks/auth';
|
|
141
|
+
export type { UsernameValidationResult } from './ui/hooks/auth';
|
|
140
142
|
|
|
141
143
|
// UI hooks - Query hooks (TanStack Query)
|
|
142
144
|
export {
|
|
@@ -148,6 +150,8 @@ export {
|
|
|
148
150
|
useUserByUsername,
|
|
149
151
|
useUsersBySessions,
|
|
150
152
|
usePrivacySettings,
|
|
153
|
+
useBlockedUsers,
|
|
154
|
+
useRestrictedUsers,
|
|
151
155
|
// Service queries
|
|
152
156
|
useSessions,
|
|
153
157
|
useSession,
|
|
@@ -167,6 +171,8 @@ export {
|
|
|
167
171
|
useUpdateAccountSettings,
|
|
168
172
|
useUpdatePrivacySettings,
|
|
169
173
|
useUploadFile,
|
|
174
|
+
useUnblockUser,
|
|
175
|
+
useUnrestrictUser,
|
|
170
176
|
// Service mutations
|
|
171
177
|
useSwitchSession,
|
|
172
178
|
useLogoutSession,
|
package/src/models/interfaces.ts
CHANGED
|
@@ -25,19 +25,20 @@ export interface OxyConfig {
|
|
|
25
25
|
* User Model
|
|
26
26
|
*
|
|
27
27
|
* IMPORTANT:
|
|
28
|
-
* - id:
|
|
29
|
-
* - publicKey:
|
|
28
|
+
* - id: Public key (ECDSA secp256k1 public key, 130 hex characters) - CANONICAL USER IDENTITY
|
|
29
|
+
* - publicKey: Same as id (kept for backward compatibility and explicit clarity)
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
* -
|
|
33
|
-
* -
|
|
34
|
-
* -
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* publicKey is the canonical user identity across the ecosystem because it's:
|
|
32
|
+
* - Globally unique
|
|
33
|
+
* - Local-first (stored in Accounts app)
|
|
34
|
+
* - Avoids device-bound artifacts
|
|
35
|
+
*
|
|
36
|
+
* MongoDB _id remains internal to backend only and is not exposed in the User interface.
|
|
37
|
+
* Backend may use MongoDB ObjectId internally for database operations, but client always uses publicKey.
|
|
37
38
|
*/
|
|
38
39
|
export interface User {
|
|
39
|
-
id: string; //
|
|
40
|
-
publicKey: string; //
|
|
40
|
+
id: string; // Public key - CANONICAL USER IDENTITY (130 hex chars for secp256k1)
|
|
41
|
+
publicKey: string; // Same as id (kept for backward compatibility)
|
|
41
42
|
username: string;
|
|
42
43
|
email?: string;
|
|
43
44
|
// Avatar file id (asset id)
|
package/src/models/session.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* Client Session Model
|
|
3
3
|
*
|
|
4
4
|
* IMPORTANT:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
5
|
+
* - publicKey: Canonical user identity (ECDSA secp256k1 public key) - PRIMARY IDENTIFIER
|
|
6
|
+
* - userId: MongoDB ObjectId (kept for backward compatibility, but publicKey is canonical)
|
|
7
|
+
* - Sessions are bound to the local identity (publicKey) stored in Accounts app
|
|
7
8
|
*/
|
|
8
9
|
export interface ClientSession {
|
|
9
10
|
sessionId: string;
|
|
10
11
|
deviceId: string;
|
|
11
12
|
expiresAt: string;
|
|
12
13
|
lastActive: string;
|
|
13
|
-
|
|
14
|
+
publicKey: string; // Canonical user identity - PRIMARY IDENTIFIER
|
|
15
|
+
userId?: string; // MongoDB ObjectId (internal backend reference, optional for compatibility)
|
|
14
16
|
isCurrent?: boolean;
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -222,7 +222,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
222
222
|
// CRITICAL: Invalidate cache on app startup to ensure fresh state check
|
|
223
223
|
// This prevents stale cache from previous session from showing incorrect state
|
|
224
224
|
KeyManager.invalidateCache();
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
// Check if identity exists and verify integrity
|
|
227
227
|
const hasIdentity = await KeyManager.hasIdentity();
|
|
228
228
|
if (hasIdentity) {
|
|
@@ -421,7 +421,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
421
421
|
if (pendingTransfers.length > 0) {
|
|
422
422
|
const activeTransferId = getActiveTransferId();
|
|
423
423
|
const hasActiveTransfer = activeTransferId && pendingTransfers.some((t: { transferId: string; data: any }) => t.transferId === activeTransferId);
|
|
424
|
-
|
|
424
|
+
|
|
425
425
|
if (hasActiveTransfer) {
|
|
426
426
|
throw new Error(
|
|
427
427
|
'Cannot delete identity: An active identity transfer is in progress. ' +
|
|
@@ -485,11 +485,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
485
485
|
if (wasOffline) {
|
|
486
486
|
const now = Date.now();
|
|
487
487
|
const timeSinceLastLog = now - lastReconnectionLog;
|
|
488
|
-
|
|
488
|
+
|
|
489
489
|
if (timeSinceLastLog >= RECONNECTION_LOG_DEBOUNCE_MS) {
|
|
490
490
|
logger('Network reconnected, checking identity sync...');
|
|
491
491
|
lastReconnectionLog = now;
|
|
492
|
-
|
|
492
|
+
|
|
493
493
|
// Sync identity first (if not synced)
|
|
494
494
|
try {
|
|
495
495
|
const hasIdentityValue = await hasIdentity();
|
|
@@ -520,7 +520,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
520
520
|
// This is handled by useCheckPendingTransfers hook which runs automatically
|
|
521
521
|
// when authenticated and online
|
|
522
522
|
}
|
|
523
|
-
|
|
523
|
+
|
|
524
524
|
// TanStack Query will automatically retry pending mutations
|
|
525
525
|
// Reset flag immediately after processing (whether logged or not)
|
|
526
526
|
wasOffline = false;
|
|
@@ -580,6 +580,22 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
580
580
|
setTokenReady(false);
|
|
581
581
|
|
|
582
582
|
try {
|
|
583
|
+
// CRITICAL: Get current local identity (publicKey) before restoring sessions
|
|
584
|
+
// Sessions must belong to the current local identity stored in Accounts app
|
|
585
|
+
let currentPublicKey: string | null = null;
|
|
586
|
+
try {
|
|
587
|
+
if (Platform.OS !== 'web') {
|
|
588
|
+
// Only check identity on native platforms (web doesn't have local identity)
|
|
589
|
+
// KeyManager is already imported at the top of the file
|
|
590
|
+
currentPublicKey = await KeyManager.getPublicKey();
|
|
591
|
+
}
|
|
592
|
+
} catch (identityError) {
|
|
593
|
+
if (__DEV__) {
|
|
594
|
+
logger('Failed to get current local identity during session restoration', identityError);
|
|
595
|
+
}
|
|
596
|
+
// Continue without identity check if it fails (e.g., on web platform)
|
|
597
|
+
}
|
|
598
|
+
|
|
583
599
|
const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
|
|
584
600
|
const storedSessionIds: string[] = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
|
|
585
601
|
const storedActiveSessionId = await storage.getItem(storageKeys.activeSessionId);
|
|
@@ -592,12 +608,25 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
592
608
|
const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
|
|
593
609
|
if (validation?.valid && validation.user) {
|
|
594
610
|
const now = new Date();
|
|
611
|
+
// user.id is now publicKey (canonical identity)
|
|
612
|
+
const sessionPublicKey = validation.user.publicKey || validation.user.id || '';
|
|
613
|
+
|
|
614
|
+
// IDENTITY BINDING: Only restore sessions that match current local identity
|
|
615
|
+
// If we have a current local identity, filter out sessions that don't match
|
|
616
|
+
if (currentPublicKey && sessionPublicKey !== currentPublicKey) {
|
|
617
|
+
if (__DEV__) {
|
|
618
|
+
logger(`Skipping session ${sessionId.substring(0, 8)}... - belongs to different identity (${sessionPublicKey.substring(0, 16)}... vs ${currentPublicKey.substring(0, 16)}...)`);
|
|
619
|
+
}
|
|
620
|
+
continue; // Skip this session - it belongs to a different identity
|
|
621
|
+
}
|
|
622
|
+
|
|
595
623
|
validSessions.push({
|
|
596
624
|
sessionId,
|
|
597
625
|
deviceId: '',
|
|
598
626
|
expiresAt: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
599
627
|
lastActive: now.toISOString(),
|
|
600
|
-
|
|
628
|
+
publicKey: sessionPublicKey, // Canonical user identity
|
|
629
|
+
userId: validation.user.id?.toString(), // Optional MongoDB ObjectId for backward compatibility
|
|
601
630
|
isCurrent: sessionId === storedActiveSessionId,
|
|
602
631
|
});
|
|
603
632
|
}
|
|
@@ -615,6 +644,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
615
644
|
|
|
616
645
|
if (validSessions.length > 0) {
|
|
617
646
|
updateSessions(validSessions, { merge: false });
|
|
647
|
+
} else if (currentPublicKey && storedSessionIds.length > 0) {
|
|
648
|
+
// All sessions were filtered out due to identity mismatch - clear stored sessions
|
|
649
|
+
if (__DEV__) {
|
|
650
|
+
logger('All stored sessions belong to different identity - clearing session storage');
|
|
651
|
+
}
|
|
652
|
+
await storage.removeItem(storageKeys.sessionIds);
|
|
653
|
+
await storage.removeItem(storageKeys.activeSessionId);
|
|
618
654
|
}
|
|
619
655
|
}
|
|
620
656
|
|
|
@@ -726,9 +762,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
726
762
|
return isAuthenticatedFromStore || isAuthenticatedFromSessions;
|
|
727
763
|
}, [isAuthenticatedFromStore, isAuthenticatedFromSessions]);
|
|
728
764
|
|
|
729
|
-
// Get userId from JWT token
|
|
730
|
-
//
|
|
731
|
-
//
|
|
765
|
+
// Get userId from JWT token for socket room matching
|
|
766
|
+
// Note: JWT token may contain MongoDB ObjectId internally, but user.id is publicKey (canonical identity)
|
|
767
|
+
// Socket rooms may need internal mapping from publicKey to MongoDB ObjectId if backend requires it
|
|
732
768
|
const userId = oxyServices.getCurrentUserId() || user?.id;
|
|
733
769
|
|
|
734
770
|
// Use Zustand store for transfer state management
|
|
@@ -748,16 +784,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
748
784
|
// Load transfer codes
|
|
749
785
|
const storedCodes = await storage.getItem(TRANSFER_CODES_STORAGE_KEY);
|
|
750
786
|
const storedActiveTransferId = await storage.getItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
751
|
-
|
|
787
|
+
|
|
752
788
|
const parsedCodes = storedCodes ? JSON.parse(storedCodes) : {};
|
|
753
789
|
const activeTransferId = storedActiveTransferId || null;
|
|
754
|
-
|
|
790
|
+
|
|
755
791
|
// Restore to Zustand store (store handles validation and expiration)
|
|
756
792
|
restoreFromStorage(parsedCodes, activeTransferId);
|
|
757
793
|
markRestored();
|
|
758
|
-
|
|
794
|
+
|
|
759
795
|
if (__DEV__ && Object.keys(parsedCodes).length > 0) {
|
|
760
|
-
logger('Restored transfer codes from storage', {
|
|
796
|
+
logger('Restored transfer codes from storage', {
|
|
761
797
|
count: Object.keys(parsedCodes).length,
|
|
762
798
|
hasActiveTransfer: !!activeTransferId,
|
|
763
799
|
});
|
|
@@ -782,7 +818,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
782
818
|
const persistTransferCodes = async () => {
|
|
783
819
|
try {
|
|
784
820
|
await storage.setItem(TRANSFER_CODES_STORAGE_KEY, JSON.stringify(transferCodes));
|
|
785
|
-
|
|
821
|
+
|
|
786
822
|
if (activeTransferId) {
|
|
787
823
|
await storage.setItem(ACTIVE_TRANSFER_STORAGE_KEY, activeTransferId);
|
|
788
824
|
} else {
|
|
@@ -810,7 +846,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
810
846
|
// Transfer code management functions using Zustand store
|
|
811
847
|
const storeTransferCode = useCallback(async (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
812
848
|
storeTransferCodeStore(transferId, code, sourceDeviceId, publicKey);
|
|
813
|
-
|
|
849
|
+
|
|
814
850
|
if (__DEV__) {
|
|
815
851
|
logger('Stored transfer code', { transferId, sourceDeviceId, publicKey: publicKey.substring(0, 16) + '...' });
|
|
816
852
|
}
|
|
@@ -822,7 +858,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
822
858
|
|
|
823
859
|
const updateTransferState = useCallback(async (transferId: string, state: 'pending' | 'completed' | 'failed') => {
|
|
824
860
|
updateTransferStateStore(transferId, state);
|
|
825
|
-
|
|
861
|
+
|
|
826
862
|
if (__DEV__) {
|
|
827
863
|
logger('Updated transfer state', { transferId, state });
|
|
828
864
|
}
|
|
@@ -830,7 +866,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
830
866
|
|
|
831
867
|
const clearTransferCode = useCallback(async (transferId: string) => {
|
|
832
868
|
clearTransferCodeStore(transferId);
|
|
833
|
-
|
|
869
|
+
|
|
834
870
|
if (__DEV__) {
|
|
835
871
|
logger('Cleared transfer code', { transferId });
|
|
836
872
|
}
|
|
@@ -872,7 +908,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
872
908
|
const storedTransfer = getTransferCode(data.transferId);
|
|
873
909
|
|
|
874
910
|
if (!storedTransfer) {
|
|
875
|
-
logger('Transfer code not found for transferId', {
|
|
911
|
+
logger('Transfer code not found for transferId', {
|
|
876
912
|
transferId: data.transferId,
|
|
877
913
|
});
|
|
878
914
|
toast.error('Transfer verification failed: Code not found. Identity will not be deleted.');
|
|
@@ -894,7 +930,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
894
930
|
// Verify deviceId matches - very lenient since publicKey is the critical check
|
|
895
931
|
// If publicKey matches, we allow deletion even if deviceId doesn't match exactly
|
|
896
932
|
// This handles cases where deviceId might not be available or slightly different
|
|
897
|
-
const deviceIdMatches =
|
|
933
|
+
const deviceIdMatches =
|
|
898
934
|
// Exact match
|
|
899
935
|
(data.sourceDeviceId && data.sourceDeviceId === currentDeviceId) ||
|
|
900
936
|
// Stored sourceDeviceId matches current deviceId
|
|
@@ -974,7 +1010,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
974
1010
|
}
|
|
975
1011
|
|
|
976
1012
|
await deleteIdentityAndClearAccount(false, false, true);
|
|
977
|
-
|
|
1013
|
+
|
|
978
1014
|
// Verify identity was actually deleted
|
|
979
1015
|
const identityDeleted = !(await KeyManager.hasIdentity());
|
|
980
1016
|
if (!identityDeleted) {
|
|
@@ -146,9 +146,9 @@ export const useAuthOperations = ({
|
|
|
146
146
|
const localDeviceId = `device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
147
147
|
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days
|
|
148
148
|
|
|
149
|
-
// Create minimal user object with publicKey as id
|
|
149
|
+
// Create minimal user object with publicKey as id (canonical identity)
|
|
150
150
|
fullUser = {
|
|
151
|
-
id: publicKey, //
|
|
151
|
+
id: publicKey, // publicKey is the canonical user identity
|
|
152
152
|
publicKey,
|
|
153
153
|
username: '',
|
|
154
154
|
privacySettings: {},
|
|
@@ -170,7 +170,7 @@ export const useAuthOperations = ({
|
|
|
170
170
|
deviceId: localDeviceId,
|
|
171
171
|
expiresAt,
|
|
172
172
|
lastActive: new Date().toISOString(),
|
|
173
|
-
|
|
173
|
+
publicKey, // Canonical user identity
|
|
174
174
|
isCurrent: true,
|
|
175
175
|
};
|
|
176
176
|
|
|
@@ -204,17 +204,10 @@ export const useAuthOperations = ({
|
|
|
204
204
|
// Get full user data
|
|
205
205
|
fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
206
206
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (fullUser.id && !/^[0-9a-fA-F]{24}$/.test(fullUser.id)) {
|
|
212
|
-
console.warn('[useAuthOperations] User.id is not MongoDB ObjectId format:', {
|
|
213
|
-
id: fullUser.id.substring(0, 20),
|
|
214
|
-
publicKey: fullUser.publicKey.substring(0, 20),
|
|
215
|
-
message: 'API should return MongoDB ObjectId as user.id, not publicKey'
|
|
216
|
-
});
|
|
217
|
-
// Don't override - let the API fix this issue
|
|
207
|
+
// user.id is now publicKey (canonical identity) - this is correct
|
|
208
|
+
// Ensure publicKey field is set for consistency
|
|
209
|
+
if (!fullUser.publicKey && fullUser.id) {
|
|
210
|
+
fullUser.publicKey = fullUser.id;
|
|
218
211
|
}
|
|
219
212
|
|
|
220
213
|
// Fetch device sessions
|
|
@@ -222,7 +215,8 @@ export const useAuthOperations = ({
|
|
|
222
215
|
try {
|
|
223
216
|
allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
|
|
224
217
|
fallbackDeviceId: sessionResponse.deviceId,
|
|
225
|
-
fallbackUserId: fullUser.id,
|
|
218
|
+
fallbackUserId: fullUser.id, // Still pass for backward compatibility
|
|
219
|
+
fallbackPublicKey: fullUser.publicKey || fullUser.id, // publicKey is canonical identity
|
|
226
220
|
logger,
|
|
227
221
|
});
|
|
228
222
|
} catch (error) {
|
|
@@ -292,6 +286,13 @@ export const useAuthOperations = ({
|
|
|
292
286
|
setAuthState({ isLoading: true, error: null });
|
|
293
287
|
|
|
294
288
|
try {
|
|
289
|
+
// SESSION CLEANUP: Clear all sessions before creating new identity
|
|
290
|
+
// New identity means old sessions are no longer valid
|
|
291
|
+
await clearSessionState();
|
|
292
|
+
if (__DEV__ && logger) {
|
|
293
|
+
logger('Cleared all sessions before creating new identity');
|
|
294
|
+
}
|
|
295
|
+
|
|
295
296
|
// Generate new key pair directly (works offline)
|
|
296
297
|
const { publicKey, privateKey } = await KeyManager.generateKeyPair();
|
|
297
298
|
await KeyManager.importKeyPair(privateKey);
|
|
@@ -463,6 +464,13 @@ export const useAuthOperations = ({
|
|
|
463
464
|
setAuthState({ isLoading: true, error: null });
|
|
464
465
|
|
|
465
466
|
try {
|
|
467
|
+
// SESSION CLEANUP: Clear all sessions before importing new identity
|
|
468
|
+
// Importing a different identity means old sessions are no longer valid
|
|
469
|
+
await clearSessionState();
|
|
470
|
+
if (__DEV__ && logger) {
|
|
471
|
+
logger('Cleared all sessions before importing identity');
|
|
472
|
+
}
|
|
473
|
+
|
|
466
474
|
// Decrypt private key from backup data
|
|
467
475
|
const Crypto = await import('expo-crypto');
|
|
468
476
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import type { OxyServices } from '../../../core';
|
|
4
|
+
import { handleHttpError, ErrorCodes } from '../../../utils/errorUtils';
|
|
5
|
+
import { useDebounce } from '../../../utils/hookUtils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Username validation constants
|
|
9
|
+
*/
|
|
10
|
+
export const USERNAME_MIN_LENGTH = 4;
|
|
11
|
+
export const USERNAME_REGEX = /^[a-z0-9]+$/i;
|
|
12
|
+
export const USERNAME_FORMAT_ERROR = 'You can use a-z, 0-9. Minimum length is 4 characters.';
|
|
13
|
+
export const USERNAME_DEBOUNCE_MS = 500;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Username validation result interface
|
|
17
|
+
*/
|
|
18
|
+
export interface UsernameValidationResult {
|
|
19
|
+
isValid: boolean;
|
|
20
|
+
isAvailable: boolean | null; // null = not checked yet
|
|
21
|
+
error: string | null;
|
|
22
|
+
isChecking: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate username format using services validation utilities
|
|
27
|
+
*/
|
|
28
|
+
function validateUsernameFormat(username: string): boolean {
|
|
29
|
+
// Use stricter validation: lowercase alphanumeric only, min 4 chars
|
|
30
|
+
return username.length >= USERNAME_MIN_LENGTH && USERNAME_REGEX.test(username);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if an error is a network or timeout error
|
|
35
|
+
*/
|
|
36
|
+
function isNetworkOrTimeoutError(error: unknown): boolean {
|
|
37
|
+
const apiError = handleHttpError(error);
|
|
38
|
+
return (
|
|
39
|
+
apiError.code === ErrorCodes.NETWORK_ERROR ||
|
|
40
|
+
apiError.code === ErrorCodes.TIMEOUT ||
|
|
41
|
+
apiError.code === ErrorCodes.CONNECTION_FAILED
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract error message from an unknown error shape
|
|
47
|
+
*/
|
|
48
|
+
function extractAuthErrorMessage(error: unknown, fallbackMessage = 'An error occurred'): string {
|
|
49
|
+
const apiError = handleHttpError(error);
|
|
50
|
+
return apiError.message || fallbackMessage;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hook for username validation with debouncing and availability checking
|
|
55
|
+
*
|
|
56
|
+
* Uses TanStack Query for efficient API calls with:
|
|
57
|
+
* - Automatic request cancellation when username changes
|
|
58
|
+
* - Built-in caching (same username checked multiple times = cached result)
|
|
59
|
+
* - Request deduplication (multiple components checking same username = single request)
|
|
60
|
+
* - Proper error handling
|
|
61
|
+
*
|
|
62
|
+
* @param username - The username to validate
|
|
63
|
+
* @param oxyServices - OxyServices instance for API calls
|
|
64
|
+
* @returns Username validation state and result
|
|
65
|
+
*/
|
|
66
|
+
export function useUsernameValidation(
|
|
67
|
+
username: string,
|
|
68
|
+
oxyServices: OxyServices | null
|
|
69
|
+
): UsernameValidationResult {
|
|
70
|
+
// Debounce the username input to avoid excessive API calls
|
|
71
|
+
const debouncedUsername = useDebounce(username.trim().toLowerCase(), USERNAME_DEBOUNCE_MS);
|
|
72
|
+
|
|
73
|
+
// Validate format synchronously (no API call needed)
|
|
74
|
+
const isValid = useMemo(() => validateUsernameFormat(username), [username]);
|
|
75
|
+
|
|
76
|
+
// Determine if we should check availability
|
|
77
|
+
const shouldCheckAvailability = useMemo(() => {
|
|
78
|
+
if (!debouncedUsername || debouncedUsername.length < USERNAME_MIN_LENGTH) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return validateUsernameFormat(debouncedUsername);
|
|
82
|
+
}, [debouncedUsername]);
|
|
83
|
+
|
|
84
|
+
// Use TanStack Query for the API call
|
|
85
|
+
// This provides automatic caching, request cancellation, and deduplication
|
|
86
|
+
const {
|
|
87
|
+
data: availabilityResult,
|
|
88
|
+
isLoading: isChecking,
|
|
89
|
+
error: queryError,
|
|
90
|
+
isFetching,
|
|
91
|
+
} = useQuery({
|
|
92
|
+
queryKey: ['username', 'availability', debouncedUsername],
|
|
93
|
+
queryFn: async () => {
|
|
94
|
+
if (!oxyServices) {
|
|
95
|
+
throw new Error('OxyServices not available');
|
|
96
|
+
}
|
|
97
|
+
return await oxyServices.checkUsernameAvailability(debouncedUsername);
|
|
98
|
+
},
|
|
99
|
+
enabled: shouldCheckAvailability && !!oxyServices,
|
|
100
|
+
staleTime: 5 * 60 * 1000, // Cache for 5 minutes (usernames don't change often)
|
|
101
|
+
gcTime: 30 * 60 * 1000, // Keep in cache for 30 minutes
|
|
102
|
+
retry: (failureCount, error) => {
|
|
103
|
+
// Don't retry on network/timeout errors (user might be offline)
|
|
104
|
+
if (isNetworkOrTimeoutError(error)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
// Retry up to 2 times for other errors
|
|
108
|
+
return failureCount < 2;
|
|
109
|
+
},
|
|
110
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Compute the result based on validation and query state
|
|
114
|
+
return useMemo(() => {
|
|
115
|
+
// If username is too short or invalid format, return early validation
|
|
116
|
+
if (!username || username.length < USERNAME_MIN_LENGTH) {
|
|
117
|
+
return {
|
|
118
|
+
isValid: false,
|
|
119
|
+
isAvailable: null,
|
|
120
|
+
error: null,
|
|
121
|
+
isChecking: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!isValid) {
|
|
126
|
+
return {
|
|
127
|
+
isValid: false,
|
|
128
|
+
isAvailable: false,
|
|
129
|
+
error: USERNAME_FORMAT_ERROR,
|
|
130
|
+
isChecking: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If we're not checking yet (debounce period), show checking state only if user is typing
|
|
135
|
+
const isCurrentlyChecking = isChecking || isFetching;
|
|
136
|
+
|
|
137
|
+
// Handle network/timeout errors gracefully
|
|
138
|
+
if (queryError && isNetworkOrTimeoutError(queryError)) {
|
|
139
|
+
// Allow proceeding if offline/network issue (optimistic)
|
|
140
|
+
return {
|
|
141
|
+
isValid: true,
|
|
142
|
+
isAvailable: true, // Optimistic: allow proceeding
|
|
143
|
+
error: null,
|
|
144
|
+
isChecking: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle other errors
|
|
149
|
+
if (queryError) {
|
|
150
|
+
return {
|
|
151
|
+
isValid: true,
|
|
152
|
+
isAvailable: false,
|
|
153
|
+
error: extractAuthErrorMessage(queryError, 'Failed to check username availability'),
|
|
154
|
+
isChecking: false,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If we have a result, use it
|
|
159
|
+
if (availabilityResult) {
|
|
160
|
+
return {
|
|
161
|
+
isValid: true,
|
|
162
|
+
isAvailable: availabilityResult.available,
|
|
163
|
+
error: availabilityResult.available ? null : (availabilityResult.message || 'Username is already taken'),
|
|
164
|
+
isChecking: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Still checking (or waiting for debounce)
|
|
169
|
+
return {
|
|
170
|
+
isValid: true,
|
|
171
|
+
isAvailable: null,
|
|
172
|
+
error: null,
|
|
173
|
+
isChecking: isCurrentlyChecking,
|
|
174
|
+
};
|
|
175
|
+
}, [username, isValid, availabilityResult, isChecking, isFetching, queryError]);
|
|
176
|
+
}
|
|
177
|
+
|
package/src/ui/hooks/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useFollow, useFollowerCounts } from './useFollow';
|
|
2
2
|
export { useFileDownloadUrl, setOxyFileUrlInstance } from './useFileDownloadUrl';
|
|
3
3
|
export { useThemeStyles } from './useThemeStyles';
|
|
4
|
-
export { useThemeColors } from './useThemeColors';
|
|
4
|
+
export { useThemeColors } from './useThemeColors';
|
|
5
|
+
export * from './auth';
|