- 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
151 lines
4.6 KiB
JavaScript
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')
|
|
}
|
|
}
|
|
},
|
|
}
|
|
})
|