POWR/lib/auth/AuthStateManager.ts
DocNR 9ad50956f8 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
2025-04-05 23:47:12 -04:00

179 lines
5.6 KiB
TypeScript

import { create } from "zustand";
import { NDKUser } from "@nostr-dev-kit/ndk";
import * as SecureStore from "expo-secure-store";
import {
AuthState,
AuthActions,
AuthMethod,
SigningOperation
} from "./types";
import { SECURE_STORE_KEYS } from "./constants";
const PRIVATE_KEY_STORAGE_KEY = SECURE_STORE_KEYS.PRIVATE_KEY; // Use the constant from constants.ts
/**
* Zustand store that manages the authentication state
* Acts as a state machine to ensure consistent transitions
*/
export const useAuthStore = create<AuthState & AuthActions>((set, get) => ({
status: 'unauthenticated',
/**
* Sets the state to authenticating with the specified method
*/
setAuthenticating: (method) => {
console.log(`[Auth] Setting state to authenticating with method: ${method}`);
set({
status: 'authenticating',
method
});
},
/**
* Sets the state to authenticated with the specified user and method
*/
setAuthenticated: (user, method) => {
console.log(`[Auth] Setting state to authenticated for user: ${user.npub}`);
set({
status: 'authenticated',
user,
method,
});
},
/**
* Manages transitions to and from the signing state
* When inProgress is true, adds an operation to the signing state
* When inProgress is false, removes an operation from the signing state
*/
setSigningInProgress: (inProgress, operation) => {
const currentState = get();
if (inProgress) {
// Handle transition to signing state
if (currentState.status === 'signing') {
// Already in signing state, update the operations list and count
console.log(`[Auth] Adding operation to signing state (total: ${currentState.operationCount + 1})`);
set({
operationCount: currentState.operationCount + 1,
operations: [...currentState.operations, operation]
});
} else if (currentState.status === 'authenticated') {
// Transition from authenticated to signing
console.log(`[Auth] Transitioning from authenticated to signing state`);
set({
status: 'signing',
user: currentState.user,
method: currentState.method,
operationCount: 1,
operations: [operation]
});
} else {
// Invalid state transition - can only sign when authenticated
console.error(`[Auth] Cannot sign: not authenticated (current state: ${currentState.status})`);
set({
status: 'error',
error: new Error(`Cannot sign: not in authenticated state (current: ${currentState.status})`),
previousState: currentState
});
}
} else {
// Handle transition from signing state
if (currentState.status === 'signing') {
// Remove the completed operation
const updatedOperations = currentState.operations.filter(
op => op !== operation
);
if (updatedOperations.length === 0) {
// No more operations, return to authenticated state
console.log(`[Auth] All operations complete, returning to authenticated state`);
set({
status: 'authenticated',
user: currentState.user,
method: currentState.method
});
} else {
// Still have pending operations
console.log(`[Auth] Operation complete, ${updatedOperations.length} operations remain`);
set({
operations: updatedOperations,
operationCount: updatedOperations.length
});
}
}
// If not in signing state, this is a no-op
}
},
/**
* Performs a secure logout, clearing all auth state and secure storage
*/
logout: async () => {
try {
console.log(`[Auth] Logging out user`);
// Cancel any pending operations
const currentState = get();
if (currentState.status === 'signing') {
console.log(`[Auth] Canceling ${currentState.operations.length} pending signing operations`);
// Reject any pending operations with cancellation error
currentState.operations.forEach(operation => {
operation.reject(new Error('Authentication session terminated'));
});
}
// Securely clear all sensitive data from storage
const keysToDelete = [
PRIVATE_KEY_STORAGE_KEY,
'nostr_privkey', // Original key name from ndk store
'nostr_external_signer' // External signer info
];
// Delete all secure keys
await Promise.all(
keysToDelete.map(key => SecureStore.deleteItemAsync(key))
);
// Reset state to unauthenticated
set({
status: 'unauthenticated'
});
// Log the logout event (without PII)
console.info('[Auth] User logged out successfully');
return true;
} catch (error) {
console.error('[Auth] Error during logout:', error);
return false;
}
},
/**
* Sets the state to error with the specified error
*/
setError: (error) => {
console.error(`[Auth] Error: ${error.message}`);
const currentState = get();
set({
status: 'error',
error,
previousState: currentState
});
}
}));
/**
* Singleton for easier access to the auth store from non-React contexts
*/
export const AuthStateManager = {
getState: useAuthStore.getState,
setState: useAuthStore,
setAuthenticating: useAuthStore.getState().setAuthenticating,
setAuthenticated: useAuthStore.getState().setAuthenticated,
setSigningInProgress: useAuthStore.getState().setSigningInProgress,
logout: useAuthStore.getState().logout,
setError: useAuthStore.getState().setError
};