Update admin app authentication in API mode too, reusing components between it and the main app.
This commit is contained in:
+4
-23
@@ -3,19 +3,8 @@
|
||||
<StatusMessage />
|
||||
<main class="app-main">
|
||||
<ProfileView v-if="authenticated" />
|
||||
<div v-else-if="loading" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>{{ loadingMessage }}</p>
|
||||
</div>
|
||||
<div v-else-if="showBackMessage" class="message-container">
|
||||
<div class="message-content">
|
||||
<h2>🔒 Authentication Required</h2>
|
||||
<p>You need to authenticate to access this page.</p>
|
||||
<div class="button-row">
|
||||
<button class="btn-primary" @click="reloadPage">Reload Page</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingView v-else-if="loading" :message="loadingMessage" />
|
||||
<AuthRequiredMessage v-else-if="showBackMessage" @reload="reloadPage" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,6 +14,8 @@ import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import StatusMessage from '@/components/StatusMessage.vue'
|
||||
import ProfileView from '@/components/ProfileView.vue'
|
||||
import LoadingView from '@/components/LoadingView.vue'
|
||||
import AuthRequiredMessage from '@/components/AuthRequiredMessage.vue'
|
||||
|
||||
const store = useAuthStore()
|
||||
const loading = ref(true)
|
||||
@@ -184,14 +175,4 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; gap: 1rem; }
|
||||
.loading-spinner { width: 40px; height: 40px; border: 4px solid var(--color-border); border-top: 4px solid var(--color-primary); border-radius: 50%; animation: spin 1s linear infinite; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
.loading-container p { color: var(--color-text-muted); margin: 0; }
|
||||
|
||||
.message-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; padding: 2rem; }
|
||||
.message-content { text-align: center; max-width: 480px; }
|
||||
.message-content h2 { margin: 0 0 1rem; color: var(--color-heading); }
|
||||
.message-content p { color: var(--color-text-muted); margin: 0 0 1.5rem; }
|
||||
.message-content .button-row { display: flex; gap: 0.75rem; justify-content: center; }
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount, onUnmounted, computed, watch } from 'vue'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||
import CredentialList from '@/components/CredentialList.vue'
|
||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
||||
import StatusMessage from '@/components/StatusMessage.vue'
|
||||
import LoadingView from '@/components/LoadingView.vue'
|
||||
import AuthRequiredMessage from '@/components/AuthRequiredMessage.vue'
|
||||
import AdminOverview from './AdminOverview.vue'
|
||||
import AdminOrgDetail from './AdminOrgDetail.vue'
|
||||
import AdminUserDetail from './AdminUserDetail.vue'
|
||||
@@ -14,6 +16,9 @@ import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
|
||||
|
||||
const info = ref(null)
|
||||
const loading = ref(true)
|
||||
const loadingMessage = ref('Loading...')
|
||||
const authenticated = ref(false)
|
||||
const showBackMessage = ref(false)
|
||||
const error = ref(null)
|
||||
const orgs = ref([])
|
||||
const permissions = ref([])
|
||||
@@ -31,6 +36,16 @@ const editingPermDisplay = ref(null)
|
||||
const renameDisplayValue = ref('')
|
||||
const dialog = ref({ type: null, data: null, busy: false, error: '' })
|
||||
const safeIdRegex = /[^A-Za-z0-9:._~-]/g
|
||||
let authIframe = null
|
||||
|
||||
watch(() => authStore.authRequired, (required) => {
|
||||
if (required) {
|
||||
authenticated.value = false
|
||||
loading.value = true
|
||||
showAuthIframe()
|
||||
authStore.clearAuthRequired()
|
||||
}
|
||||
})
|
||||
|
||||
function sanitizeRenameId() { if (renameIdValue.value) renameIdValue.value = renameIdValue.value.replace(safeIdRegex, '') }
|
||||
|
||||
@@ -135,9 +150,12 @@ function parseHash() {
|
||||
|
||||
async function loadOrgs() {
|
||||
const res = await fetch('/auth/admin/orgs')
|
||||
if (res.status === 401) {
|
||||
authStore.authRequired = true
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
const data = await res.json()
|
||||
if (data.detail) throw new Error(data.detail)
|
||||
// Restructure to attach users to roles instead of flat user list at org level
|
||||
orgs.value = data.map(o => {
|
||||
const roles = o.roles.map(r => ({ ...r, org_uuid: o.uuid, users: [] }))
|
||||
const roleMap = Object.fromEntries(roles.map(r => [r.display_name, r]))
|
||||
@@ -150,6 +168,10 @@ async function loadOrgs() {
|
||||
|
||||
async function loadPermissions() {
|
||||
const res = await fetch('/auth/admin/permissions')
|
||||
if (res.status === 401) {
|
||||
authStore.authRequired = true
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
const data = await res.json()
|
||||
if (data.detail) throw new Error(data.detail)
|
||||
permissions.value = data
|
||||
@@ -157,16 +179,22 @@ async function loadPermissions() {
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
loadingMessage.value = 'Loading...'
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch('/auth/api/user-info', { method: 'POST' })
|
||||
const res = await fetch('/auth/api/user-info', { method: 'POST' })
|
||||
if (res.status === 401) {
|
||||
authStore.authRequired = true
|
||||
loading.value = true
|
||||
return
|
||||
}
|
||||
const data = await res.json()
|
||||
if (data.detail) throw new Error(data.detail)
|
||||
info.value = data
|
||||
authenticated.value = true
|
||||
if (data.authenticated && (data.is_global_admin || data.is_org_admin)) {
|
||||
await Promise.all([loadOrgs(), loadPermissions()])
|
||||
}
|
||||
// After loading orgs decide view if not global admin
|
||||
if (!data.is_global_admin && data.is_org_admin && orgs.value.length === 1) {
|
||||
if (!window.location.hash || window.location.hash === '#overview') {
|
||||
currentOrgId.value = orgs.value[0].uuid
|
||||
@@ -288,11 +316,72 @@ function deletePermission(p) {
|
||||
} })
|
||||
}
|
||||
|
||||
function showAuthIframe() {
|
||||
hideAuthIframe()
|
||||
authIframe = document.createElement('iframe')
|
||||
authIframe.id = 'auth-iframe'
|
||||
authIframe.title = 'Authentication'
|
||||
authIframe.src = '/auth/restricted-api/?mode=login'
|
||||
document.body.appendChild(authIframe)
|
||||
loadingMessage.value = 'Authentication required...'
|
||||
}
|
||||
|
||||
function hideAuthIframe() {
|
||||
if (authIframe) {
|
||||
authIframe.remove()
|
||||
authIframe = null
|
||||
}
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function handleAuthMessage(event) {
|
||||
const data = event.data
|
||||
if (!data?.type) return
|
||||
|
||||
switch (data.type) {
|
||||
case 'auth-success':
|
||||
hideAuthIframe()
|
||||
loading.value = true
|
||||
loadingMessage.value = 'Loading admin panel...'
|
||||
authStore.clearAuthRequired()
|
||||
load()
|
||||
break
|
||||
|
||||
case 'auth-error':
|
||||
if (!data.cancelled) {
|
||||
authStore.showMessage(data.message || 'Authentication failed', 'error', 5000)
|
||||
}
|
||||
break
|
||||
|
||||
case 'auth-back':
|
||||
hideAuthIframe()
|
||||
loading.value = false
|
||||
showBackMessage.value = true
|
||||
break
|
||||
|
||||
case 'auth-close-request':
|
||||
hideAuthIframe()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('message', handleAuthMessage)
|
||||
window.addEventListener('hashchange', parseHash)
|
||||
const settings = await getSettings()
|
||||
if (settings?.rp_name) document.title = settings.rp_name + ' Admin'
|
||||
load()
|
||||
await load()
|
||||
if (authStore.authRequired) {
|
||||
showAuthIframe()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', handleAuthMessage)
|
||||
hideAuthIframe()
|
||||
})
|
||||
|
||||
const selectedOrg = computed(() => orgs.value.find(o => o.uuid === currentOrgId.value) || null)
|
||||
@@ -484,7 +573,13 @@ async function submitDialog() {
|
||||
<div class="app-shell admin-shell">
|
||||
<StatusMessage />
|
||||
<main class="app-main">
|
||||
<section class="view-root view-root--wide view-admin">
|
||||
<LoadingView v-if="loading" :message="loadingMessage" />
|
||||
<AuthRequiredMessage
|
||||
v-else-if="showBackMessage"
|
||||
message="You need to authenticate to access the admin panel."
|
||||
@reload="reloadPage"
|
||||
/>
|
||||
<section v-else class="view-root view-root--wide view-admin">
|
||||
<header class="view-header">
|
||||
<h1>{{ pageHeading }}</h1>
|
||||
<Breadcrumbs :entries="breadcrumbEntries" />
|
||||
@@ -492,8 +587,7 @@ async function submitDialog() {
|
||||
|
||||
<section class="section-block admin-section">
|
||||
<div class="section-body admin-section-body">
|
||||
<div v-if="loading" class="surface surface--tight">Loading…</div>
|
||||
<div v-else-if="error" class="surface surface--tight error">{{ error }}</div>
|
||||
<div v-if="error" class="surface surface--tight error">{{ error }}</div>
|
||||
<template v-else>
|
||||
<div v-if="!info?.authenticated" class="surface surface--tight">
|
||||
<p>You must be authenticated.</p>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="message-container">
|
||||
<div class="message-content">
|
||||
<h2>🔒 Authentication Required</h2>
|
||||
<p>{{ message }}</p>
|
||||
<div class="button-row">
|
||||
<button class="btn-primary" @click="$emit('reload')">Reload Page</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: 'You need to authenticate to access this page.'
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['reload'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.message-content h2 {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
color: var(--color-text-muted);
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.message-content .button-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: 'Loading...'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top: 4px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user