Files
paskia/frontend/src/stores/auth.js
T
LeoVasanko 84985501f5 MultiSite: one instance serves authentication across many domains (#4)
- Serve multiple domains (RP IDs) from one instance: host-based dispatch,
  per-domain credentials and sessions, domains managed at runtime in the
  admin UI — previously one RP per instance
- Cross-domain sign-in via Related Origin Requests: per-domain related-origins
  list with a served .well-known/webauthn document
- Explicit per-domain origin lists with shell-glob wildcards (**. for apex +
  any subdomain depth, *. for one level), editable in the admin UI with
  validation and self-lockout guards
- Per-domain auth hosts: the account/admin UI can live on a different host
  per domain, no longer confined to subdomains of a single RP
- CLI: 'paskia init <rp-id [rp-name]' initializes or adds a domain to an
  existing database; 'paskia migrate' converts legacy databases

BREAKING CHANGES (v2.0):
- Database schema: config is now per-domain and credentials/sessions carry
  an rp_id — existing databases must be converted with 'paskia migrate'
- Origins are now explicit: main implicitly allowed every subdomain of the
  RP; configure '**.' origins to reproduce that behavior
- CLI: the flat '--rp-id/--rp-name/--origin/--auth/--save' flags are
  replaced by the 'init' and 'migrate' subcommandsReviewed-on: #4
2026-09-07 22:14:42 +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')
}
}
},
}
})