Refactor validate endpoint to return session context, leaving user-info only for extra profile data. Completely separate token-info for reset tokens. Simplified by reusing same data structures in various places and mandating fields to have values not needing fallbacks. Implemented consistent AccessDenied view in profile and admin apps.
This commit is contained in:
+21
-37
@@ -2,10 +2,10 @@
|
||||
<div class="app-shell">
|
||||
<StatusMessage />
|
||||
<main class="app-main">
|
||||
<HostProfileView v-if="authenticated && isHostMode" :initializing="loading" />
|
||||
<ProfileView v-else-if="authenticated" />
|
||||
<LoadingView v-else-if="loading" :message="loadingMessage" />
|
||||
<AuthRequiredMessage v-else-if="showBackMessage" @reload="reloadPage" />
|
||||
<HostProfileView v-if="viewState === 'profile' && isHostMode" />
|
||||
<ProfileView v-else-if="viewState === 'profile'" />
|
||||
<LoadingView v-else-if="viewState === 'loading'" :message="loadingMessage" />
|
||||
<AccessDenied v-else-if="viewState === 'terminal'" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,13 +18,11 @@ import StatusMessage from '@/components/StatusMessage.vue'
|
||||
import ProfileView from '@/components/ProfileView.vue'
|
||||
import HostProfileView from '@/components/HostProfileView.vue'
|
||||
import LoadingView from '@/components/LoadingView.vue'
|
||||
import AuthRequiredMessage from '@/components/AccessDenied.vue'
|
||||
import AccessDenied from '@/components/AccessDenied.vue'
|
||||
|
||||
const store = useAuthStore()
|
||||
const loading = ref(true)
|
||||
const viewState = ref('loading') // 'loading' | 'profile' | 'terminal'
|
||||
const loadingMessage = ref('Loading...')
|
||||
const authenticated = ref(false)
|
||||
const showBackMessage = ref(false)
|
||||
|
||||
/**
|
||||
* Normalize a host string for comparison (lowercase, strip default ports).
|
||||
@@ -51,14 +49,19 @@ const isHostMode = computed(() => {
|
||||
let validationTimer = null
|
||||
let authIframe = null
|
||||
|
||||
function terminateSession() {
|
||||
store.userInfo = null
|
||||
viewState.value = 'terminal'
|
||||
}
|
||||
|
||||
async function loadUserInfo() {
|
||||
try {
|
||||
store.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
|
||||
authenticated.value = true
|
||||
loading.value = false
|
||||
viewState.value = 'profile'
|
||||
startSessionValidation()
|
||||
return true
|
||||
} catch (e) {
|
||||
} catch {
|
||||
store.userInfo = null
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -85,10 +88,6 @@ function hideAuthIframe() {
|
||||
}
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function handleAuthMessage(event) {
|
||||
const data = event.data
|
||||
if (!data?.type) return
|
||||
@@ -97,7 +96,7 @@ function handleAuthMessage(event) {
|
||||
case 'auth-success':
|
||||
// Authentication successful - reload user info
|
||||
hideAuthIframe()
|
||||
loading.value = true
|
||||
viewState.value = 'loading'
|
||||
loadingMessage.value = 'Loading user profile...'
|
||||
loadUserInfo()
|
||||
break
|
||||
@@ -117,11 +116,9 @@ function handleAuthMessage(event) {
|
||||
break
|
||||
|
||||
case 'auth-back':
|
||||
// User clicked Back - show message with reload option
|
||||
// User clicked Back - show terminal state
|
||||
hideAuthIframe()
|
||||
loading.value = false
|
||||
showBackMessage.value = true
|
||||
store.showMessage('Authentication cancelled', 'info', 3000)
|
||||
terminateSession()
|
||||
break
|
||||
|
||||
case 'auth-close-request':
|
||||
@@ -133,23 +130,10 @@ function handleAuthMessage(event) {
|
||||
|
||||
async function validateSession() {
|
||||
try {
|
||||
await apiJson('/auth/api/validate', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
})
|
||||
// If successful, session was renewed automatically
|
||||
} catch (error) {
|
||||
if (error.status === 401) {
|
||||
// Session expired - need to re-authenticate
|
||||
console.log('Session expired, requiring re-authentication')
|
||||
authenticated.value = false
|
||||
loading.value = true
|
||||
stopSessionValidation()
|
||||
showAuthIframe()
|
||||
} else {
|
||||
console.error('Session validation error:', error)
|
||||
// Don't treat network errors as session expiry
|
||||
}
|
||||
await apiJson('/auth/api/validate', { method: 'POST' })
|
||||
} catch {
|
||||
stopSessionValidation()
|
||||
terminateSession()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import CredentialList from '@/components/CredentialList.vue'
|
||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||
import StatusMessage from '@/components/StatusMessage.vue'
|
||||
import LoadingView from '@/components/LoadingView.vue'
|
||||
import AuthRequiredMessage from '@/components/AccessDenied.vue'
|
||||
import AccessDenied from '@/components/AccessDenied.vue'
|
||||
import AdminOverview from '@/admin/AdminOverview.vue'
|
||||
import AdminOrgDetail from '@/admin/AdminOrgDetail.vue'
|
||||
import AdminUserDetail from '@/admin/AdminUserDetail.vue'
|
||||
@@ -48,8 +48,8 @@ const adminUserDetailRef = ref(null)
|
||||
const hasActiveModal = computed(() => dialog.value.type !== null || showRegModal.value)
|
||||
|
||||
// Derive admin status from permissions
|
||||
const isGlobalAdmin = computed(() => info.value?.permissions?.includes('auth:admin') ?? false)
|
||||
const isOrgAdmin = computed(() => info.value?.permissions?.includes('auth:org:admin') ?? false)
|
||||
const isMasterAdmin = computed(() => info.value?.ctx.permissions.includes('auth:admin'))
|
||||
const isOrgAdmin = computed(() => info.value?.ctx.permissions.includes('auth:org:admin'))
|
||||
|
||||
function sanitizeRenameId() { if (renameIdValue.value) renameIdValue.value = renameIdValue.value.replace(safeIdRegex, '') }
|
||||
|
||||
@@ -144,10 +144,19 @@ async function loadPermissions() {
|
||||
}
|
||||
|
||||
async function loadUserInfo() {
|
||||
info.value = await apiJson('/auth/api/user-info', { method: 'POST' })
|
||||
const data = await apiJson('/auth/api/validate', { method: 'POST' })
|
||||
info.value = data
|
||||
authenticated.value = true
|
||||
}
|
||||
|
||||
function clearSensitiveState() {
|
||||
info.value = null
|
||||
orgs.value = []
|
||||
permissions.value = []
|
||||
userDetail.value = null
|
||||
authenticated.value = false
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
loadingMessage.value = 'Loading...'
|
||||
@@ -158,7 +167,7 @@ async function load() {
|
||||
// If we get here, user has admin access - now fetch user info for display
|
||||
await loadUserInfo()
|
||||
|
||||
if (!isGlobalAdmin.value && isOrgAdmin.value && orgs.value.length === 1) {
|
||||
if (!isMasterAdmin.value && isOrgAdmin.value && orgs.value.length === 1) {
|
||||
if (!window.location.hash || window.location.hash === '#overview') {
|
||||
currentOrgId.value = orgs.value[0].uuid
|
||||
window.location.hash = `#org/${currentOrgId.value}`
|
||||
@@ -168,6 +177,7 @@ async function load() {
|
||||
}
|
||||
} else parseHash()
|
||||
} catch (e) {
|
||||
clearSensitiveState()
|
||||
if (e.name === 'AuthCancelledError') {
|
||||
showBackMessage.value = true
|
||||
} else {
|
||||
@@ -191,8 +201,6 @@ async function performOrgDeletion(orgUuid) {
|
||||
}
|
||||
|
||||
function deleteOrg(org) {
|
||||
if (!isGlobalAdmin.value) { authStore.showMessage('Global admin only'); return }
|
||||
|
||||
const userCount = org.roles.reduce((acc, r) => acc + r.users.length, 0)
|
||||
|
||||
if (userCount === 0) {
|
||||
@@ -333,10 +341,6 @@ function deletePermission(p) {
|
||||
} })
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const selectedOrg = computed(() => orgs.value.find(o => o.uuid === currentOrgId.value) || null)
|
||||
|
||||
function openOrg(o) {
|
||||
@@ -384,7 +388,7 @@ const breadcrumbEntries = computed(() => {
|
||||
entries.push({ label: orgToShow.display_name, href: `#org/${orgToShow.uuid}` })
|
||||
}
|
||||
if (selectedUser.value) {
|
||||
entries.push({ label: selectedUser.value.display_name || 'User', href: `#user/${selectedUser.value.uuid}` })
|
||||
entries.push({ label: selectedUser.value.display_name, href: `#user/${selectedUser.value.uuid}` })
|
||||
}
|
||||
return entries
|
||||
})
|
||||
@@ -703,23 +707,19 @@ async function submitDialog() {
|
||||
<StatusMessage />
|
||||
<main class="app-main">
|
||||
<LoadingView v-if="loading" :message="loadingMessage" />
|
||||
<AuthRequiredMessage
|
||||
v-else-if="showBackMessage"
|
||||
@reload="reloadPage"
|
||||
<AccessDenied v-else-if="showBackMessage" />
|
||||
<AccessDenied
|
||||
v-else-if="error"
|
||||
icon="⚠️"
|
||||
title="Error"
|
||||
:message="error"
|
||||
/>
|
||||
<!-- Access denied: authenticated but not admin, or error occurred -->
|
||||
<div v-else-if="error || (authenticated && !isGlobalAdmin && !isOrgAdmin)" class="access-denied-container">
|
||||
<div class="access-denied-content">
|
||||
<h2>⛔ Access Denied</h2>
|
||||
<p v-if="error" class="error-detail">{{ error }}</p>
|
||||
<p v-else class="error-detail">You do not have admin permissions for this application.</p>
|
||||
<div class="button-row">
|
||||
<button class="btn-secondary" @click="goBack">Back</button>
|
||||
<button class="btn-primary" @click="reloadPage">Reload Page</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section v-else-if="authenticated && (isGlobalAdmin || isOrgAdmin)" class="view-root view-root--wide view-admin">
|
||||
<AccessDenied
|
||||
v-else-if="authenticated && !isMasterAdmin && !isOrgAdmin"
|
||||
icon="⛔"
|
||||
message="You do not have admin permissions for this application."
|
||||
/>
|
||||
<section v-else-if="authenticated && (isMasterAdmin || isOrgAdmin)" class="view-root view-root--wide view-admin">
|
||||
<header class="view-header">
|
||||
<h1>{{ pageHeading }}</h1>
|
||||
<Breadcrumbs ref="breadcrumbsRef" :entries="breadcrumbEntries" @keydown="handleBreadcrumbKeydown" />
|
||||
@@ -729,7 +729,7 @@ async function submitDialog() {
|
||||
<div class="section-body admin-section-body">
|
||||
<div class="admin-panels">
|
||||
<AdminOverview
|
||||
v-if="!selectedUser && !selectedOrg && (isGlobalAdmin || isOrgAdmin)"
|
||||
v-if="!selectedUser && !selectedOrg && (isMasterAdmin || isOrgAdmin)"
|
||||
ref="adminOverviewRef"
|
||||
:info="info"
|
||||
:orgs="orgs"
|
||||
@@ -805,9 +805,4 @@ async function submitDialog() {
|
||||
.admin-section { margin-top: var(--space-xl); }
|
||||
.admin-section-body { display: flex; flex-direction: column; gap: var(--space-xl); }
|
||||
.admin-panels { display: flex; flex-direction: column; gap: var(--space-xl); }
|
||||
.access-denied-container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; padding: 2rem; }
|
||||
.access-denied-content { text-align: center; max-width: 480px; }
|
||||
.access-denied-content h2 { margin: 0 0 1rem; color: var(--color-heading); font-size: 1.5rem; }
|
||||
.access-denied-content .error-detail { margin: 0 0 1.5rem; color: var(--color-text-muted); }
|
||||
.access-denied-content .button-row { display: flex; gap: 0.75rem; justify-content: center; }
|
||||
</style>
|
||||
|
||||
@@ -71,12 +71,12 @@ const initializing = ref(true)
|
||||
const loading = ref(false)
|
||||
const token = ref('')
|
||||
const settings = ref(null)
|
||||
const userInfo = ref(null)
|
||||
const tokenInfo = ref(null)
|
||||
const displayName = ref('')
|
||||
const errorMessage = ref('')
|
||||
let statusTimer = null
|
||||
|
||||
const sessionDescriptor = computed(() => userInfo.value?.session_type || 'your enrollment')
|
||||
const sessionDescriptor = computed(() => tokenInfo.value?.token_type || 'your enrollment')
|
||||
const subtitleMessage = computed(() => {
|
||||
if (initializing.value) return 'Preparing your secure enrollment…'
|
||||
if (!canRegister.value) return 'This authentication link is no longer valid.'
|
||||
@@ -85,7 +85,7 @@ const subtitleMessage = computed(() => {
|
||||
|
||||
const basePath = computed(() => uiBasePath())
|
||||
|
||||
const canRegister = computed(() => !!(token.value && userInfo.value))
|
||||
const canRegister = computed(() => !!(token.value && tokenInfo.value))
|
||||
|
||||
function showMessage(message, type = 'info', duration = 3000) {
|
||||
status.show = true
|
||||
@@ -109,15 +109,16 @@ async function fetchSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
async function fetchTokenInfo() {
|
||||
if (!token.value) return
|
||||
try {
|
||||
userInfo.value = await apiJson(`/auth/api/user-info?reset=${encodeURIComponent(token.value)}`, {
|
||||
method: 'POST'
|
||||
tokenInfo.value = await apiJson('/auth/api/token-info', {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token.value}` },
|
||||
})
|
||||
displayName.value = userInfo.value?.user?.user_name || ''
|
||||
displayName.value = tokenInfo.value.display_name
|
||||
} catch (error) {
|
||||
console.error('Failed to load user info', error)
|
||||
console.error('Failed to load token info', error)
|
||||
const message = error instanceof ApiError
|
||||
? (error.data?.detail || 'The authentication link is invalid or expired.')
|
||||
: getUserFriendlyErrorMessage(error)
|
||||
@@ -196,7 +197,7 @@ onMounted(async () => {
|
||||
initializing.value = false
|
||||
return
|
||||
}
|
||||
await fetchUserInfo()
|
||||
await fetchTokenInfo()
|
||||
initializing.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -26,9 +26,9 @@ const sortedOrgs = computed(() => [...props.orgs].sort((a,b)=> {
|
||||
}))
|
||||
const sortedPermissions = computed(() => [...props.permissions].sort((a,b)=> a.scope.localeCompare(b.scope)))
|
||||
|
||||
// Derive admin status from permissions
|
||||
const isGlobalAdmin = computed(() => props.info?.permissions?.includes('auth:admin') ?? false)
|
||||
const isOrgAdmin = computed(() => props.info?.permissions?.includes('auth:org:admin') ?? false)
|
||||
// Derive admin status from permissions (info contains ctx from validate response)
|
||||
const isMasterAdmin = computed(() => props.info?.ctx.permissions.includes('auth:admin'))
|
||||
const isOrgAdmin = computed(() => props.info?.ctx.permissions.includes('auth:org:admin'))
|
||||
|
||||
function permissionDisplayName(scope) {
|
||||
return props.permissions.find(p => p.scope === scope)?.display_name || scope
|
||||
@@ -93,7 +93,7 @@ function handleTableKeydown(event, tableType) {
|
||||
} else if (direction === 'down' && currentIndex === rows.length - 1) {
|
||||
// At bottom of org table, navigate to permissions section
|
||||
event.preventDefault()
|
||||
if (tableType === 'org' && isGlobalAdmin.value) {
|
||||
if (tableType === 'org' && isMasterAdmin.value) {
|
||||
// Navigate to permissions matrix or actions
|
||||
if (permMatrixRef.value) {
|
||||
const firstCheckbox = permMatrixRef.value.querySelector('input[type="checkbox"]')
|
||||
@@ -236,7 +236,7 @@ function handlePermActionsKeydown(event) {
|
||||
|
||||
// Focus helper for external navigation
|
||||
function focusFirstElement() {
|
||||
if (isGlobalAdmin.value) {
|
||||
if (isMasterAdmin.value) {
|
||||
focusPreferred(orgActionsRef.value, { itemSelector: 'button' })
|
||||
} else {
|
||||
const firstFocusable = orgTableRef.value?.querySelector('tbody tr a, tbody tr button:not([disabled])')
|
||||
@@ -249,9 +249,9 @@ defineExpose({ focusFirstElement })
|
||||
|
||||
<template>
|
||||
<div class="permissions-section" ref="orgSection">
|
||||
<h2>{{ isGlobalAdmin ? 'Organizations' : 'Your Organizations' }}</h2>
|
||||
<h2>{{ isMasterAdmin ? 'Organizations' : 'Your Organizations' }}</h2>
|
||||
<div class="actions" ref="orgActionsRef" @keydown="handleOrgActionsKeydown">
|
||||
<button v-if="isGlobalAdmin" @click="$emit('createOrg')">+ Create Org</button>
|
||||
<button v-if="isMasterAdmin" @click="$emit('createOrg')">+ Create Org</button>
|
||||
</div>
|
||||
<table class="org-table" ref="orgTableRef" @keydown="e => handleTableKeydown(e, 'org')">
|
||||
<thead>
|
||||
@@ -259,18 +259,18 @@ defineExpose({ focusFirstElement })
|
||||
<th>Name</th>
|
||||
<th>Roles</th>
|
||||
<th>Members</th>
|
||||
<th v-if="isGlobalAdmin">Actions</th>
|
||||
<th v-if="isMasterAdmin">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="o in sortedOrgs" :key="o.uuid">
|
||||
<td>
|
||||
<a href="#org/{{o.uuid}}" @click.prevent="$emit('openOrg', o)">{{ o.display_name }}</a>
|
||||
<button v-if="isGlobalAdmin || isOrgAdmin" @click="$emit('updateOrg', o)" class="icon-btn edit-org-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
||||
<button v-if="isMasterAdmin || isOrgAdmin" @click="$emit('updateOrg', o)" class="icon-btn edit-org-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
||||
</td>
|
||||
<td class="role-names">{{ getRoleNames(o) }}</td>
|
||||
<td class="center">{{ o.roles.reduce((acc,r)=>acc + r.users.length,0) }}</td>
|
||||
<td v-if="isGlobalAdmin" class="center">
|
||||
<td v-if="isMasterAdmin" class="center">
|
||||
<button @click="$emit('deleteOrg', o)" class="icon-btn delete-icon" aria-label="Delete organization" title="Delete organization">❌</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -278,7 +278,7 @@ defineExpose({ focusFirstElement })
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="isGlobalAdmin" class="permissions-section">
|
||||
<div v-if="isMasterAdmin" class="permissions-section">
|
||||
<h2>Permissions</h2>
|
||||
<div class="matrix-wrapper" ref="permMatrixRef" @keydown="handleMatrixKeydown">
|
||||
<div class="matrix-scroll">
|
||||
@@ -317,7 +317,7 @@ defineExpose({ focusFirstElement })
|
||||
<p class="matrix-hint muted">Toggle which permissions each organization can grant to its members.</p>
|
||||
</div>
|
||||
<div class="actions" ref="permActionsRef" @keydown="handlePermActionsKeydown">
|
||||
<button v-if="isGlobalAdmin" @click="$emit('openDialog', 'perm-create', { display_name: '', scope: '', domain: '' })">+ Create Permission</button>
|
||||
<button v-if="isMasterAdmin" @click="$emit('openDialog', 'perm-create', { display_name: '', scope: '', domain: '' })">+ Create Permission</button>
|
||||
</div>
|
||||
<table class="org-table" ref="permTableRef" @keydown="e => handleTableKeydown(e, 'perm')">
|
||||
<thead>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="message-container">
|
||||
<div class="message-content">
|
||||
<h2>🔒 Access Denied</h2>
|
||||
<h2>{{ icon }} {{ title }}</h2>
|
||||
<p v-if="message" class="error-detail">{{ message }}</p>
|
||||
<div class="button-row">
|
||||
<button class="btn-secondary" @click="goBack">Back</button>
|
||||
<button class="btn-primary" @click="$emit('reload')">Reload Page</button>
|
||||
<button class="btn-primary" @click="reload">Reload Page</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,7 +14,15 @@
|
||||
<script setup>
|
||||
import { goBack } from '@/utils/helpers'
|
||||
|
||||
defineEmits(['reload'])
|
||||
const props = defineProps({
|
||||
title: { type: String, default: 'Access Denied' },
|
||||
icon: { type: String, default: '🔒' },
|
||||
message: { type: String, default: null },
|
||||
})
|
||||
|
||||
function reload() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -32,10 +41,15 @@ defineEmits(['reload'])
|
||||
}
|
||||
|
||||
.message-content h2 {
|
||||
margin: 0 0 1.5rem;
|
||||
margin: 0 0 1rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
.message-content .error-detail {
|
||||
margin: 0 0 1.5rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.message-content .button-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<section class="section-block" ref="userInfoSection">
|
||||
<div class="section-body">
|
||||
<UserBasicInfo
|
||||
v-if="user"
|
||||
:name="user.user_name"
|
||||
:visits="user.visits || 0"
|
||||
:created-at="user.created_at"
|
||||
:last-seen="user.last_seen"
|
||||
v-if="ctx"
|
||||
:name="ctx.user.display_name"
|
||||
:visits="authStore.userInfo?.visits || 0"
|
||||
:created-at="authStore.userInfo?.created_at"
|
||||
:last-seen="authStore.userInfo?.last_seen"
|
||||
:org-display-name="orgDisplayName"
|
||||
:role-name="roleDisplayName"
|
||||
:can-edit="false"
|
||||
@@ -78,9 +78,9 @@ const currentHost = window.location.host
|
||||
const userInfoSection = ref(null)
|
||||
const buttonRow = ref(null)
|
||||
|
||||
const user = computed(() => authStore.userInfo?.user || null)
|
||||
const orgDisplayName = computed(() => authStore.userInfo?.org?.display_name || '')
|
||||
const roleDisplayName = computed(() => authStore.userInfo?.role?.display_name || '')
|
||||
const ctx = computed(() => authStore.userInfo?.ctx || null)
|
||||
const orgDisplayName = computed(() => ctx.value?.org.display_name ?? '')
|
||||
const roleDisplayName = computed(() => ctx.value?.role.display_name ?? '')
|
||||
|
||||
const headingTitle = computed(() => {
|
||||
const service = authStore.settings?.rp_name
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
<section class="section-block" ref="userInfoSection">
|
||||
<UserBasicInfo
|
||||
v-if="authStore.userInfo?.user"
|
||||
v-if="authStore.userInfo?.ctx"
|
||||
ref="userBasicInfo"
|
||||
:name="authStore.userInfo.user.user_name"
|
||||
:visits="authStore.userInfo.user.visits || 0"
|
||||
:created-at="authStore.userInfo.user.created_at"
|
||||
:last-seen="authStore.userInfo.user.last_seen"
|
||||
:name="authStore.userInfo.ctx.user.display_name"
|
||||
:visits="authStore.userInfo.visits"
|
||||
:created-at="authStore.userInfo.created_at"
|
||||
:last-seen="authStore.userInfo.last_seen"
|
||||
:loading="authStore.isLoading"
|
||||
update-endpoint="/auth/api/user/display-name"
|
||||
@saved="authStore.loadUserInfo()"
|
||||
@@ -151,7 +151,7 @@ const userInfoSection = ref(null)
|
||||
// Check if any modal/dialog is open (blocks arrow key navigation)
|
||||
const hasActiveModal = computed(() => showNameDialog.value || showRegLink.value)
|
||||
|
||||
watch(showNameDialog, (newVal) => { if (newVal) newName.value = authStore.userInfo?.user?.user_name || '' })
|
||||
watch(showNameDialog, (newVal) => { if (newVal) newName.value = authStore.userInfo?.ctx.user.display_name ?? '' })
|
||||
|
||||
onMounted(() => {
|
||||
updateInterval.value = setInterval(() => { if (authStore.userInfo) authStore.userInfo = { ...authStore.userInfo } }, 60000)
|
||||
@@ -323,9 +323,9 @@ const terminateSession = async (session) => {
|
||||
|
||||
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?.ctx.user.display_name ?? ''; showNameDialog.value = true }
|
||||
const isAdmin = computed(() => {
|
||||
const perms = authStore.userInfo?.permissions ?? []
|
||||
const perms = authStore.userInfo?.ctx.permissions
|
||||
return perms.includes('auth:admin') || perms.includes('auth:org:admin')
|
||||
})
|
||||
const hasMultipleSessions = computed(() => sessions.value.length > 1)
|
||||
|
||||
@@ -76,13 +76,13 @@ const status = reactive({ show: false, message: '', type: 'info' })
|
||||
const initializing = ref(true)
|
||||
const loading = ref(false)
|
||||
const settings = ref(null)
|
||||
const userInfo = ref(null)
|
||||
const session = ref(null)
|
||||
const currentView = ref('initial') // 'initial', 'login', 'forbidden'
|
||||
const authView = ref('local') // 'local' or 'remote'
|
||||
const buttonRow = ref(null)
|
||||
let statusTimer = null
|
||||
|
||||
const isAuthenticated = computed(() => !!userInfo.value?.authenticated)
|
||||
const isAuthenticated = computed(() => !!session.value)
|
||||
|
||||
const canAuthenticate = computed(() => {
|
||||
if (initializing.value) return false
|
||||
@@ -115,7 +115,7 @@ const headerMessage = computed(() => {
|
||||
return 'Please sign in with your passkey.'
|
||||
})
|
||||
|
||||
const userDisplayName = computed(() => userInfo.value?.user?.user_name || 'User')
|
||||
const userDisplayName = computed(() => session.value?.ctx.user.display_name || 'User')
|
||||
|
||||
function showMessage(message, type = 'info', duration = 3000) {
|
||||
status.show = true
|
||||
@@ -140,22 +140,21 @@ async function fetchSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
async function validateSession() {
|
||||
try {
|
||||
userInfo.value = await fetchJson('/auth/api/user-info', { method: 'POST' })
|
||||
session.value = await fetchJson('/auth/api/validate', { method: 'POST' })
|
||||
if (isAuthenticated.value && props.mode !== 'reauth') {
|
||||
currentView.value = 'forbidden'
|
||||
emit('forbidden', userInfo.value)
|
||||
emit('forbidden', session.value)
|
||||
} else {
|
||||
currentView.value = 'login'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load user info', error)
|
||||
session.value = null
|
||||
currentView.value = 'login'
|
||||
if (error.status !== 401 && error.status !== 403) {
|
||||
showMessage(getUserFriendlyErrorMessage(error), 'error', 4000)
|
||||
}
|
||||
userInfo.value = null
|
||||
currentView.value = 'login'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +187,7 @@ async function logoutUser() {
|
||||
loading.value = true
|
||||
try {
|
||||
await fetchJson('/auth/api/logout', { method: 'POST' })
|
||||
userInfo.value = null
|
||||
session.value = null
|
||||
currentView.value = 'login'
|
||||
showMessage('Logged out. You can sign in with a different account.', 'info', 3000)
|
||||
} catch (error) {
|
||||
@@ -266,7 +265,7 @@ watch(initializing, (newVal) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchSettings()
|
||||
await fetchUserInfo()
|
||||
await validateSession()
|
||||
initializing.value = false
|
||||
|
||||
// Add click handler for inline links
|
||||
|
||||
Reference in New Issue
Block a user