diff --git a/frontend/auth/App.vue b/frontend/auth/App.vue index 7822e23..a4a54d1 100644 --- a/frontend/auth/App.vue +++ b/frontend/auth/App.vue @@ -2,10 +2,10 @@
- - - - + + + +
@@ -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() } } diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index 68a8f6b..4a2b802 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -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() {
- + - -
-
-

⛔ Access Denied

-

{{ error }}

-

You do not have admin permissions for this application.

-
- - -
-
-
-
+ +

{{ pageHeading }}

@@ -729,7 +729,7 @@ async function submitDialog() {
diff --git a/frontend/int/reset/ResetApp.vue b/frontend/int/reset/ResetApp.vue index 9b2ad53..ff2048c 100644 --- a/frontend/int/reset/ResetApp.vue +++ b/frontend/int/reset/ResetApp.vue @@ -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 }) diff --git a/frontend/src/admin/AdminOverview.vue b/frontend/src/admin/AdminOverview.vue index e8ec2af..143da8b 100644 --- a/frontend/src/admin/AdminOverview.vue +++ b/frontend/src/admin/AdminOverview.vue @@ -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 })