Frontend: domain terminology, object-keyed origins, 🔑 auth host in table, display-time ordering
This commit is contained in:
@@ -39,7 +39,7 @@ function normalizeHost(raw) {
|
||||
/**
|
||||
* Host mode is active when an own_auth_host is configured AND the current host differs from it.
|
||||
* In host mode, we show a limited profile view with logout and link to full profile.
|
||||
* own_auth_host (not auth_host) is used so that realms sharing another realm's auth host
|
||||
* own_auth_host (not auth_host) is used so that domains sharing another domain's auth host
|
||||
* still serve the full profile on their own hosts.
|
||||
*/
|
||||
const isHostMode = computed(() => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { apiJson, SessionValidator, settings as paskiaSettings } from 'paskia'
|
||||
import { updateThemeFromSession } from '@/utils/theme'
|
||||
import { uuidv7 } from 'uuidv7'
|
||||
import { getDirection } from '@/utils/keynav'
|
||||
import { goBack } from '@/utils/helpers'
|
||||
import { goBack, originDisplayEntries } from '@/utils/helpers'
|
||||
|
||||
const info = ref(null)
|
||||
const loading = ref(true)
|
||||
@@ -28,7 +28,7 @@ const error = ref(null)
|
||||
const orgs = ref([])
|
||||
const permissions = ref([])
|
||||
const oidcClients = ref([])
|
||||
const realms = ref([])
|
||||
const domains = ref([])
|
||||
const currentOrgId = ref(null) // UUID of selected org for detail view
|
||||
const currentUserId = ref(null) // UUID for user detail view
|
||||
const currentOidcId = ref(null) // UUID for OIDC client detail view
|
||||
@@ -175,13 +175,13 @@ async function loadAdminData() {
|
||||
oidcClients.value = Object.entries(data.oidc_clients).map(([uuid, c]) => ({ uuid, ...c }))
|
||||
}
|
||||
|
||||
// Realm list is master-admin only; callers guard on isMasterAdmin
|
||||
async function loadRealms() {
|
||||
// Domain list is master-admin only; callers guard on isMasterAdmin
|
||||
async function loadDomains() {
|
||||
try {
|
||||
realms.value = await apiJson('/auth/api/admin/realms/')
|
||||
domains.value = await apiJson('/auth/api/admin/domains/')
|
||||
} catch (e) {
|
||||
console.warn('Unable to load realms', e)
|
||||
realms.value = []
|
||||
console.warn('Unable to load domains', e)
|
||||
domains.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ function clearSensitiveState() {
|
||||
orgs.value = []
|
||||
permissions.value = []
|
||||
oidcClients.value = []
|
||||
realms.value = []
|
||||
domains.value = []
|
||||
userDetail.value = null
|
||||
editingOidcClient.value = null
|
||||
authenticated.value = false
|
||||
@@ -248,7 +248,7 @@ async function load() {
|
||||
await loadAdminData()
|
||||
// If we get here, user has admin access - now fetch user info for display
|
||||
await loadUserInfo()
|
||||
if (isMasterAdmin.value) await loadRealms()
|
||||
if (isMasterAdmin.value) await loadDomains()
|
||||
|
||||
if (!isMasterAdmin.value && isOrgAdmin.value && orgs.value.length === 1) {
|
||||
if (!window.location.hash || window.location.hash === '#overview') {
|
||||
@@ -475,8 +475,8 @@ function createPermissionForClient(clientId) {
|
||||
openDialog('perm-create', { display_name: '', scope: '', domain: clientId })
|
||||
}
|
||||
|
||||
function createRealm() {
|
||||
openDialog('realm-edit', {
|
||||
function createDomain() {
|
||||
openDialog('domain-edit', {
|
||||
isNew: true,
|
||||
rp_id: '',
|
||||
rp_name: '',
|
||||
@@ -487,31 +487,29 @@ function createRealm() {
|
||||
})
|
||||
}
|
||||
|
||||
function openRealm(realm) {
|
||||
// One combined list for editing: in-domain sites and related origins,
|
||||
// classified by hostname. The default is always shown explicitly as the
|
||||
// '*.rp_id' wildcard entry. Strip https:// scheme for editing.
|
||||
const stored = realm.origins || []
|
||||
const origins = [...(stored.length ? stored : ['*.' + realm.rp_id]), ...(realm.related_origins || [])]
|
||||
.map(o => o.replace(/^https:\/\//, ''))
|
||||
openDialog('realm-edit', {
|
||||
function openDomain(domain) {
|
||||
// One combined list for editing, in display order: in-domain sites and
|
||||
// related origins, classified by hostname. The default is always shown
|
||||
// explicitly as the '*.rp_id' wildcard entry.
|
||||
const rows = originDisplayEntries(domain)
|
||||
openDialog('domain-edit', {
|
||||
isNew: false,
|
||||
rp_id: realm.rp_id,
|
||||
rp_name: realm.rp_name || '',
|
||||
auth_host: (realm.auth_host || '').replace(/^https:\/\//, ''),
|
||||
origins,
|
||||
originValidation: origins.map(() => null),
|
||||
rp_id: domain.rp_id,
|
||||
rp_name: domain.rp_name || '',
|
||||
auth_host: rows.find(r => r.auth)?.key || '',
|
||||
origins: rows.map(r => r.key),
|
||||
originValidation: rows.map(() => null),
|
||||
wellKnownCheck: null,
|
||||
})
|
||||
}
|
||||
|
||||
function deleteRealm(realm) {
|
||||
function deleteDomain(domain) {
|
||||
openDialog('confirm', {
|
||||
message: `Delete domain "${realm.rp_id}"? This is refused while any passkeys remain registered for it.`,
|
||||
message: `Delete domain "${domain.rp_id}"? This is refused while any passkeys remain registered for it.`,
|
||||
action: async () => {
|
||||
await apiJson(`/auth/api/admin/realms/${realm.rp_id}`, { method: 'DELETE' })
|
||||
authStore.showMessage(`Domain "${realm.rp_id}" deleted.`, 'success', 2500)
|
||||
await loadRealms()
|
||||
await apiJson(`/auth/api/admin/domains/${domain.rp_id}`, { method: 'DELETE' })
|
||||
authStore.showMessage(`Domain "${domain.rp_id}" deleted.`, 'success', 2500)
|
||||
await loadDomains()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -937,39 +935,44 @@ async function submitDialog() {
|
||||
authStore.showMessage(e.message || `Failed to ${isNew ? 'create' : 'update'} OIDC client`, 'error')
|
||||
})
|
||||
return // Don't call closeDialog() again
|
||||
} else if (t === 'realm-edit') {
|
||||
} else if (t === 'domain-edit') {
|
||||
const d = dialog.value.data
|
||||
const rp_id = d.rp_id?.trim().toLowerCase()
|
||||
if (!rp_id) throw new Error('Domain (rp-id) required')
|
||||
const rp_name = d.rp_name?.trim() || ''
|
||||
const auth_host = d.auth_host?.trim() || ''
|
||||
// The combined origins list is split by hostname: entries on the
|
||||
// rp-id domain form the in-domain allow-list, entries elsewhere are
|
||||
// related origins (ROR). Wildcards ('*.app.example.com') classify by
|
||||
// their base domain. Bare hostnames are sent as-is; the backend
|
||||
// normalizes them with https://.
|
||||
const origins = []
|
||||
const related_origins = []
|
||||
// rp-id domain form the in-domain origins object (the auth host
|
||||
// entry is marked), entries elsewhere are related origins (ROR).
|
||||
// Wildcards ('*.app.example.com') classify by their base domain.
|
||||
// Keys are stored without the https:// scheme.
|
||||
const keyOf = o => o.replace(/^https:\/\//, '').replace(/\/+$/, '')
|
||||
const origins = {}
|
||||
const related = {}
|
||||
for (const o of (d.origins || []).map(o => o.trim()).filter(o => o)) {
|
||||
const key = keyOf(o)
|
||||
let hn = null
|
||||
if (o.startsWith('*.')) {
|
||||
hn = o.slice(2).replace(/\.+$/, '')
|
||||
if (key.startsWith('*.')) {
|
||||
hn = key.slice(2).replace(/\.+$/, '')
|
||||
} else {
|
||||
try { hn = new URL(o.startsWith('http') ? o : 'https://' + o).hostname } catch { continue }
|
||||
try { hn = new URL(key.startsWith('http') ? key : 'https://' + key).hostname } catch { continue }
|
||||
}
|
||||
if (!hn) continue
|
||||
if (hn === rp_id || hn.endsWith('.' + rp_id)) origins.push(o)
|
||||
else related_origins.push(o)
|
||||
if (hn === rp_id || hn.endsWith('.' + rp_id)) {
|
||||
origins[key] = key === auth_host ? { auth_host: true } : true
|
||||
} else {
|
||||
related[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
closeDialog()
|
||||
const req = d.isNew
|
||||
? apiJson('/auth/api/admin/realms/', { method: 'POST', body: { rp_id, rp_name, auth_host, origins, related_origins } })
|
||||
: apiJson(`/auth/api/admin/realms/${rp_id}`, { method: 'PATCH', body: { rp_name, auth_host, origins, related_origins } })
|
||||
? apiJson('/auth/api/admin/domains/', { method: 'POST', body: { rp_id, rp_name, origins, related } })
|
||||
: apiJson(`/auth/api/admin/domains/${rp_id}`, { method: 'PATCH', body: { rp_name, origins, related } })
|
||||
req
|
||||
.then(() => {
|
||||
authStore.showMessage(`Domain "${rp_id}" ${d.isNew ? 'created' : 'updated'}.`, 'success', 2500)
|
||||
loadRealms()
|
||||
loadDomains()
|
||||
// Reload settings to reflect rp_name changes
|
||||
authStore.loadSettings(true).then(() => {
|
||||
if (authStore.settings?.rp_name) document.title = authStore.settings.rp_name + ' Admin'
|
||||
@@ -1031,7 +1034,8 @@ async function submitDialog() {
|
||||
:orgs="orgs"
|
||||
:permissions="permissions"
|
||||
:oidc-clients="oidcClients"
|
||||
:realms="realms"
|
||||
:domains="domains"
|
||||
:current-rp-id="authStore.settings?.rp_id || ''"
|
||||
:navigation-disabled="hasActiveModal"
|
||||
:permission-summary="permissionSummary"
|
||||
@create-org="createOrg"
|
||||
@@ -1045,9 +1049,9 @@ async function submitDialog() {
|
||||
@create-oidc-client="createOidcClient"
|
||||
@open-oidc-client="openOidcClient"
|
||||
@delete-oidc-client="deleteOidcClient"
|
||||
@create-realm="createRealm"
|
||||
@open-realm="openRealm"
|
||||
@delete-realm="deleteRealm"
|
||||
@create-domain="createDomain"
|
||||
@open-domain="openDomain"
|
||||
@delete-domain="deleteDomain"
|
||||
@navigate-out="handlePanelNavigateOut"
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user