// Authentication store — login, logout, session management import { defineStore } from 'pinia' import { ref } from 'vue' import { rpcClient } from '../api/rpc-client' import { useSyncStore } from './sync' export const useAuthStore = defineStore('auth', () => { // State const isAuthenticated = ref(localStorage.getItem('neode-auth') === 'true') const isLoading = ref(false) const error = ref(null) let sessionValidated = false // Actions async function login(password: string): Promise<{ requires_totp?: boolean }> { isLoading.value = true error.value = null try { const result = await rpcClient.login(password) if (result && result.requires_totp) { return { requires_totp: true } } isAuthenticated.value = true sessionValidated = true try { localStorage.setItem('neode-auth', 'true') } catch { /* localStorage full or unavailable */ } const sync = useSyncStore() // Initialize data structure immediately so dashboard can render await sync.initializeData() // Verify session cookies are established before WebSocket connect. // Without this, the WS upgrade can race ahead of cookie processing → 401. try { await rpcClient.call({ method: 'server.echo', params: { message: 'session-ready' } }) } catch { // Non-fatal: WS reconnect logic will handle it } // Connect WebSocket in background sync.connectWebSocket().catch((err) => { if (import.meta.env.DEV) console.warn('[Store] WebSocket connection failed after login, will retry:', err) }) return {} } catch (err) { error.value = err instanceof Error ? err.message : 'Login failed' throw err } finally { isLoading.value = false } } async function completeLoginAfterTotp(): Promise { isAuthenticated.value = true sessionValidated = true try { localStorage.setItem('neode-auth', 'true') } catch { /* localStorage full or unavailable */ } const sync = useSyncStore() await sync.initializeData() // Verify session cookies are established before WebSocket connect try { await rpcClient.call({ method: 'server.echo', params: { message: 'session-ready' } }) } catch { // Non-fatal: WS reconnect logic will handle it } sync.connectWebSocket().catch((err) => { if (import.meta.env.DEV) console.warn('[Store] WebSocket connection failed after TOTP login, will retry:', err) }) } async function logout(): Promise { const sync = useSyncStore() try { await rpcClient.logout() } catch (err) { if (import.meta.env.DEV) console.error('Logout error:', err) } finally { isAuthenticated.value = false sessionValidated = false localStorage.removeItem('neode-auth') sync.resetOnLogout() } } async function checkSession(): Promise { if (!localStorage.getItem('neode-auth')) { return false } try { await rpcClient.call({ method: 'server.echo', params: { message: 'ping' } }) isAuthenticated.value = true sessionValidated = true const sync = useSyncStore() await sync.initializeData() sync.connectWebSocket().catch((err) => { if (import.meta.env.DEV) console.warn('[Store] WebSocket reconnection failed, will retry:', err) }) return true } catch (err) { if (import.meta.env.DEV) console.error('[Store] Session check failed:', err) localStorage.removeItem('neode-auth') isAuthenticated.value = false sessionValidated = false const sync = useSyncStore() sync.resetOnLogout() return false } } function needsSessionValidation(): boolean { return isAuthenticated.value && !sessionValidated } return { // State isAuthenticated, isLoading, error, // Actions login, completeLoginAfterTotp, logout, checkSession, needsSessionValidation, } })