A major refactoring for more consistent and stricter flows.
- Force using the dedicated authentication site configured via auth-host - Stricter host validation - Using the restricted app consistently for all access control (instead of the old loginview).
This commit is contained in:
@@ -2,10 +2,7 @@
|
||||
<div class="app-shell">
|
||||
<StatusMessage />
|
||||
<main class="app-main">
|
||||
<template v-if="initialized">
|
||||
<LoginView v-if="store.currentView === 'login'" />
|
||||
<ProfileView v-if="store.currentView === 'profile'" />
|
||||
</template>
|
||||
<ProfileView v-if="initialized" />
|
||||
<div v-else class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading...</p>
|
||||
@@ -17,27 +14,17 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getSettings } from '@/utils/settings'
|
||||
import StatusMessage from '@/components/StatusMessage.vue'
|
||||
import LoginView from '@/components/LoginView.vue'
|
||||
import ProfileView from '@/components/ProfileView.vue'
|
||||
const store = useAuthStore()
|
||||
const initialized = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadSettings()
|
||||
const message = location.hash.substring(1)
|
||||
if (message) {
|
||||
store.showMessage(decodeURIComponent(message), 'error')
|
||||
history.replaceState(null, '', location.pathname)
|
||||
}
|
||||
try {
|
||||
await store.loadUserInfo()
|
||||
} catch (error) {
|
||||
console.log('Failed to load user info:', error)
|
||||
} finally {
|
||||
initialized.value = true
|
||||
store.selectView()
|
||||
}
|
||||
const settings = await getSettings()
|
||||
if (settings?.rp_name) document.title = settings.rp_name
|
||||
try { await store.loadUserInfo() } catch (_) { /* user info load errors ignored */ }
|
||||
initialized.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import AdminOrgDetail from './AdminOrgDetail.vue'
|
||||
import AdminUserDetail from './AdminUserDetail.vue'
|
||||
import AdminDialogs from './AdminDialogs.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
|
||||
|
||||
const info = ref(null)
|
||||
const loading = ref(true)
|
||||
@@ -289,10 +290,8 @@ function deletePermission(p) {
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('hashchange', parseHash)
|
||||
await authStore.loadSettings()
|
||||
if (authStore.settings?.rp_name) {
|
||||
document.title = authStore.settings.rp_name + ' Admin'
|
||||
}
|
||||
const settings = await getSettings()
|
||||
if (settings?.rp_name) document.title = settings.rp_name + ' Admin'
|
||||
load()
|
||||
})
|
||||
|
||||
@@ -324,14 +323,14 @@ const selectedUser = computed(() => {
|
||||
const pageHeading = computed(() => {
|
||||
if (selectedUser.value) return 'Admin: User'
|
||||
if (selectedOrg.value) return 'Admin: Org'
|
||||
return (authStore.settings?.rp_name || 'Master') + ' Admin'
|
||||
return ((authStore.settings?.rp_name) || 'Master') + ' Admin'
|
||||
})
|
||||
|
||||
// Breadcrumb entries for admin app.
|
||||
const breadcrumbEntries = computed(() => {
|
||||
const entries = [
|
||||
{ label: 'Auth', href: authStore.uiHref() },
|
||||
{ label: 'Admin', href: authStore.adminHomeHref() }
|
||||
{ label: 'Auth', href: makeUiHref() },
|
||||
{ label: 'Admin', href: adminUiPath() }
|
||||
]
|
||||
// Determine organization for user view if selectedOrg not explicitly chosen.
|
||||
let orgForUser = null
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<div class="dialog-backdrop">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-content dialog-content--narrow">
|
||||
<header class="view-header">
|
||||
<h1>🔐 {{ (authStore.settings?.rp_name || location.origin)}}</h1>
|
||||
<p class="view-lede">User authentication is required for access.</p>
|
||||
</header>
|
||||
<section class="section-block">
|
||||
<form class="section-body" @submit.prevent="handleLogin">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primary"
|
||||
:disabled="authStore.isLoading"
|
||||
>
|
||||
{{ authStore.isLoading ? 'Authenticating...' : 'Login with Your Device' }}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
authStore.showMessage('Starting authentication...', 'info')
|
||||
await authStore.authenticate()
|
||||
authStore.showMessage('Authentication successful!', 'success', 2000)
|
||||
authStore.currentView = 'profile'
|
||||
} catch (error) {
|
||||
authStore.showMessage(error.message, 'error')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.view-lede {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.section-body {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -88,6 +88,7 @@ import NameEditForm from '@/components/NameEditForm.vue'
|
||||
import SessionList from '@/components/SessionList.vue'
|
||||
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminUiPath, makeUiHref } from '@/utils/settings'
|
||||
import passkey from '@/utils/passkey'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -147,7 +148,7 @@ const terminateSession = async (session) => {
|
||||
const logoutEverywhere = async () => { await authStore.logoutEverywhere() }
|
||||
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 breadcrumbEntries = computed(() => { const entries = [{ label: 'Auth', href: authStore.uiHref() }]; if (isAdmin.value) entries.push({ label: 'Admin', href: authStore.adminHomeHref() }); 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 name = newName.value.trim()
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import passkey from '@/utils/passkey'
|
||||
import { getSettings, uiBasePath } from '@/utils/settings'
|
||||
|
||||
const status = reactive({
|
||||
show: false,
|
||||
@@ -87,11 +88,7 @@ const subtitleMessage = computed(() => {
|
||||
return `Finish setting up a passkey for ${userInfo.value?.user?.user_name || 'your account'}.`
|
||||
})
|
||||
|
||||
const uiBasePath = computed(() => {
|
||||
const base = settings.value?.ui_base_path || '/auth/'
|
||||
if (base === '/') return '/'
|
||||
return base.endsWith('/') ? base : `${base}/`
|
||||
})
|
||||
const basePath = computed(() => uiBasePath())
|
||||
|
||||
const canRegister = computed(() => !!(token.value && userInfo.value))
|
||||
|
||||
@@ -109,13 +106,9 @@ function showMessage(message, type = 'info', duration = 3000) {
|
||||
|
||||
async function fetchSettings() {
|
||||
try {
|
||||
const res = await fetch('/auth/api/settings')
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
const data = await getSettings()
|
||||
settings.value = data
|
||||
if (data?.rp_name) {
|
||||
document.title = `${data.rp_name} · Passkey Setup`
|
||||
}
|
||||
if (data?.rp_name) document.title = `${data.rp_name} · Passkey Setup`
|
||||
} catch (error) {
|
||||
console.warn('Unable to load settings', error)
|
||||
}
|
||||
|
||||
@@ -8,29 +8,22 @@
|
||||
|
||||
<main class="view-root">
|
||||
<div class="view-content">
|
||||
<div class="surface surface--tight" style="max-width: 520px; margin: 0 auto; width: 100%;">
|
||||
<header class="view-header" style="text-align: center;">
|
||||
<h1>🚫 Access Restricted</h1>
|
||||
<div v-if="!initializing" class="surface surface--tight">
|
||||
<header class="view-header center">
|
||||
<h1>{{ headingTitle }}</h1>
|
||||
<p v-if="isAuthenticated" class="user-line">👤 {{ userDisplayName }}</p>
|
||||
<p class="view-lede">{{ headerMessage }}</p>
|
||||
</header>
|
||||
|
||||
<section class="section-block" v-if="initializing">
|
||||
<section class="section-block">
|
||||
<div class="section-body center">
|
||||
<p>Checking your session…</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block" v-else>
|
||||
<div class="section-body center" style="gap: 1.75rem;">
|
||||
<p>{{ detailText }}</p>
|
||||
|
||||
<div class="button-row center" style="justify-content: center;">
|
||||
<div class="button-row center">
|
||||
<button class="btn-secondary" :disabled="loading" @click="backNav">Back</button>
|
||||
<button v-if="canAuthenticate" class="btn-primary" :disabled="loading" @click="authenticateUser">
|
||||
{{ loading ? 'Signing in…' : 'Sign in with Passkey' }}
|
||||
</button>
|
||||
<button class="btn-secondary" :disabled="loading" @click="returnHome">
|
||||
Go back to Auth Home
|
||||
{{ loading ? 'Signing in…' : 'Login' }}
|
||||
</button>
|
||||
<button v-if="isAuthenticated" class="btn-danger" :disabled="loading" @click="logoutUser">Logout</button>
|
||||
<button v-if="isAuthenticated" class="btn-primary" :disabled="loading" @click="returnHome">Profile</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -43,64 +36,44 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import passkey from '@/utils/passkey'
|
||||
import { getSettings, uiBasePath } from '@/utils/settings'
|
||||
|
||||
const status = reactive({
|
||||
show: false,
|
||||
message: '',
|
||||
type: 'info'
|
||||
})
|
||||
|
||||
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 fallbackDetail = ref('')
|
||||
let statusTimer = null
|
||||
|
||||
const isAuthenticated = computed(() => !!userInfo.value?.authenticated)
|
||||
const canAuthenticate = computed(() => !initializing.value && !isAuthenticated.value)
|
||||
const uiBasePath = computed(() => {
|
||||
const base = settings.value?.ui_base_path || '/auth/'
|
||||
if (base === '/') return '/'
|
||||
return base.endsWith('/') ? base : `${base}/`
|
||||
const basePath = computed(() => uiBasePath())
|
||||
|
||||
const headingTitle = computed(() => {
|
||||
if (!isAuthenticated.value) return `🔐 ${settings.value?.rp_name || location.origin}`
|
||||
return '🚫 Forbidden'
|
||||
})
|
||||
|
||||
const headerMessage = computed(() => {
|
||||
if (initializing.value) return 'Checking your access permissions…'
|
||||
if (isAuthenticated.value) {
|
||||
return 'Your account is signed in, but this resource needs extra permissions.'
|
||||
}
|
||||
return 'Sign in to continue to the requested resource.'
|
||||
if (!isAuthenticated.value) return 'Please sign in to access this page.'
|
||||
return 'You lack the permissions required to access this page.'
|
||||
})
|
||||
|
||||
const detailText = computed(() => {
|
||||
if (isAuthenticated.value) {
|
||||
return fallbackDetail.value || 'You do not have the required permissions to view this page.'
|
||||
}
|
||||
return fallbackDetail.value || 'Use your registered passkey to sign in securely.'
|
||||
})
|
||||
const userDisplayName = computed(() => userInfo.value?.user?.user_name || 'User')
|
||||
|
||||
function showMessage(message, type = 'info', duration = 3000) {
|
||||
status.show = true
|
||||
status.message = message
|
||||
status.type = type
|
||||
if (statusTimer) clearTimeout(statusTimer)
|
||||
if (duration > 0) {
|
||||
statusTimer = setTimeout(() => {
|
||||
status.show = false
|
||||
}, duration)
|
||||
}
|
||||
if (duration > 0) statusTimer = setTimeout(() => { status.show = false }, duration)
|
||||
}
|
||||
|
||||
async function fetchSettings() {
|
||||
try {
|
||||
const res = await fetch('/auth/api/settings')
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
const data = await getSettings()
|
||||
settings.value = data
|
||||
if (data?.rp_name) {
|
||||
document.title = `${data.rp_name} · Access Restricted`
|
||||
}
|
||||
if (data?.rp_name) document.title = isAuthenticated.value ? `${data.rp_name} · Forbidden` : `${data.rp_name} · Sign In`
|
||||
} catch (error) {
|
||||
console.warn('Unable to load settings', error)
|
||||
}
|
||||
@@ -109,15 +82,18 @@ async function fetchSettings() {
|
||||
async function fetchUserInfo() {
|
||||
try {
|
||||
const res = await fetch('/auth/api/user-info', { method: 'POST' })
|
||||
console.log("fetchUserInfo response:", res); // Debug log
|
||||
if (!res.ok) {
|
||||
const payload = await safeParseJson(res)
|
||||
fallbackDetail.value = payload?.detail || 'Please sign in to continue.'
|
||||
showMessage(payload.detail || 'Unable to load user session info.', 'error', 2000)
|
||||
return
|
||||
}
|
||||
userInfo.value = await res.json()
|
||||
// If the user is authenticated but still here, they lack permissions.
|
||||
if (isAuthenticated.value) showMessage('Permission Denied', 'error', 2000)
|
||||
} catch (error) {
|
||||
console.error('Failed to load user info', error)
|
||||
fallbackDetail.value = 'We were unable to verify your session. Try again shortly.'
|
||||
showMessage('Could not contact the authentication server', 'error', 2000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,83 +101,76 @@ async function authenticateUser() {
|
||||
if (!canAuthenticate.value || loading.value) return
|
||||
loading.value = true
|
||||
showMessage('Starting authentication…', 'info')
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await passkey.authenticate()
|
||||
} catch (error) {
|
||||
try { result = await passkey.authenticate() } catch (error) {
|
||||
loading.value = false
|
||||
const message = error?.message || 'Passkey authentication cancelled'
|
||||
const cancelled = message === 'Passkey authentication cancelled'
|
||||
showMessage(cancelled ? message : `Authentication failed: ${message}`, cancelled ? 'info' : 'error', 4000)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await setSessionCookie(result.session_token)
|
||||
} catch (error) {
|
||||
try { await setSessionCookie(result.session_token) } catch (error) {
|
||||
loading.value = false
|
||||
const message = error?.message || 'Failed to establish session'
|
||||
showMessage(message, 'error', 4000)
|
||||
return
|
||||
}
|
||||
location.reload()
|
||||
}
|
||||
|
||||
showMessage('Signed in successfully!', 'success', 2000)
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
window.location.reload()
|
||||
}, 800)
|
||||
async function logoutUser() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try { await fetch('/auth/api/logout', { method: 'POST' }) } catch (_) { /* ignore */ }
|
||||
finally { loading.value = false; window.location.reload() }
|
||||
}
|
||||
|
||||
async function setSessionCookie(sessionToken) {
|
||||
const response = await fetch('/auth/api/set-session', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${sessionToken}`
|
||||
}
|
||||
method: 'POST', headers: { Authorization: `Bearer ${sessionToken}` }
|
||||
})
|
||||
const payload = await safeParseJson(response)
|
||||
if (!response.ok || payload?.detail) {
|
||||
const detail = payload?.detail || 'Session could not be established.'
|
||||
throw new Error(detail)
|
||||
}
|
||||
if (!response.ok || payload?.detail) throw new Error(payload?.detail || 'Session could not be established.')
|
||||
return payload
|
||||
}
|
||||
|
||||
function returnHome() {
|
||||
const target = uiBasePath.value || '/auth/'
|
||||
if (window.location.pathname !== target) {
|
||||
history.replaceState(null, '', target)
|
||||
}
|
||||
const target = basePath.value || '/auth/'
|
||||
if (window.location.pathname !== target) history.replaceState(null, '', target)
|
||||
window.location.href = target
|
||||
}
|
||||
|
||||
async function safeParseJson(response) {
|
||||
function backNav() {
|
||||
try {
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
if (history.length > 1) {
|
||||
history.back()
|
||||
return
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
returnHome()
|
||||
}
|
||||
|
||||
async function safeParseJson(response) { try { return await response.json() } catch (_) { return null } }
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchSettings()
|
||||
await fetchUserInfo()
|
||||
if (!canAuthenticate.value && !isAuthenticated.value && !fallbackDetail.value) {
|
||||
fallbackDetail.value = 'Please try signing in again.'
|
||||
}
|
||||
initializing.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-row.center {
|
||||
.button-row.center { display: flex; justify-content: center; gap: 0.75rem; }
|
||||
.user-line { margin: 0.5rem 0 0; font-weight: 500; color: var(--color-text); }
|
||||
/* Vertically center the restricted "dialog" surface in the viewport */
|
||||
main.view-root { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 2rem 1rem; }
|
||||
main.view-root .view-content { width: 100%; }
|
||||
.surface.surface--tight {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
flex-direction: column;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,6 @@ export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
// Auth State
|
||||
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info}
|
||||
settings: null, // Server provided settings (/auth/settings)
|
||||
isLoading: false,
|
||||
|
||||
// UI State
|
||||
@@ -17,15 +16,6 @@ export const useAuthStore = defineStore('auth', {
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
uiBasePath(state) {
|
||||
const configured = state.settings?.ui_base_path || '/auth/'
|
||||
if (!configured.endsWith('/')) return `${configured}/`
|
||||
return configured
|
||||
},
|
||||
adminUiPath() {
|
||||
const base = this.uiBasePath
|
||||
return base === '/' ? '/admin/' : `${base}admin/`
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setLoading(flag) {
|
||||
@@ -43,15 +33,6 @@ export const useAuthStore = defineStore('auth', {
|
||||
}, duration)
|
||||
}
|
||||
},
|
||||
uiHref(suffix = '') {
|
||||
const trimmed = suffix.startsWith('/') ? suffix.slice(1) : suffix
|
||||
if (!trimmed) return this.uiBasePath
|
||||
if (this.uiBasePath === '/') return `/${trimmed}`
|
||||
return `${this.uiBasePath}${trimmed}`
|
||||
},
|
||||
adminHomeHref() {
|
||||
return this.adminUiPath
|
||||
},
|
||||
async setSessionCookie(sessionToken) {
|
||||
const response = await fetch('/auth/api/set-session', {
|
||||
method: 'POST',
|
||||
@@ -113,19 +94,6 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.userInfo = result
|
||||
console.log('User info loaded:', result)
|
||||
},
|
||||
async loadSettings() {
|
||||
try {
|
||||
const res = await fetch('/auth/api/settings')
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
this.settings = data
|
||||
if (data?.rp_name) {
|
||||
document.title = data.rp_name
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
async deleteCredential(uuid) {
|
||||
const response = await fetch(`/auth/api/user/credential/${uuid}`, {method: 'Delete'})
|
||||
const result = await response.json()
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'
|
||||
import aWebSocket from '@/utils/awaitable-websocket'
|
||||
import { getSettings } from '@/utils/settings'
|
||||
|
||||
// Generic path normalizer: if an auth_host is configured and differs from current
|
||||
// host, return absolute URL (scheme derived by aWebSocket). Otherwise, keep as-is.
|
||||
async function makeUrl(path) {
|
||||
const s = await getSettings()
|
||||
const h = s?.auth_host
|
||||
return h && location.host !== h ? `//${h}${path}` : path
|
||||
}
|
||||
|
||||
export async function register(resetToken = null, displayName = null) {
|
||||
let params = []
|
||||
if (resetToken) params.push(`reset=${encodeURIComponent(resetToken)}`)
|
||||
if (displayName) params.push(`name=${encodeURIComponent(displayName)}`)
|
||||
const qs = params.length ? `?${params.join('&')}` : ''
|
||||
const url = `/auth/ws/register${qs}`
|
||||
const ws = await aWebSocket(url)
|
||||
const ws = await aWebSocket(await makeUrl(`/auth/ws/register${qs}`))
|
||||
try {
|
||||
const optionsJSON = await ws.receive_json()
|
||||
const registrationResponse = await startRegistration({ optionsJSON })
|
||||
@@ -23,7 +31,7 @@ export async function register(resetToken = null, displayName = null) {
|
||||
}
|
||||
|
||||
export async function authenticate() {
|
||||
const ws = await aWebSocket('/auth/ws/authenticate')
|
||||
const ws = await aWebSocket(await makeUrl('/auth/ws/authenticate'))
|
||||
try {
|
||||
const optionsJSON = await ws.receive_json()
|
||||
const authResponse = await startAuthentication({ optionsJSON })
|
||||
|
||||
29
frontend/src/utils/settings.js
Normal file
29
frontend/src/utils/settings.js
Normal file
@@ -0,0 +1,29 @@
|
||||
let _settingsPromise = null
|
||||
let _settings = null
|
||||
|
||||
export function getSettingsCached() { return _settings }
|
||||
|
||||
export async function getSettings() {
|
||||
if (_settings) return _settings
|
||||
if (_settingsPromise) return _settingsPromise
|
||||
_settingsPromise = fetch('/auth/api/settings')
|
||||
.then(r => (r.ok ? r.json() : {}))
|
||||
.then(obj => { _settings = obj || {}; return _settings })
|
||||
.catch(() => { _settings = {}; return _settings })
|
||||
return _settingsPromise
|
||||
}
|
||||
|
||||
export function uiBasePath() {
|
||||
const base = _settings?.ui_base_path || '/auth/'
|
||||
if (base === '/') return '/'
|
||||
return base.endsWith('/') ? base : base + '/'
|
||||
}
|
||||
|
||||
export function adminUiPath() { return uiBasePath() === '/' ? '/admin/' : uiBasePath() + 'admin/' }
|
||||
|
||||
export function makeUiHref(suffix = '') {
|
||||
const trimmed = suffix.startsWith('/') ? suffix.slice(1) : suffix
|
||||
if (!trimmed) return uiBasePath()
|
||||
if (uiBasePath() === '/') return '/' + trimmed
|
||||
return uiBasePath() + trimmed
|
||||
}
|
||||
Reference in New Issue
Block a user