Dorian 5bd3caf141 fix: auth, container resilience, ISO build, gamepad polish
- 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>
2026-03-30 13:35:02 +01:00

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,
}
})