fix: resolve user identity caching issues on logout/login

- Preserve tldraw-user-id-* and crypto keys on logout (prevents duplicate cursors)
- Add session-logged-in event for immediate tool enabling after login
- Add session-cleared event for component state cleanup
- Clear only session-specific data (permissions, graph cache, room ID)
- CryptIDDropdown: reset connections state on logout
- useNetworkGraph: clear graph cache on logout

The key fix is preserving tldraw user IDs across login/logout cycles.
Previously, clearing these IDs caused each login to create a new presence
record while old ones persisted in Automerge, resulting in stacked cursors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-15 18:34:41 -05:00
parent 65eee48665
commit e94ceb39c9
3 changed files with 99 additions and 6 deletions

View File

@ -110,16 +110,22 @@ const CryptIDDropdown: React.FC<CryptIDDropdownProps> = ({ isDarkMode = false })
}
}, [session.authed, session.username]);
// Load connections when authenticated
// Load connections when authenticated, clear when logged out
useEffect(() => {
const loadConnections = async () => {
if (!session.authed || !session.username) return;
if (!session.authed || !session.username) {
// Clear connections state when user logs out
setConnections([]);
setConnectionsLoading(false);
return;
}
setConnectionsLoading(true);
try {
const myConnections = await getMyConnections();
setConnections(myConnections as UserConnectionWithProfile[]);
} catch (error) {
console.error('Failed to load connections:', error);
setConnections([]); // Clear on error too
} finally {
setConnectionsLoading(false);
}
@ -127,6 +133,23 @@ const CryptIDDropdown: React.FC<CryptIDDropdownProps> = ({ isDarkMode = false })
loadConnections();
}, [session.authed, session.username]);
// Listen for session-cleared event to immediately clear connections state
useEffect(() => {
const handleSessionCleared = () => {
console.log('🔐 CryptIDDropdown: Session cleared - resetting connections state');
setConnections([]);
setConnectionsLoading(false);
setShowDropdown(false);
setShowCryptIDModal(false);
setExpandedSection('none');
setEditingConnectionId(null);
setEditingMetadata({});
};
window.addEventListener('session-cleared', handleSessionCleared);
return () => window.removeEventListener('session-cleared', handleSessionCleared);
}, []);
// Connection handlers
const handleConnect = async (userId: string, trustLevel: TrustLevel) => {
if (!session.authed || !session.username) return;

View File

@ -310,6 +310,24 @@ export function useNetworkGraph(options: UseNetworkGraphOptions = {}): UseNetwor
fetchGraph();
}, [fetchGraph]);
// Listen for session-cleared event to immediately clear graph state
useEffect(() => {
const handleSessionCleared = () => {
console.log('🔐 useNetworkGraph: Session cleared - resetting graph state');
clearGraphCache();
setState({
nodes: [],
edges: [],
myConnections: [],
isLoading: false,
error: null,
});
};
window.addEventListener('session-cleared', handleSessionCleared);
return () => window.removeEventListener('session-cleared', handleSessionCleared);
}, []);
// Refresh interval
useEffect(() => {
if (refreshInterval > 0) {

View File

@ -30,6 +30,16 @@ export const saveSession = (session: Session): boolean => {
};
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(storedSession));
// Dispatch event to notify components that session was saved (e.g., after login)
// This helps components like Board.tsx update their state immediately
if (session.authed && session.username) {
window.dispatchEvent(new CustomEvent('session-logged-in', {
detail: { username: session.username }
}));
console.log('🔐 Session saved and session-logged-in event dispatched for:', session.username);
}
return true;
} catch (error) {
console.error('🔧 Error saving session:', error);
@ -72,15 +82,57 @@ export const loadSession = (): StoredSession | null => {
};
/**
* Clear stored session
* Clear stored session and all related user data
* This ensures a clean slate when logging out
*/
export const clearStoredSession = (): boolean => {
if (typeof window === 'undefined') return false;
try {
// Get the current username before clearing (to clean up user-specific keys)
const currentSession = loadSession();
const username = currentSession?.username;
// Clear the main session
localStorage.removeItem(SESSION_STORAGE_KEY);
// IMPORTANT: Do NOT clear tldraw-user-id-* keys!
// These must persist so the same user keeps the same presence ID across login/logout cycles.
// If we clear them, each login creates a NEW presence ID, but old presence records
// persist in the shared Automerge document and sync back - causing stacked cursors.
// IMPORTANT: Do NOT clear crypto keys or user account data - they must persist for re-login!
// The following are PRESERVED across logout (tied to user ACCOUNT, not session):
// - tldraw-user-id-${username} (presence ID - must stay same to avoid duplicate cursors)
// - ${username}_authData (crypto challenge/signature required for login)
// - ${username}_publicKey (public key for verification)
// - ${username}_fathomApiKey (integration credentials)
// - ${username}_miroApiKey (integration credentials)
// - registeredUsers (list of registered accounts on this device)
//
// Only SESSION-SPECIFIC data is cleared below:
// Clear any cached permission data (session-specific)
localStorage.removeItem('boardPermissions');
localStorage.removeItem('currentBoardPermission');
// Clear network graph cache to force fresh state (session-specific)
localStorage.removeItem('network_graph_cache');
// Clear current room ID to prevent stale room associations (session-specific)
localStorage.removeItem('currentRoomId');
// Dispatch event to notify all components to clear their state
// This helps ensure components like CryptIDDropdown, NetworkGraphPanel, etc.
// properly reset their internal state
window.dispatchEvent(new CustomEvent('session-cleared', {
detail: { previousUsername: username }
}));
console.log('🔐 Session cleared - removed session state, preserved account data (crypto keys, tldraw IDs)');
return true;
} catch (error) {
console.error('🔧 Error clearing session:', error);
return false;
}
};