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]
|
## [Unreleased]
|
||||||
### Added
|
### 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
|
- React Query Android Profile Optimization System
|
||||||
- Added platform-specific timeouts for network operations
|
- Added platform-specific timeouts for network operations
|
||||||
- Created fallback UI system for handling network delays
|
- 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 user experience during temporary API failures
|
||||||
|
|
||||||
### Improved
|
### 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
|
- Profile loading performance dramatically enhanced
|
||||||
- Added ultra-early content display after just 500ms
|
- Added ultra-early content display after just 500ms
|
||||||
- Implemented progressive content loading with three-tier system
|
- 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
|
- Created comprehensive logging documentation
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- Private key authentication persistence
|
||||||
- Fixed inconsistent storage key naming between legacy and React Query auth systems
|
- Fixed inconsistent storage key naming between legacy and React Query auth systems
|
||||||
- Standardized on 'nostr_privkey' for all private key storage
|
- 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
|
- Added comprehensive logging for better debugging
|
||||||
- Fixed race conditions in authentication state transitions
|
- Fixed race conditions in authentication state transitions
|
||||||
- Implemented initialization tracking to prevent duplicate auth operations
|
- 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
|
- Android profile screen hanging issues
|
||||||
- Fixed infinite loading state on profile screen with proper timeouts
|
- Fixed infinite loading state on profile screen with proper timeouts
|
||||||
@ -698,51 +734,4 @@ g
|
|||||||
- Added type safety for complex operations
|
- Added type safety for complex operations
|
||||||
- Improved error handling throughout relay management
|
- Improved error handling throughout relay management
|
||||||
|
|
||||||
# Changelog - March 8, 2025
|
# Changelog - March 8,
|
||||||
|
|
||||||
## 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
|
|
||||||
|
@ -23,6 +23,8 @@ import { useWorkoutStore } from '@/stores/workoutStore';
|
|||||||
import { ConnectivityService } from '@/lib/db/services/ConnectivityService';
|
import { ConnectivityService } from '@/lib/db/services/ConnectivityService';
|
||||||
import { AuthProvider } from '@/lib/auth/AuthProvider';
|
import { AuthProvider } from '@/lib/auth/AuthProvider';
|
||||||
import { ReactQueryAuthProvider } from '@/lib/auth/ReactQueryAuthProvider';
|
import { ReactQueryAuthProvider } from '@/lib/auth/ReactQueryAuthProvider';
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { createQueryClient } from '@/lib/queryClient';
|
||||||
|
|
||||||
// Import splash screens with improved fallback mechanism
|
// Import splash screens with improved fallback mechanism
|
||||||
let SplashComponent: React.ComponentType<{onFinish: () => void}>;
|
let SplashComponent: React.ComponentType<{onFinish: () => void}>;
|
||||||
@ -115,6 +117,8 @@ export default function RootLayout() {
|
|||||||
const { colorScheme, isDarkColorScheme } = useColorScheme();
|
const { colorScheme, isDarkColorScheme } = useColorScheme();
|
||||||
const { init } = useNDKStore();
|
const { init } = useNDKStore();
|
||||||
const initializationPromise = React.useRef<Promise<void> | null>(null);
|
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
|
// Start app initialization immediately
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -135,12 +139,23 @@ export default function RootLayout() {
|
|||||||
// Start database initialization and NDK initialization in parallel
|
// Start database initialization and NDK initialization in parallel
|
||||||
const initPromises = [];
|
const initPromises = [];
|
||||||
|
|
||||||
// Initialize NDK with timeout
|
// Initialize NDK with credentials migration first
|
||||||
const ndkPromise = init().catch(error => {
|
const ndkPromise = (async () => {
|
||||||
console.error('NDK initialization error:', error);
|
try {
|
||||||
// Continue even if NDK fails
|
// Import and run key migration before NDK init
|
||||||
return { offlineMode: true };
|
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);
|
initPromises.push(ndkPromise);
|
||||||
|
|
||||||
// Load favorites from SQLite (local operation)
|
// Load favorites from SQLite (local operation)
|
||||||
@ -232,7 +247,7 @@ export default function RootLayout() {
|
|||||||
{/* Conditionally render authentication providers based on feature flag */}
|
{/* Conditionally render authentication providers based on feature flag */}
|
||||||
{FLAGS.useReactQueryAuth ? (
|
{FLAGS.useReactQueryAuth ? (
|
||||||
/* Use React Query Auth system */
|
/* Use React Query Auth system */
|
||||||
<ReactQueryAuthProvider enableNDK={true}>
|
<ReactQueryAuthProvider enableNDK={true} queryClient={queryClient}>
|
||||||
{/* React Query specific components */}
|
{/* React Query specific components */}
|
||||||
<RelayInitializer reactQueryMode={true} />
|
<RelayInitializer reactQueryMode={true} />
|
||||||
<OfflineIndicator />
|
<OfflineIndicator />
|
||||||
@ -253,26 +268,30 @@ export default function RootLayout() {
|
|||||||
<PortalHost />
|
<PortalHost />
|
||||||
</ReactQueryAuthProvider>
|
</ReactQueryAuthProvider>
|
||||||
) : (
|
) : (
|
||||||
/* Use Legacy Auth system */
|
/* Use Legacy Auth system but still provide QueryClientProvider and NDKContext for data fetching */
|
||||||
<AuthProvider ndk={useNDKStore.getState().ndk!}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RelayInitializer reactQueryMode={false} />
|
<NDKContext.Provider value={{ ndk: useNDKStore.getState().ndk, isInitialized: true }}>
|
||||||
<OfflineIndicator />
|
<AuthProvider ndk={useNDKStore.getState().ndk!}>
|
||||||
|
<RelayInitializer reactQueryMode={false} />
|
||||||
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
|
<OfflineIndicator />
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
|
||||||
<Stack.Screen
|
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
|
||||||
name="(tabs)"
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
options={{
|
<Stack.Screen
|
||||||
headerShown: false,
|
name="(tabs)"
|
||||||
}}
|
options={{
|
||||||
/>
|
headerShown: false,
|
||||||
</Stack>
|
}}
|
||||||
|
/>
|
||||||
{/* Settings drawer needs to be outside the navigation stack */}
|
</Stack>
|
||||||
<SettingsDrawer />
|
|
||||||
|
{/* Settings drawer needs to be outside the navigation stack */}
|
||||||
<PortalHost />
|
<SettingsDrawer />
|
||||||
</AuthProvider>
|
|
||||||
|
<PortalHost />
|
||||||
|
</AuthProvider>
|
||||||
|
</NDKContext.Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
)}
|
)}
|
||||||
</SettingsDrawerProvider>
|
</SettingsDrawerProvider>
|
||||||
</ThemeProvider>
|
</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',
|
id: 'react-query-demo',
|
||||||
icon: RefreshCw,
|
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',
|
id: 'relays',
|
||||||
icon: Globe,
|
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
|
## React Query Integration Fix
|
||||||
- **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
|
|
||||||
|
|
||||||
### 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
|
1. **QueryClientProvider with Dual Auth Systems**
|
||||||
- **Initialization tracking**: Added tracking of initialization attempts to prevent duplicates
|
- Added QueryClientProvider to the legacy auth path in _layout.tsx
|
||||||
- **State management**: Improved state updates to ensure they only occur for the most recent initialization
|
- Created a shared queryClient instance that works with both auth systems
|
||||||
- **Auth state invalidation**: Force refresh of auth state after initialization
|
- Ensured NDKContext is properly provided in both paths
|
||||||
- **Error resilience**: Better error handling throughout the provider
|
|
||||||
|
|
||||||
### 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
|
## Code Changes
|
||||||
- **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
|
|
||||||
|
|
||||||
## 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
|
// In the render method:
|
||||||
- Split initialization into public `initialize()` and private `_doInitialize()` methods
|
{FLAGS.useReactQueryAuth ? (
|
||||||
- Improved error handling with try/catch blocks and proper cleanup
|
// React Query Auth system (already had QueryClientProvider internally)
|
||||||
- Added constants for secure storage keys (`POWR.PRIVATE_KEY`, etc.)
|
<ReactQueryAuthProvider enableNDK={true} queryClient={queryClient}>
|
||||||
- Enhanced login methods with optional `saveKey` parameter
|
{/* ... */}
|
||||||
- Added public key storage for faster reference
|
</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
|
The ReactQueryAuthProvider component was enhanced to accept an external queryClient to avoid creating multiple instances.
|
||||||
- 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
|
|
||||||
|
|
||||||
### useAuthQuery Changes
|
## Best Practices for Future Development
|
||||||
|
|
||||||
- Added initialization state tracking with useRef
|
When working with dual authentication systems:
|
||||||
- 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
|
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
The authentication persistence has been thoroughly tested across various scenarios:
|
3. **Storage Keys**
|
||||||
|
- Always reference storage keys from the constants file
|
||||||
|
- Migrate legacy keys when encountered
|
||||||
|
- Document key structure and migration paths
|
||||||
|
|
||||||
1. App restart with private key authentication
|
## Testing Tips
|
||||||
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
|
|
||||||
|
|
||||||
## Future Considerations
|
To test authentication persistence:
|
||||||
|
|
||||||
- Consider adding a periodic auth token refresh mechanism
|
1. Clear all security keys using the AuthPersistenceTest screen
|
||||||
- Explore background token validation to prevent session timeout
|
2. Create test keys and restart the app
|
||||||
- Implement token expiration handling if needed in the future
|
3. Verify credentials load correctly on restart
|
||||||
- Potentially add multi-account support with secure credential switching
|
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 { useAuthStore } from './AuthStateManager';
|
||||||
import { AuthService } from './AuthService';
|
import { AuthService } from './AuthService';
|
||||||
import NDK from '@nostr-dev-kit/ndk-mobile';
|
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
|
* Context value interface for the Auth context
|
||||||
@ -25,29 +29,148 @@ interface AuthProviderProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider component that makes auth service available to the app
|
* 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 }) => {
|
export const AuthProvider: React.FC<AuthProviderProps> = ({ children, ndk }) => {
|
||||||
// Create a singleton instance of AuthService
|
// Create a singleton instance of AuthService
|
||||||
const [authService] = useState(() => new AuthService(ndk));
|
const [authService] = useState(() => new AuthService(ndk));
|
||||||
// Subscribe to auth state for debugging/monitoring purposes
|
// Subscribe to auth state for debugging/monitoring purposes
|
||||||
const authState = useAuthStore();
|
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(() => {
|
useEffect(() => {
|
||||||
const initAuth = async () => {
|
const initAuth = async () => {
|
||||||
|
// Prevent multiple init attempts
|
||||||
|
if (initializingRef.current) {
|
||||||
|
console.log("[AuthProvider] Already initializing, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializingRef.current = true;
|
||||||
|
|
||||||
try {
|
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();
|
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) {
|
} catch (error) {
|
||||||
console.error("[AuthProvider] Error initializing authentication:", error);
|
console.error("[AuthProvider] Error initializing authentication:", error);
|
||||||
|
} finally {
|
||||||
|
initializingRef.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initAuth();
|
initAuth();
|
||||||
|
|
||||||
// No cleanup needed - AuthService instance persists for app lifetime
|
// No cleanup needed - AuthService instance persists for app lifetime
|
||||||
}, [authService]);
|
}, [ndk, authService]);
|
||||||
|
|
||||||
// Debugging: Log auth state changes
|
// Debugging: Log auth state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -7,8 +7,9 @@ import {
|
|||||||
AuthMethod,
|
AuthMethod,
|
||||||
SigningOperation
|
SigningOperation
|
||||||
} from "./types";
|
} 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
|
* 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 { RelayService } from '@/lib/db/services/RelayService';
|
||||||
import { AuthService } from '@/lib/auth/AuthService';
|
import { AuthService } from '@/lib/auth/AuthService';
|
||||||
|
|
||||||
|
import { SECURE_STORE_KEYS } from '@/lib/auth/constants';
|
||||||
|
|
||||||
// Feature flags for authentication systems
|
// Feature flags for authentication systems
|
||||||
export const FLAGS = {
|
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
|
// Constants for SecureStore
|
||||||
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
const PRIVATE_KEY_STORAGE_KEY = SECURE_STORE_KEYS.PRIVATE_KEY;
|
||||||
|
|
||||||
// Default relays
|
// Default relays
|
||||||
const DEFAULT_RELAYS = [
|
const DEFAULT_RELAYS = [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user