- fix: login disconnect — verify session before WebSocket connect - fix: 403 on app install — distinguish CSRF vs RBAC errors, only retry CSRF - fix: health monitor now watches ALL containers (removed skip list for backend services like nbxplorer, databases, UI containers) - fix: server.get-state added to CSRF-exempt list (read-only) - fix: ISO build includes container-specs.sh and lib/common.sh in rootfs so reconcile actually works on fresh installs - fix: gamepad nav — improved Server tab zone nav, focus styles, autofocus - chore: move L484 web-only apps to Services tab - chore: install store for cross-view install tracking Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
3.9 KiB
TypeScript
138 lines
3.9 KiB
TypeScript
// 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<string | null>(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<void> {
|
|
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<void> {
|
|
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<boolean> {
|
|
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,
|
|
}
|
|
})
|