Files
paskia/frontend/src/stores/auth.js
T
LeoVasanko 8e7acd6b9e Frontend: realm admin UI, passkey realm badges, cross-realm notices
- Admin: replace Server Options dialog with per-realm management —
  realms table on the overview, add/edit/delete realm dialog backed by
  /auth/api/admin/realms/. Origins may be any well-formed origin;
  non-subdomain ones are related origins (ROR, max 5) and the dialog
  points at the .well-known/webauthn URL that must list them.
  Connectivity checks compare against the edited realm's rp-id and
  degrade to warnings instead of blocking saves.
- Host mode (limited profile) now keys off own_auth_host so realms
  sharing another realm's auth host serve the full profile locally.
- Credential list shows a realm badge on passkeys registered for a
  different rp-id than the current realm.
- Profile shows an enrollment prompt when the user has no passkey for
  the current realm (e.g. after a cross-realm remote login).
- Remote auth permit shows the requesting realm when it differs from
  the approver's own.
- settings cache can be force-refreshed after realm changes.
2026-09-06 04:50:35 +00:00

151 lines
4.6 KiB
JavaScript

import { defineStore } from 'pinia'
import { register, authenticate } from '@/utils/passkey'
import { getSettings } from '@/utils/settings'
import { apiJson, settings as paskiaSettings } from 'paskia'
import { updateThemeFromSession } from '@/utils/theme'
export const useAuthStore = defineStore('auth', {
state: () => ({
// Auth State
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info}
ctx: null, // Session context from validate
isLoading: false,
// Settings
settings: null,
// UI State
currentView: 'login',
status: {
message: '',
type: 'info',
show: false
},
}),
getters: {
},
actions: {
setLoading(flag) {
this.isLoading = !!flag
},
showMessage(message, type = 'info', duration = null) {
// Default duration: 5 seconds for errors, 3 seconds for others
const effectiveDuration = duration ?? (type === 'error' ? 5000 : 3000)
this.status = {
message,
type,
show: true
}
if (effectiveDuration > 0) {
setTimeout(() => {
this.status.show = false
}, effectiveDuration)
}
},
async setSessionCookie(result) {
if (!result?.session_token) {
console.error('setSessionCookie called with missing session_token:', result)
throw new Error('Authentication response missing session_token')
}
return await apiJson('/auth/api/set-session', {
method: 'POST',
headers: {'Authorization': `Bearer ${result.session_token}`},
timeout: paskiaSettings.auth_ms,
})
},
async register() {
this.isLoading = true
try {
const result = await register()
await this.setSessionCookie(result)
await this.loadUserInfo()
this.selectView()
return result
} finally {
this.isLoading = false
}
},
async authenticate() {
this.isLoading = true
try {
const result = await authenticate()
await this.setSessionCookie(result)
await this.loadUserInfo()
this.selectView()
return result
} finally {
this.isLoading = false
}
},
selectView() {
if (!this.userInfo) this.currentView = 'login'
else this.currentView = 'profile'
},
async loadSettings(force = false) {
this.settings = await getSettings(force)
},
async loadUserInfo() {
try {
this.userInfo = await apiJson('/auth/api/user-info', { method: 'GET', timeout: paskiaSettings.auth_ms })
updateThemeFromSession(this.userInfo)
console.log('User info loaded:', this.userInfo)
} catch (error) {
// Suppress toast for 401/403 errors - the auth iframe will handle these
if (error.status === 401 || error.status === 403) {
console.log('Authentication required:', error.message)
} else {
this.showMessage(error.message || 'Failed to load user info', 'error', 5000)
}
throw error
}
},
async deleteCredential(uuid) {
await apiJson(`/auth/api/user/credential/${uuid}`, { method: 'DELETE' })
await this.loadUserInfo()
},
async terminateSession(sessionKey) {
try {
const payload = await apiJson(`/auth/api/user/session/${sessionKey}`, { method: 'DELETE' })
if (payload?.current_session_terminated) {
sessionStorage.clear()
location.reload()
return
}
await this.loadUserInfo()
this.showMessage('Session terminated', 'success', 2500)
} catch (error) {
console.error('Terminate session error:', error)
throw error
}
},
async logout() {
try {
await apiJson('/auth/api/logout', {method: 'POST', timeout: paskiaSettings.auth_ms})
sessionStorage.clear()
location.reload()
} catch (error) {
console.error('Logout error:', error)
// Suppress toast for 401/403 errors - the auth iframe will handle these
if (error.status !== 401 && error.status !== 403) {
this.showMessage(error.message, 'error')
}
}
},
async logoutEverywhere() {
try {
await apiJson('/auth/api/user/logout-all', {method: 'POST', timeout: paskiaSettings.auth_ms})
sessionStorage.clear()
location.reload()
} catch (error) {
console.error('Logout-all error:', error)
// Suppress toast for 401/403 errors - the auth iframe will handle these
if (error.status !== 401 && error.status !== 403) {
this.showMessage(error.message, 'error')
}
}
},
}
})