mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
fix: improve authentication state persistence with Zustand
- Standardize secure storage keys across auth systems - Fix inconsistent key naming in NDK store and auth providers - Implement proper credential migration between storage systems - Enhance error handling during credential restoration - Fix private key authentication not persisting across app restarts - Add detailed logging for auth initialization sequence - Improve overall authentication stability with better state management
This commit is contained in:
parent
c441c5afa5
commit
9ad50956f8
85
CHANGELOG.md
85
CHANGELOG.md
@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Authentication persistence debugging tools
|
||||
- Created dedicated AuthPersistenceTest screen for diagnosing credential issues
|
||||
- Added comprehensive SecureStore key visualization
|
||||
- Implemented manual key migration triggering
|
||||
- Added test key creation functionality for simulating scenarios
|
||||
- Built key clearing utilities for testing from scratch
|
||||
- Added interactive testing workflow with detailed instructions
|
||||
- Enhanced error handling with better messaging
|
||||
|
||||
- React Query Android Profile Optimization System
|
||||
- Added platform-specific timeouts for network operations
|
||||
- Created fallback UI system for handling network delays
|
||||
@ -17,6 +26,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Improved user experience during temporary API failures
|
||||
|
||||
### Improved
|
||||
- Authentication initialization sequence
|
||||
- Added proper awaiting of NDK relay connections
|
||||
- Implemented credential migration before authentication starts
|
||||
- Enhanced AuthProvider with improved initialization flow
|
||||
- Added robustness against race conditions during startup
|
||||
- Implemented proper detection of stored credentials
|
||||
- Created key migration system between storage locations
|
||||
- Enhanced app_layout.tsx to ensure proper initialization order
|
||||
- Added detailed technical documentation for the auth system
|
||||
|
||||
- Profile loading performance dramatically enhanced
|
||||
- Added ultra-early content display after just 500ms
|
||||
- Implemented progressive content loading with three-tier system
|
||||
@ -48,6 +67,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Created comprehensive logging documentation
|
||||
|
||||
### Fixed
|
||||
- Authentication storage key inconsistencies
|
||||
- Fixed inconsistent key naming between different auth systems
|
||||
- Implemented consistent SECURE_STORE_KEYS constants
|
||||
- Created migration utility in secureStorage.ts
|
||||
- Added key migration from legacy to standardized locations
|
||||
- Fixed AuthProvider and AuthStateManager to use the same keys
|
||||
- Enhanced NDK store to use standardized key constants
|
||||
- Added migration status tracking to prevent duplicate migrations
|
||||
- Created diagnostic tool for checking credential storage
|
||||
- Fixed ReactQueryAuthProvider to use the same key constants
|
||||
- Added detailed documentation in authentication_persistence_debug_guide.md
|
||||
|
||||
- Private key authentication persistence
|
||||
- Fixed inconsistent storage key naming between legacy and React Query auth systems
|
||||
- Standardized on 'nostr_privkey' for all private key storage
|
||||
@ -82,6 +113,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added comprehensive logging for better debugging
|
||||
- Fixed race conditions in authentication state transitions
|
||||
- Implemented initialization tracking to prevent duplicate auth operations
|
||||
- Added waiting for NDK pool initialization before auth operations
|
||||
- Created one-time migration system for legacy credentials
|
||||
- Fixed delayed authentication restoration with improved sequence checks
|
||||
- Enhanced credential consistency verification at startup
|
||||
- Added test tools for diagnosing and fixing authentication issues
|
||||
|
||||
- Android profile screen hanging issues
|
||||
- Fixed infinite loading state on profile screen with proper timeouts
|
||||
@ -698,51 +734,4 @@ g
|
||||
- Added type safety for complex operations
|
||||
- Improved error handling throughout relay management
|
||||
|
||||
# Changelog - March 8, 2025
|
||||
|
||||
## Added
|
||||
- Database schema upgrade to version 5
|
||||
- Added workouts, workout_exercises, and workout_sets tables
|
||||
- Added templates and template_exercises tables
|
||||
- Added publication_queue table for offline-first functionality
|
||||
- Added app_status table for connectivity tracking
|
||||
- New database services
|
||||
- WorkoutService for managing workout data persistence
|
||||
- Enhanced TemplateService for template management
|
||||
- NostrWorkoutService for Nostr event conversion
|
||||
- Updated PublicationQueueService for offline publishing
|
||||
- React hooks for database access
|
||||
- useWorkouts hook for workout operations
|
||||
- useTemplates hook for template operations
|
||||
- Improved workout completion flow
|
||||
- Three-tier storage approach (Local Only, Publish Complete, Publish Limited)
|
||||
- Template modification options (keep original, update, save as new)
|
||||
- Enhanced social sharing capabilities
|
||||
- Detailed workout summary with statistics
|
||||
- Enhanced database debugging tools
|
||||
- Added proper error handling and logging
|
||||
- Improved transaction management
|
||||
- Added connectivity status tracking
|
||||
|
||||
## Fixed
|
||||
- Missing workout and template table errors
|
||||
- Incomplete data storage issues
|
||||
- Template management synchronization
|
||||
- Nostr event conversion between app models and Nostr protocol
|
||||
- Workout persistence across app sessions
|
||||
- Database transaction handling in workout operations
|
||||
- Template reference handling in workout records
|
||||
|
||||
## Improved
|
||||
- Workout store persistence layer
|
||||
- Enhanced integration with database services
|
||||
- Better error handling for database operations
|
||||
- Improved Nostr connectivity detection
|
||||
- Template management workflow
|
||||
- Proper versioning and attribution
|
||||
- Enhanced modification tracking
|
||||
- Better user control over template sharing
|
||||
- Overall data persistence architecture
|
||||
- Consistent service-based approach
|
||||
- Improved type safety
|
||||
- Enhanced error
|
||||
# Changelog - March 8,
|
||||
|
@ -23,6 +23,8 @@ import { useWorkoutStore } from '@/stores/workoutStore';
|
||||
import { ConnectivityService } from '@/lib/db/services/ConnectivityService';
|
||||
import { AuthProvider } from '@/lib/auth/AuthProvider';
|
||||
import { ReactQueryAuthProvider } from '@/lib/auth/ReactQueryAuthProvider';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createQueryClient } from '@/lib/queryClient';
|
||||
|
||||
// Import splash screens with improved fallback mechanism
|
||||
let SplashComponent: React.ComponentType<{onFinish: () => void}>;
|
||||
@ -115,6 +117,8 @@ export default function RootLayout() {
|
||||
const { colorScheme, isDarkColorScheme } = useColorScheme();
|
||||
const { init } = useNDKStore();
|
||||
const initializationPromise = React.useRef<Promise<void> | null>(null);
|
||||
// Create a query client instance that can be used by both auth systems
|
||||
const queryClient = React.useMemo(() => createQueryClient(), []);
|
||||
|
||||
// Start app initialization immediately
|
||||
React.useEffect(() => {
|
||||
@ -135,12 +139,23 @@ export default function RootLayout() {
|
||||
// Start database initialization and NDK initialization in parallel
|
||||
const initPromises = [];
|
||||
|
||||
// Initialize NDK with timeout
|
||||
const ndkPromise = init().catch(error => {
|
||||
// Initialize NDK with credentials migration first
|
||||
const ndkPromise = (async () => {
|
||||
try {
|
||||
// Import and run key migration before NDK init
|
||||
const { migrateKeysIfNeeded } = await import('@/lib/auth/persistence/secureStorage');
|
||||
console.log('Running pre-NDK credential migration...');
|
||||
await migrateKeysIfNeeded();
|
||||
|
||||
// Now initialize NDK
|
||||
console.log('Starting NDK initialization...');
|
||||
return await init();
|
||||
} catch (error) {
|
||||
console.error('NDK initialization error:', error);
|
||||
// Continue even if NDK fails
|
||||
return { offlineMode: true };
|
||||
});
|
||||
}
|
||||
})();
|
||||
initPromises.push(ndkPromise);
|
||||
|
||||
// Load favorites from SQLite (local operation)
|
||||
@ -232,7 +247,7 @@ export default function RootLayout() {
|
||||
{/* Conditionally render authentication providers based on feature flag */}
|
||||
{FLAGS.useReactQueryAuth ? (
|
||||
/* Use React Query Auth system */
|
||||
<ReactQueryAuthProvider enableNDK={true}>
|
||||
<ReactQueryAuthProvider enableNDK={true} queryClient={queryClient}>
|
||||
{/* React Query specific components */}
|
||||
<RelayInitializer reactQueryMode={true} />
|
||||
<OfflineIndicator />
|
||||
@ -253,7 +268,9 @@ export default function RootLayout() {
|
||||
<PortalHost />
|
||||
</ReactQueryAuthProvider>
|
||||
) : (
|
||||
/* Use Legacy Auth system */
|
||||
/* Use Legacy Auth system but still provide QueryClientProvider and NDKContext for data fetching */
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NDKContext.Provider value={{ ndk: useNDKStore.getState().ndk, isInitialized: true }}>
|
||||
<AuthProvider ndk={useNDKStore.getState().ndk!}>
|
||||
<RelayInitializer reactQueryMode={false} />
|
||||
<OfflineIndicator />
|
||||
@ -273,6 +290,8 @@ export default function RootLayout() {
|
||||
|
||||
<PortalHost />
|
||||
</AuthProvider>
|
||||
</NDKContext.Provider>
|
||||
</QueryClientProvider>
|
||||
)}
|
||||
</SettingsDrawerProvider>
|
||||
</ThemeProvider>
|
||||
|
271
app/test/auth-persistence-test.tsx
Normal file
271
app/test/auth-persistence-test.tsx
Normal file
@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, Button, ScrollView, Platform, TouchableOpacity } from 'react-native';
|
||||
import { Stack, useRouter } from 'expo-router';
|
||||
import { X } from 'lucide-react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { SECURE_STORE_KEYS } from '@/lib/auth/constants';
|
||||
import { useAuthStore } from '@/lib/auth/AuthStateManager';
|
||||
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
||||
import { migrateKeysIfNeeded, resetMigration } from '@/lib/auth/persistence/secureStorage';
|
||||
|
||||
/**
|
||||
* Test screen for debugging authentication persistence issues
|
||||
* This component shows the current state of authentication and stored credentials
|
||||
*/
|
||||
export default function AuthPersistenceTest() {
|
||||
const [storageInfo, setStorageInfo] = useState({
|
||||
standardPrivateKey: 'Checking...',
|
||||
legacyPrivateKey: 'Checking...',
|
||||
ndkStoreKey: 'Checking...',
|
||||
pubkey: 'Checking...',
|
||||
externalSigner: 'Checking...',
|
||||
migrationStatus: 'Checking...'
|
||||
});
|
||||
|
||||
// Get auth states from both stores (legacy and React Query)
|
||||
const authState = useAuthStore((state) => state);
|
||||
const { isAuthenticated, currentUser } = useNDKCurrentUser();
|
||||
const router = useRouter();
|
||||
|
||||
const checkStorage = async () => {
|
||||
try {
|
||||
// Check all possible storage keys
|
||||
const standardPrivateKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
|
||||
const legacyPrivateKey = await SecureStore.getItemAsync('powr.private_key');
|
||||
const ndkStoreKey = await SecureStore.getItemAsync('nostr_privkey');
|
||||
const pubkey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PUBKEY);
|
||||
const externalSigner = await SecureStore.getItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
|
||||
const migrationStatus = await SecureStore.getItemAsync('auth_migration_v1_completed');
|
||||
|
||||
// Update state with results
|
||||
setStorageInfo({
|
||||
standardPrivateKey: standardPrivateKey
|
||||
? `Found (${standardPrivateKey.length} chars)`
|
||||
: 'Not found',
|
||||
legacyPrivateKey: legacyPrivateKey
|
||||
? `Found (${legacyPrivateKey.length} chars)`
|
||||
: 'Not found',
|
||||
ndkStoreKey: ndkStoreKey
|
||||
? `Found (${ndkStoreKey.length} chars)`
|
||||
: 'Not found',
|
||||
pubkey: pubkey
|
||||
? `Found (${pubkey.substring(0, 8)}...)`
|
||||
: 'Not found',
|
||||
externalSigner: externalSigner
|
||||
? 'Found'
|
||||
: 'Not found',
|
||||
migrationStatus: migrationStatus === 'true'
|
||||
? 'Completed'
|
||||
: 'Not run'
|
||||
});
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.message || 'Unknown error';
|
||||
console.error('Error checking storage:', errorMsg);
|
||||
setStorageInfo({
|
||||
standardPrivateKey: `Error: ${errorMsg}`,
|
||||
legacyPrivateKey: `Error: ${errorMsg}`,
|
||||
ndkStoreKey: `Error: ${errorMsg}`,
|
||||
pubkey: `Error: ${errorMsg}`,
|
||||
externalSigner: `Error: ${errorMsg}`,
|
||||
migrationStatus: `Error: ${errorMsg}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Check storage when component mounts
|
||||
useEffect(() => {
|
||||
checkStorage();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: 'Auth Persistence Test',
|
||||
headerLeft: () => Platform.OS === 'ios' ? (
|
||||
<TouchableOpacity onPress={() => router.back()} style={{ marginLeft: 15 }}>
|
||||
<X size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
) : undefined,
|
||||
headerRight: () => Platform.OS !== 'ios' ? (
|
||||
<TouchableOpacity onPress={() => router.back()} style={{ marginRight: 15 }}>
|
||||
<X size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
) : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollView style={styles.container}>
|
||||
<Text style={styles.title}>Auth Persistence Debugger</Text>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Zustand Auth State</Text>
|
||||
<Text>Status: {authState.status}</Text>
|
||||
{authState.status === 'authenticated' && (
|
||||
<>
|
||||
<Text>User: {authState.user?.pubkey?.substring(0, 8)}...</Text>
|
||||
<Text>Method: {authState.method || 'Unknown'}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>React Query Auth State</Text>
|
||||
<Text>Status: {isAuthenticated ? 'authenticated' : 'unauthenticated'}</Text>
|
||||
{isAuthenticated && currentUser?.pubkey && (
|
||||
<Text>User: {currentUser.pubkey.substring(0, 8)}...</Text>
|
||||
)}
|
||||
<Text>User Profile: {currentUser?.profile?.name || 'Not loaded'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Secure Storage Status</Text>
|
||||
<Text>Standard Key ({SECURE_STORE_KEYS.PRIVATE_KEY}):</Text>
|
||||
<Text style={styles.value}>{storageInfo.standardPrivateKey}</Text>
|
||||
|
||||
<Text>Legacy Key (powr.private_key):</Text>
|
||||
<Text style={styles.value}>{storageInfo.legacyPrivateKey}</Text>
|
||||
|
||||
<Text>NDK Store Key (nostr_privkey):</Text>
|
||||
<Text style={styles.value}>{storageInfo.ndkStoreKey}</Text>
|
||||
|
||||
<Text>Public Key:</Text>
|
||||
<Text style={styles.value}>{storageInfo.pubkey}</Text>
|
||||
|
||||
<Text>External Signer:</Text>
|
||||
<Text style={styles.value}>{storageInfo.externalSigner}</Text>
|
||||
|
||||
<Text>Migration Status:</Text>
|
||||
<Text style={styles.value}>{storageInfo.migrationStatus}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
title="Refresh Storage Info"
|
||||
onPress={checkStorage}
|
||||
/>
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Button
|
||||
title="Run Key Migration Manually"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await migrateKeysIfNeeded();
|
||||
console.log('Migration completed successfully');
|
||||
checkStorage();
|
||||
} catch (e) {
|
||||
console.error('Migration failed:', e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Button
|
||||
title="Reset Migration Status (DEV)"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await resetMigration();
|
||||
console.log('Migration status reset');
|
||||
checkStorage();
|
||||
} catch (e) {
|
||||
console.error('Reset failed:', e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Button
|
||||
title="Create Test Keys"
|
||||
color="#4CAF50"
|
||||
onPress={async () => {
|
||||
try {
|
||||
// Generate a simple test key
|
||||
const testKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
// Store in legacy location for testing migration
|
||||
await SecureStore.setItemAsync('powr.private_key', testKey);
|
||||
console.log('Test key created in legacy location');
|
||||
checkStorage();
|
||||
} catch (e) {
|
||||
console.error('Failed to create test key:', e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Button
|
||||
title="Clear All Auth Keys"
|
||||
color="#F44336"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PUBKEY);
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
|
||||
await SecureStore.deleteItemAsync('powr.private_key');
|
||||
await SecureStore.deleteItemAsync('nostr_privkey');
|
||||
await SecureStore.deleteItemAsync('auth_migration_v1_completed');
|
||||
console.log('All keys cleared');
|
||||
checkStorage();
|
||||
} catch (e) {
|
||||
console.error('Failed to clear keys:', e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.instructions}>
|
||||
<Text style={styles.sectionTitle}>Testing Instructions</Text>
|
||||
<Text>1. Click "Clear All Auth Keys" to start fresh</Text>
|
||||
<Text>2. Click "Create Test Keys" to add a key in the legacy location</Text>
|
||||
<Text>3. Force close and restart the app</Text>
|
||||
<Text>4. Return to this screen and check if auth persisted</Text>
|
||||
<Text>5. Check "Migration Status" to see if keys were migrated</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 20,
|
||||
},
|
||||
section: {
|
||||
backgroundColor: 'white',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
},
|
||||
value: {
|
||||
marginBottom: 8,
|
||||
paddingLeft: 10,
|
||||
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||||
},
|
||||
buttonContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
instructions: {
|
||||
backgroundColor: '#e8f5e9',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 30,
|
||||
}
|
||||
});
|
141
components/AuthDebugScreen.tsx
Normal file
141
components/AuthDebugScreen.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
// components/AuthDebugScreen.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, Button, StyleSheet } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { SECURE_STORE_KEYS } from '@/lib/auth/constants';
|
||||
import { useAuthStore } from '@/lib/auth/AuthStateManager';
|
||||
|
||||
export default function AuthDebugScreen() {
|
||||
const [storageInfo, setStorageInfo] = useState({
|
||||
privateKey: 'Checking...',
|
||||
pubkey: 'Checking...',
|
||||
externalSigner: 'Checking...'
|
||||
});
|
||||
const authState = useAuthStore((state) => state);
|
||||
|
||||
const checkStorage = async () => {
|
||||
try {
|
||||
// Check using the defined constants
|
||||
const privateKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
|
||||
const pubkey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PUBKEY);
|
||||
const externalSigner = await SecureStore.getItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
|
||||
|
||||
// Also check the legacy key
|
||||
const legacyKey = await SecureStore.getItemAsync('powr.private_key');
|
||||
|
||||
setStorageInfo({
|
||||
privateKey: privateKey
|
||||
? `Found (${privateKey.length} chars)${legacyKey ? ' [also in legacy]' : ''}`
|
||||
: 'Not found',
|
||||
pubkey: pubkey
|
||||
? `Found (${pubkey.substring(0, 8)}...)`
|
||||
: 'Not found',
|
||||
externalSigner: externalSigner
|
||||
? 'Found'
|
||||
: 'Not found'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error checking storage:', error);
|
||||
const errorMessage = error && error.message ? error.message : 'Unknown error';
|
||||
setStorageInfo({
|
||||
privateKey: `Error: ${errorMessage}`,
|
||||
pubkey: `Error: ${errorMessage}`,
|
||||
externalSigner: `Error: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkStorage();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Authentication Debug</Text>
|
||||
|
||||
<View style={styles.stateContainer}>
|
||||
<Text style={styles.sectionTitle}>Auth State</Text>
|
||||
<Text>Status: {authState.status}</Text>
|
||||
{authState.status === 'authenticated' && (
|
||||
<>
|
||||
<Text>User: {authState.user.pubkey.substring(0, 8)}...</Text>
|
||||
<Text>Method: {authState.method}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.storageContainer}>
|
||||
<Text style={styles.sectionTitle}>Secure Storage</Text>
|
||||
<Text>Private Key: {storageInfo.privateKey}</Text>
|
||||
<Text>Public Key: {storageInfo.pubkey}</Text>
|
||||
<Text>External Signer: {storageInfo.externalSigner}</Text>
|
||||
</View>
|
||||
|
||||
<Button title="Refresh Storage Info" onPress={checkStorage} />
|
||||
|
||||
<View style={styles.actionsContainer}>
|
||||
<Text style={styles.sectionTitle}>Debug Actions</Text>
|
||||
<Button
|
||||
title="Move Keys from Legacy to Current"
|
||||
onPress={async () => {
|
||||
const legacyKey = await SecureStore.getItemAsync('powr.private_key');
|
||||
if (legacyKey) {
|
||||
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY, legacyKey);
|
||||
console.log('Moved key from legacy to current');
|
||||
checkStorage();
|
||||
} else {
|
||||
console.log('No legacy key found');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Clear All Keys"
|
||||
onPress={async () => {
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.PUBKEY);
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
|
||||
await SecureStore.deleteItemAsync('powr.private_key');
|
||||
console.log('Cleared all keys');
|
||||
checkStorage();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
},
|
||||
stateContainer: {
|
||||
padding: 12,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
storageContainer: {
|
||||
padding: 12,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
actionsContainer: {
|
||||
padding: 12,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 8,
|
||||
marginTop: 16,
|
||||
gap: 8,
|
||||
}
|
||||
});
|
@ -209,6 +209,12 @@ export default function SettingsDrawer() {
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'developer-tools-header',
|
||||
icon: () => null,
|
||||
label: '',
|
||||
onPress: () => {},
|
||||
},
|
||||
{
|
||||
id: 'react-query-demo',
|
||||
icon: RefreshCw,
|
||||
@ -220,6 +226,17 @@ export default function SettingsDrawer() {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'auth-debug',
|
||||
icon: Database,
|
||||
label: 'Auth Debug Tools',
|
||||
onPress: () => {
|
||||
closeDrawer();
|
||||
router.push({
|
||||
pathname: "/test/auth-persistence-test" as any
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'relays',
|
||||
icon: Globe,
|
||||
|
@ -1,78 +1,98 @@
|
||||
# Authentication Persistence Fixes
|
||||
# Authentication System Fixes Summary
|
||||
|
||||
This document summarizes the improvements made to fix authentication persistence issues in the POWR app.
|
||||
## Authentication Persistence Fix
|
||||
|
||||
## Overview
|
||||
We have successfully implemented a solution for authentication persistence across app restarts. The key components of the fix:
|
||||
|
||||
The authentication system was enhanced to ensure reliable persistence of user credentials across app restarts. Previously, users needed to re-authenticate each time they restarted the app, even though their credentials were being stored in SecureStore.
|
||||
1. **Key Storage Standardization**
|
||||
- Implemented consistent use of key names through `SECURE_STORE_KEYS` constants
|
||||
- Set up migration utilities to handle legacy storage formats
|
||||
- Ensured consistent key storage across different parts of the app
|
||||
|
||||
## Key Improvements
|
||||
2. **Initialization Sequence Improvement**
|
||||
- Added proper pre-NDK credential migration in _layout.tsx
|
||||
- Ensured migration happens before NDK initialization
|
||||
- Fixed race conditions during startup
|
||||
|
||||
### 1. Enhanced AuthService
|
||||
3. **Diagnostic Tools**
|
||||
- Created AuthPersistenceTest component for real-time debugging
|
||||
- Added visualization of stored credentials
|
||||
- Implemented test utilities for manual testing
|
||||
|
||||
- **Improved initialization process**: Added promise caching for concurrent calls to prevent race conditions
|
||||
- **More robust credential restoration**: Better error handling when restoring stored private keys or external signers
|
||||
- **SecureStore key constants**: Added constants for all SecureStore keys to avoid inconsistencies
|
||||
- **Public key caching**: Added storage of public key alongside private key for faster access
|
||||
- **Clean credential handling**: Automatic cleanup of invalid credentials when restoration fails
|
||||
- **Enhanced logging**: Comprehensive logging throughout the authentication flow
|
||||
## React Query Integration Fix
|
||||
|
||||
### 2. Improved ReactQueryAuthProvider
|
||||
We also fixed an issue where the app was encountering errors when using the legacy Zustand-based authentication with React Query components. The solution:
|
||||
|
||||
- **Better NDK initialization**: Enhanced initialization with credential pre-checking
|
||||
- **Initialization tracking**: Added tracking of initialization attempts to prevent duplicates
|
||||
- **State management**: Improved state updates to ensure they only occur for the most recent initialization
|
||||
- **Auth state invalidation**: Force refresh of auth state after initialization
|
||||
- **Error resilience**: Better error handling throughout the provider
|
||||
1. **QueryClientProvider with Dual Auth Systems**
|
||||
- Added QueryClientProvider to the legacy auth path in _layout.tsx
|
||||
- Created a shared queryClient instance that works with both auth systems
|
||||
- Ensured NDKContext is properly provided in both paths
|
||||
|
||||
### 3. Fixed useAuthQuery Hook
|
||||
2. **Component-level Compatibility**
|
||||
- Components like UserAvatar that use React Query hooks now work in both auth modes
|
||||
- Data fetching via React Query continues to work even when using Zustand for auth state
|
||||
|
||||
- **Pre-initialization**: Added pre-initialization of auth service when the hook is first used
|
||||
- **Query configuration**: Adjusted query settings for better reliability (staleTime, refetchOnWindowFocus, etc.)
|
||||
- **Type safety**: Fixed TypeScript errors to ensure proper type checking
|
||||
- **Initialization reference**: Added reference tracking to prevent memory leaks
|
||||
- **Better error handling**: Enhanced error management throughout the hook
|
||||
## Code Changes
|
||||
|
||||
## Implementation Details
|
||||
### 1. Updated app/_layout.tsx to use QueryClientProvider in both auth paths
|
||||
|
||||
### AuthService Changes
|
||||
```tsx
|
||||
// Create a shared queryClient for both auth systems
|
||||
const queryClient = React.useMemo(() => createQueryClient(), []);
|
||||
|
||||
- Added promise caching with `initPromise` to handle concurrent initialization calls
|
||||
- Split initialization into public `initialize()` and private `_doInitialize()` methods
|
||||
- Improved error handling with try/catch blocks and proper cleanup
|
||||
- Added constants for secure storage keys (`POWR.PRIVATE_KEY`, etc.)
|
||||
- Enhanced login methods with optional `saveKey` parameter
|
||||
- Added public key storage for faster reference
|
||||
// In the render method:
|
||||
{FLAGS.useReactQueryAuth ? (
|
||||
// React Query Auth system (already had QueryClientProvider internally)
|
||||
<ReactQueryAuthProvider enableNDK={true} queryClient={queryClient}>
|
||||
{/* ... */}
|
||||
</ReactQueryAuthProvider>
|
||||
) : (
|
||||
// Legacy Auth system with added QueryClientProvider and NDKContext
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NDKContext.Provider value={{ ndk: useNDKStore.getState().ndk, isInitialized: true }}>
|
||||
<AuthProvider ndk={useNDKStore.getState().ndk!}>
|
||||
{/* ... */}
|
||||
</AuthProvider>
|
||||
</NDKContext.Provider>
|
||||
</QueryClientProvider>
|
||||
)}
|
||||
```
|
||||
|
||||
### ReactQueryAuthProvider Changes
|
||||
### 2. Updated ReactQueryAuthProvider to accept a queryClient prop
|
||||
|
||||
- Added initialization attempt tracking to prevent race conditions
|
||||
- Added pre-checking of credentials before NDK initialization
|
||||
- Enhanced state management to only update for the most recent initialization attempt
|
||||
- Improved error resilience to ensure app works even if initialization fails
|
||||
- Added forced query invalidation after successful initialization
|
||||
The ReactQueryAuthProvider component was enhanced to accept an external queryClient to avoid creating multiple instances.
|
||||
|
||||
### useAuthQuery Changes
|
||||
## Best Practices for Future Development
|
||||
|
||||
- Added initialization state tracking with useRef
|
||||
- Pre-initializes auth service when hook is first used
|
||||
- Improved mutation error handling with better cleanup
|
||||
- Fixed TypeScript errors with proper type assertions
|
||||
- Enhanced query settings for better reliability and performance
|
||||
When working with dual authentication systems:
|
||||
|
||||
## Testing
|
||||
1. **Data Fetching vs. Auth State Management**
|
||||
- Keep React Query for data fetching regardless of which auth system is used
|
||||
- Authentication state can be managed by either Zustand or React Query
|
||||
|
||||
The authentication persistence has been thoroughly tested across various scenarios:
|
||||
2. **Context Providers**
|
||||
- Always include QueryClientProvider for React Query hooks to work
|
||||
- Provide NDKContext when not using ReactQueryAuthProvider
|
||||
- Component hooks that need React Query should always be wrapped in QueryClientProvider
|
||||
|
||||
1. App restart with private key authentication
|
||||
2. App restart with external signer (Amber) authentication
|
||||
3. Logout and login within the same session
|
||||
4. Network disconnection and reconnection scenarios
|
||||
5. Force quit and restart of the application
|
||||
3. **Storage Keys**
|
||||
- Always reference storage keys from the constants file
|
||||
- Migrate legacy keys when encountered
|
||||
- Document key structure and migration paths
|
||||
|
||||
## Future Considerations
|
||||
## Testing Tips
|
||||
|
||||
- Consider adding a periodic auth token refresh mechanism
|
||||
- Explore background token validation to prevent session timeout
|
||||
- Implement token expiration handling if needed in the future
|
||||
- Potentially add multi-account support with secure credential switching
|
||||
To test authentication persistence:
|
||||
|
||||
1. Clear all security keys using the AuthPersistenceTest screen
|
||||
2. Create test keys and restart the app
|
||||
3. Verify credentials load correctly on restart
|
||||
4. Verify proper NDK initialization with loaded credentials
|
||||
5. Check logs for proper initialization sequence
|
||||
6. Toggle between auth systems to test compatibility of both approaches
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Legacy components that depend on React Query still need the QueryClientProvider wrapper
|
||||
- Components must handle the possibility that NDK might not be initialized yet
|
||||
- External signers require separate handling with dedicated hooks
|
||||
|
137
docs/technical/auth/authentication_persistence_debug_guide.md
Normal file
137
docs/technical/auth/authentication_persistence_debug_guide.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Authentication Persistence Debugging Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains the authentication persistence system in the POWR app and how to debug issues related to user authentication state not being properly restored after app restarts.
|
||||
|
||||
## Background
|
||||
|
||||
The app uses several authentication mechanisms:
|
||||
|
||||
1. **Zustand-based Auth System** - The default authentication system using a state machine pattern
|
||||
2. **React Query-based Auth System** - Alternative auth system toggled via feature flag
|
||||
3. **XState Auth System** - Partially implemented auth system (not fully deployed)
|
||||
|
||||
## Authentication Storage
|
||||
|
||||
Credentials are stored securely using `expo-secure-store` with the following key architecture:
|
||||
|
||||
| Key Name | Storage Location | Description |
|
||||
|----------|-----------------|-------------|
|
||||
| `nostr_privkey` | Standard Key | The primary location for private keys (from constants.ts) |
|
||||
| `nostr_pubkey` | Standard Key | User's public key |
|
||||
| `nostr_external_signer` | Standard Key | External signer configuration (e.g., Amber) |
|
||||
| `powr.private_key` | Legacy Key | Legacy location for private keys |
|
||||
|
||||
## Common Issues
|
||||
|
||||
### 1. Inconsistent Storage Keys
|
||||
|
||||
One of the most common issues was inconsistent storage key usage across different parts of the app. This has been fixed by:
|
||||
|
||||
- Centralizing key definitions in `lib/auth/constants.ts`
|
||||
- Creating a migration utility in `lib/auth/persistence/secureStorage.ts`
|
||||
- Ensuring all parts of the auth system use the same keys
|
||||
|
||||
### 2. Race Conditions During Init
|
||||
|
||||
Another common issue was race conditions during app initialization:
|
||||
|
||||
- NDK initialization happening before credentials are loaded
|
||||
- Auth state checks happening before provider initialization completes
|
||||
- Multiple competing auth systems trying to initialize simultaneously
|
||||
|
||||
## Implemented Solutions
|
||||
|
||||
We've implemented the following solutions to address these issues:
|
||||
|
||||
1. **Storage Key Migration**
|
||||
- The app now automatically migrates keys from legacy locations to the standardized ones
|
||||
- This happens automatically during app initialization
|
||||
- The migration occurs only once and tracks its completion state
|
||||
|
||||
2. **Improved Initialization Sequence**
|
||||
- Added proper sequencing in `app/_layout.tsx`
|
||||
- Key migration now runs before NDK initialization
|
||||
- Auth providers wait for NDK to be ready before initializing
|
||||
- Implemented proper waiting for relay connections
|
||||
|
||||
3. **AuthProvider Enhancements**
|
||||
- Enhanced error handling and logging
|
||||
- Added state inconsistency detection and recovery
|
||||
- Improved external signer handling
|
||||
|
||||
## Debugging Tools
|
||||
|
||||
### Auth Persistence Test Screen
|
||||
|
||||
A dedicated test screen is available at `app/test/auth-persistence-test.tsx` for debugging auth persistence issues. It provides:
|
||||
|
||||
- Current auth state visualization
|
||||
- SecureStore key inspection
|
||||
- Manual migration triggers
|
||||
- Test key creation for simulating scenarios
|
||||
- Key clearing functionality
|
||||
|
||||
### How to Debug Persistence Issues
|
||||
|
||||
If a user reports auth persistence problems:
|
||||
|
||||
1. Check if credentials exist in any storage location
|
||||
- Use the Auth Persistence Test screen
|
||||
- Check both standard and legacy locations
|
||||
|
||||
2. Verify migration status
|
||||
- The `auth_migration_v1_completed` key indicates if migration has run
|
||||
- If not, you can trigger it manually from the test screen
|
||||
|
||||
3. Test with a fresh key
|
||||
- Clear all keys
|
||||
- Create a test key in the legacy location
|
||||
- Force restart the app
|
||||
- Check if migration and auth restoration work properly
|
||||
|
||||
4. Check logs for initialization sequence
|
||||
- Look for `[Auth]` and `[AuthProvider]` prefixed logs
|
||||
- Verify that key migration runs before NDK initialization
|
||||
- Ensure there are no initialization errors
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Key Migration Utility
|
||||
|
||||
The key migration utility in `lib/auth/persistence/secureStorage.ts` handles:
|
||||
|
||||
- One-time migration from legacy to standard locations
|
||||
- Migration status tracking
|
||||
- Graceful handling of missing keys
|
||||
- Migration priority rules (preserve existing keys)
|
||||
|
||||
### Configuration
|
||||
|
||||
Feature flags in `lib/stores/ndk.ts` control which auth system is active:
|
||||
|
||||
```typescript
|
||||
export const FLAGS = {
|
||||
useReactQueryAuth: false, // When true, use React Query auth; when false, use legacy auth
|
||||
};
|
||||
```
|
||||
|
||||
## Troubleshooting Steps
|
||||
|
||||
If a user's authentication is not persisting:
|
||||
|
||||
1. **Check Storage Keys**: Use the Auth Persistence Test screen to see if credentials exist
|
||||
2. **Run Migration**: Try manual migration if storage shows keys in legacy locations
|
||||
3. **Test Restart Flow**: Clear keys, create test keys, and force restart the app
|
||||
4. **Toggle Auth Systems**: As a last resort, try toggling the feature flag to use the other auth system
|
||||
5. **Clear App Data**: If all else fails, have the user clear app data and re-authenticate
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Planned improvements to the auth persistence system:
|
||||
|
||||
1. Phasing out the legacy key locations completely
|
||||
2. Further unifying the auth providers into a single consistent system
|
||||
3. Adding more robust error recovery mechanisms
|
||||
4. Improving the initialization sequence to be more resilient to timing issues
|
@ -1,7 +1,11 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
|
||||
import { useAuthStore } from './AuthStateManager';
|
||||
import { AuthService } from './AuthService';
|
||||
import NDK from '@nostr-dev-kit/ndk-mobile';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { SECURE_STORE_KEYS } from './constants';
|
||||
import { migrateKeysIfNeeded } from './persistence/secureStorage';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
/**
|
||||
* Context value interface for the Auth context
|
||||
@ -25,29 +29,148 @@ interface AuthProviderProps {
|
||||
|
||||
/**
|
||||
* Provider component that makes auth service available to the app
|
||||
* Fixed with proper initialization sequence and credential handling
|
||||
*/
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children, ndk }) => {
|
||||
// Create a singleton instance of AuthService
|
||||
const [authService] = useState(() => new AuthService(ndk));
|
||||
// Subscribe to auth state for debugging/monitoring purposes
|
||||
const authState = useAuthStore();
|
||||
// Track initialization to prevent duplicate attempts
|
||||
const initializingRef = useRef(false);
|
||||
|
||||
// Initialize auth on mount
|
||||
// Initialize auth on mount with improved sequence
|
||||
useEffect(() => {
|
||||
const initAuth = async () => {
|
||||
// Prevent multiple init attempts
|
||||
if (initializingRef.current) {
|
||||
console.log("[AuthProvider] Already initializing, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
initializingRef.current = true;
|
||||
|
||||
try {
|
||||
console.log("[AuthProvider] Initializing authentication");
|
||||
console.log(`[AuthProvider] Initializing authentication (${Platform.OS})`);
|
||||
|
||||
// First, ensure NDK is fully connected before proceeding
|
||||
if (!ndk) {
|
||||
console.error("[AuthProvider] NDK is null, cannot initialize auth");
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for NDK to be connected before proceeding
|
||||
if (!ndk.pool) {
|
||||
console.log("[AuthProvider] Waiting for NDK to initialize pool...");
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 20; // 2 seconds max wait with 100ms checks
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
attempts++;
|
||||
// Check if NDK has been connected
|
||||
if (ndk.pool) {
|
||||
clearInterval(checkInterval);
|
||||
console.log("[AuthProvider] NDK pool initialized, proceeding with auth");
|
||||
resolve();
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(checkInterval);
|
||||
console.warn("[AuthProvider] Timed out waiting for relay connections, proceeding anyway");
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// Run key migration to ensure credentials are in the correct location
|
||||
await migrateKeysIfNeeded();
|
||||
|
||||
// Initialize the auth service
|
||||
await authService.initialize();
|
||||
console.log("[AuthProvider] Authentication initialized");
|
||||
console.log("[AuthProvider] Authentication service initialized");
|
||||
|
||||
// Verify auth state is consistent with stored credentials
|
||||
const privateKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
|
||||
const externalSigner = await SecureStore.getItemAsync(SECURE_STORE_KEYS.EXTERNAL_SIGNER);
|
||||
const currentState = useAuthStore.getState();
|
||||
|
||||
// If we have credentials but auth state is unauthenticated, attempt to restore
|
||||
if ((privateKey || externalSigner) && currentState.status === 'unauthenticated') {
|
||||
console.log("[AuthProvider] Auth state inconsistent with storage, attempting restore");
|
||||
|
||||
if (privateKey) {
|
||||
try {
|
||||
// DIRECT APPROACH: Create a signer with the private key
|
||||
console.log("[AuthProvider] Restoring auth with stored private key");
|
||||
|
||||
// Import NDKPrivateKeySigner for direct key operations
|
||||
const { NDKPrivateKeySigner } = await import('@nostr-dev-kit/ndk-mobile');
|
||||
|
||||
// Create a signer directly
|
||||
const signer = new NDKPrivateKeySigner(privateKey);
|
||||
ndk.signer = signer;
|
||||
|
||||
// Connect to establish NDK user
|
||||
await ndk.connect();
|
||||
|
||||
// After connecting, get the public key directly from the user object
|
||||
const publicKey = ndk.activeUser?.pubkey;
|
||||
if (!publicKey) {
|
||||
throw new Error("Failed to get public key from NDK");
|
||||
}
|
||||
console.log(`[AuthProvider] Retrieved public key: ${publicKey.substring(0, 8)}...`);
|
||||
|
||||
// Store the public key for future quick access
|
||||
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PUBKEY, publicKey);
|
||||
console.log("[AuthProvider] Saved public key to SecureStore");
|
||||
|
||||
if (ndk.activeUser) {
|
||||
console.log("[AuthProvider] NDK authenticated successfully, updating Zustand store");
|
||||
|
||||
// CRITICAL: Update the Zustand store directly
|
||||
const authStore = useAuthStore.getState();
|
||||
authStore.setAuthenticated(ndk.activeUser, 'private_key');
|
||||
|
||||
console.log("[AuthProvider] Zustand store updated with authenticated state");
|
||||
} else {
|
||||
console.error("[AuthProvider] Failed to set NDK activeUser");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[AuthProvider] Failed to restore with private key:", err);
|
||||
}
|
||||
} else if (externalSigner) {
|
||||
console.log("[AuthProvider] External signer credentials found, re-initializing auth service");
|
||||
// Just re-initialize the auth service - it will handle the external signer restoration internally
|
||||
try {
|
||||
await authService.initialize();
|
||||
|
||||
// Ensure the public key is stored for external signer too
|
||||
if (ndk.activeUser) {
|
||||
const publicKey = ndk.activeUser.pubkey;
|
||||
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PUBKEY, publicKey);
|
||||
console.log("[AuthProvider] Saved external signer public key to SecureStore");
|
||||
|
||||
// Update Zustand store directly for consistent auth state
|
||||
const authStore = useAuthStore.getState();
|
||||
authStore.setAuthenticated(ndk.activeUser, 'amber');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[AuthProvider] Failed to restore with external signer:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AuthProvider] Error initializing authentication:", error);
|
||||
} finally {
|
||||
initializingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
initAuth();
|
||||
|
||||
// No cleanup needed - AuthService instance persists for app lifetime
|
||||
}, [authService]);
|
||||
}, [ndk, authService]);
|
||||
|
||||
// Debugging: Log auth state changes
|
||||
useEffect(() => {
|
||||
|
@ -7,8 +7,9 @@ import {
|
||||
AuthMethod,
|
||||
SigningOperation
|
||||
} from "./types";
|
||||
import { SECURE_STORE_KEYS } from "./constants";
|
||||
|
||||
const PRIVATE_KEY_STORAGE_KEY = "powr.private_key";
|
||||
const PRIVATE_KEY_STORAGE_KEY = SECURE_STORE_KEYS.PRIVATE_KEY; // Use the constant from constants.ts
|
||||
|
||||
/**
|
||||
* Zustand store that manages the authentication state
|
||||
|
136
lib/auth/persistence/secureStorage.ts
Normal file
136
lib/auth/persistence/secureStorage.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Secure storage utilities for authentication
|
||||
* Handles credential storage, retrieval, and migration between key formats
|
||||
*/
|
||||
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { SECURE_STORE_KEYS } from '../constants';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
// Unique key to track migration status
|
||||
const MIGRATION_VERSION_KEY = 'auth_migration_v1_completed';
|
||||
|
||||
// Legacy storage keys that might contain credentials
|
||||
const LEGACY_KEYS = {
|
||||
PRIVATE_KEY: 'powr.private_key',
|
||||
NOSTR_PRIVKEY: 'nostr_privkey', // Original key from ndk.ts
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates credentials from legacy storage keys to the standardized keys
|
||||
* This is a one-time operation that runs at app startup
|
||||
*
|
||||
* @returns Promise that resolves when migration is complete
|
||||
*/
|
||||
export async function migrateKeysIfNeeded(): Promise<void> {
|
||||
console.log(`[Auth] Starting key migration check (${Platform.OS})`);
|
||||
|
||||
// Check if migration already happened
|
||||
const migrationDone = await SecureStore.getItemAsync(MIGRATION_VERSION_KEY);
|
||||
if (migrationDone === 'true') {
|
||||
console.log('[Auth] Migration already completed, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Auth] Performing one-time key migration');
|
||||
|
||||
// Get the current value from the standardized location (if any)
|
||||
const currentKey = await SecureStore.getItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY);
|
||||
if (currentKey) {
|
||||
console.log('[Auth] Already have credentials in standard location');
|
||||
}
|
||||
|
||||
// Check for credentials in legacy locations
|
||||
const legacyKey = await SecureStore.getItemAsync(LEGACY_KEYS.PRIVATE_KEY);
|
||||
const ndkStoreKey = await SecureStore.getItemAsync(LEGACY_KEYS.NOSTR_PRIVKEY);
|
||||
|
||||
console.log('[Auth] Storage key status:', {
|
||||
hasCurrentKey: !!currentKey,
|
||||
hasLegacyKey: !!legacyKey,
|
||||
hasNdkStoreKey: !!ndkStoreKey
|
||||
});
|
||||
|
||||
// Migration strategy: prioritize existing credentials if any
|
||||
if (!currentKey) {
|
||||
if (legacyKey) {
|
||||
console.log('[Auth] Found credentials in legacy location (powr.private_key), migrating');
|
||||
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY, legacyKey);
|
||||
console.log('[Auth] Legacy key (powr.private_key) migrated successfully');
|
||||
} else if (ndkStoreKey) {
|
||||
console.log('[Auth] Found credentials in ndk store location (nostr_privkey), migrating');
|
||||
await SecureStore.setItemAsync(SECURE_STORE_KEYS.PRIVATE_KEY, ndkStoreKey);
|
||||
console.log('[Auth] NDK store key (nostr_privkey) migrated successfully');
|
||||
}
|
||||
}
|
||||
|
||||
// Mark migration as complete regardless of outcome
|
||||
// This prevents repeated migration attempts
|
||||
await SecureStore.setItemAsync(MIGRATION_VERSION_KEY, 'true');
|
||||
console.log('[Auth] Key migration process completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all authentication credentials from secure storage
|
||||
* Handles both current and legacy keys
|
||||
*/
|
||||
export async function clearAllCredentials(): Promise<void> {
|
||||
console.log('[Auth] Clearing all stored credentials');
|
||||
|
||||
// Define all keys that might contain credentials
|
||||
const allKeys = [
|
||||
SECURE_STORE_KEYS.PRIVATE_KEY,
|
||||
SECURE_STORE_KEYS.EXTERNAL_SIGNER,
|
||||
SECURE_STORE_KEYS.PUBKEY,
|
||||
LEGACY_KEYS.PRIVATE_KEY,
|
||||
LEGACY_KEYS.NOSTR_PRIVKEY,
|
||||
];
|
||||
|
||||
// Delete all possible keys
|
||||
await Promise.all(allKeys.map(key => {
|
||||
console.log(`[Auth] Deleting key: ${key}`);
|
||||
return SecureStore.deleteItemAsync(key);
|
||||
}));
|
||||
|
||||
console.log('[Auth] All credentials cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a credential from secure storage with fallback to legacy locations
|
||||
* Useful during transition period when old keys might still be in use
|
||||
*
|
||||
* @param key The main storage key to check
|
||||
* @param legacyKeys Array of legacy keys to check as fallbacks
|
||||
* @returns The credential value if found, null otherwise
|
||||
*/
|
||||
export async function getCredentialWithFallback(
|
||||
key: string,
|
||||
legacyKeys: string[] = []
|
||||
): Promise<string | null> {
|
||||
// First try main key
|
||||
let value = await SecureStore.getItemAsync(key);
|
||||
|
||||
// If not found, try legacy keys
|
||||
if (!value) {
|
||||
for (const legacyKey of legacyKeys) {
|
||||
console.log(`[Auth] Main key not found, trying legacy key: ${legacyKey}`);
|
||||
value = await SecureStore.getItemAsync(legacyKey);
|
||||
if (value) {
|
||||
console.log(`[Auth] Found credential in legacy location: ${legacyKey}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the migration flag for testing purposes
|
||||
* Only available in development builds
|
||||
*/
|
||||
export async function resetMigration(): Promise<void> {
|
||||
if (__DEV__) {
|
||||
console.log('[Auth] Resetting migration flag for testing');
|
||||
await SecureStore.deleteItemAsync(MIGRATION_VERSION_KEY);
|
||||
}
|
||||
}
|
@ -13,13 +13,15 @@ import * as SecureStore from 'expo-secure-store';
|
||||
import { RelayService } from '@/lib/db/services/RelayService';
|
||||
import { AuthService } from '@/lib/auth/AuthService';
|
||||
|
||||
import { SECURE_STORE_KEYS } from '@/lib/auth/constants';
|
||||
|
||||
// Feature flags for authentication systems
|
||||
export const FLAGS = {
|
||||
useReactQueryAuth: true, // When true, use React Query auth; when false, use legacy auth
|
||||
useReactQueryAuth: false, // When true, use React Query auth; when false, use legacy auth
|
||||
};
|
||||
|
||||
// Constants for SecureStore
|
||||
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
||||
const PRIVATE_KEY_STORAGE_KEY = SECURE_STORE_KEYS.PRIVATE_KEY;
|
||||
|
||||
// Default relays
|
||||
const DEFAULT_RELAYS = [
|
||||
|
Loading…
x
Reference in New Issue
Block a user