Better UX for profile view logout buttons.
This commit is contained in:
@@ -14,15 +14,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { getSettings } from '@/utils/settings'
|
|
||||||
import StatusMessage from '@/components/StatusMessage.vue'
|
import StatusMessage from '@/components/StatusMessage.vue'
|
||||||
import ProfileView from '@/components/ProfileView.vue'
|
import ProfileView from '@/components/ProfileView.vue'
|
||||||
const store = useAuthStore()
|
const store = useAuthStore()
|
||||||
const initialized = ref(false)
|
const initialized = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const settings = await getSettings()
|
await store.loadSettings()
|
||||||
if (settings?.rp_name) document.title = settings.rp_name
|
if (store.settings?.rp_name) document.title = store.settings.rp_name
|
||||||
try { await store.loadUserInfo() } catch (_) { /* user info load errors ignored */ }
|
try { await store.loadUserInfo() } catch (_) { /* user info load errors ignored */ }
|
||||||
initialized.value = true
|
initialized.value = true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="view-content">
|
<div class="view-content">
|
||||||
<header class="view-header">
|
<header class="view-header">
|
||||||
<h1>👋 Welcome!</h1>
|
<h1>👋 Welcome!</h1>
|
||||||
<Breadcrumbs :entries="breadcrumbEntries" />
|
<Breadcrumbs :entries="breadcrumbEntries" />
|
||||||
<p class="view-lede">Manage your account details and passkeys.</p>
|
<p class="view-lede">Manage your account details and passkeys.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -61,10 +61,15 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<section class="section-block">
|
<section class="section-block">
|
||||||
<div class="button-row logout-row single">
|
<div class="button-row logout-row" :class="{ single: !hasMultipleSessions }">
|
||||||
<button @click="logoutEverywhere" class="btn-danger logout-button">Logout all sessions</button>
|
<button v-if="!hasMultipleSessions" @click="logoutEverywhere" class="btn-danger logout-button" :disabled="authStore.isLoading">Logout</button>
|
||||||
|
<template v-else>
|
||||||
|
<button @click="logout" class="btn-danger logout-button" :disabled="authStore.isLoading">Logout</button>
|
||||||
|
<button @click="logoutEverywhere" class="btn-danger logout-button" :disabled="authStore.isLoading">All</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p class="logout-note">Immediately revokes access for every device and browser signed in to your account.</p>
|
<p class="logout-note" v-if="!hasMultipleSessions">End your current session on {{ currentSessionHost }}.</p>
|
||||||
|
<p class="logout-note" v-else><strong>Logout</strong> this session on {{ currentSessionHost }}, or <strong>All</strong> sessions across all sites and devices for {{ rpName }}. You'll need to log in again with your passkey afterwards.</p>
|
||||||
</section>
|
</section>
|
||||||
<RegistrationLinkModal
|
<RegistrationLinkModal
|
||||||
v-if="showRegLink"
|
v-if="showRegLink"
|
||||||
@@ -129,7 +134,12 @@ const handleDelete = async (credential) => {
|
|||||||
} catch (error) { authStore.showMessage(`Failed to delete passkey: ${error.message}`, 'error') }
|
} catch (error) { authStore.showMessage(`Failed to delete passkey: ${error.message}`, 'error') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rpName = computed(() => authStore.settings?.rp_name || 'this service')
|
||||||
const sessions = computed(() => authStore.userInfo?.sessions || [])
|
const sessions = computed(() => authStore.userInfo?.sessions || [])
|
||||||
|
const currentSessionHost = computed(() => {
|
||||||
|
const currentSession = sessions.value.find(session => session.is_current)
|
||||||
|
return currentSession?.host || 'this host'
|
||||||
|
})
|
||||||
const terminatingSessions = ref({})
|
const terminatingSessions = ref({})
|
||||||
|
|
||||||
const terminateSession = async (session) => {
|
const terminateSession = async (session) => {
|
||||||
@@ -146,8 +156,10 @@ const terminateSession = async (session) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logoutEverywhere = async () => { await authStore.logoutEverywhere() }
|
const logoutEverywhere = async () => { await authStore.logoutEverywhere() }
|
||||||
|
const logout = async () => { await authStore.logout() }
|
||||||
const openNameDialog = () => { newName.value = authStore.userInfo?.user?.user_name || ''; showNameDialog.value = true }
|
const openNameDialog = () => { newName.value = authStore.userInfo?.user?.user_name || ''; showNameDialog.value = true }
|
||||||
const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin))
|
const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin))
|
||||||
|
const hasMultipleSessions = computed(() => sessions.value.length > 1)
|
||||||
const breadcrumbEntries = computed(() => { const entries = [{ label: 'Auth', href: makeUiHref() }]; if (isAdmin.value) entries.push({ label: 'Admin', href: adminUiPath() }); return entries })
|
const breadcrumbEntries = computed(() => { const entries = [{ label: 'Auth', href: makeUiHref() }]; if (isAdmin.value) entries.push({ label: 'Admin', href: adminUiPath() }); return entries })
|
||||||
|
|
||||||
const saveName = async () => {
|
const saveName = async () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { register, authenticate } from '@/utils/passkey'
|
import { register, authenticate } from '@/utils/passkey'
|
||||||
|
import { getSettings } from '@/utils/settings'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -7,6 +8,9 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info}
|
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info}
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
settings: null,
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
currentView: 'login',
|
currentView: 'login',
|
||||||
status: {
|
status: {
|
||||||
@@ -74,6 +78,9 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
if (!this.userInfo) this.currentView = 'login'
|
if (!this.userInfo) this.currentView = 'login'
|
||||||
else this.currentView = 'profile'
|
else this.currentView = 'profile'
|
||||||
},
|
},
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = await getSettings()
|
||||||
|
},
|
||||||
async loadUserInfo() {
|
async loadUserInfo() {
|
||||||
const response = await fetch('/auth/api/user-info', { method: 'POST' })
|
const response = await fetch('/auth/api/user-info', { method: 'POST' })
|
||||||
let result = null
|
let result = null
|
||||||
|
|||||||
Reference in New Issue
Block a user