Group OIDC sessions correctly in profile view.

This commit is contained in:
Leo Vasanko
2026-02-17 12:27:57 +00:00
parent 7523a49543
commit d526b2a98d
3 changed files with 28 additions and 25 deletions
+2 -2
View File
@@ -201,13 +201,13 @@ const userInfoSection = ref(null)
const hasActiveModal = computed(() => showNameDialog.value || showEmailDialog.value || showPreferredUsernameDialog.value || showRegLink.value)
watch(showNameDialog, (newVal) => { if (newVal) newName.value = authStore.userInfo?.ctx.user.display_name ?? '' })
watch(showEmailDialog, (newVal) => {
watch(showEmailDialog, (newVal) => {
if (newVal) {
newEmail.value = authStore.userInfo?.ctx.user.email ?? ''
emailError.value = ''
}
})
watch(showPreferredUsernameDialog, (newVal) => {
watch(showPreferredUsernameDialog, (newVal) => {
if (newVal) {
newPreferredUsername.value = authStore.userInfo?.ctx.user.preferred_username ?? ''
preferredUsernameError.value = ''
+15 -21
View File
@@ -7,10 +7,11 @@
<div class="section-body">
<div>
<template v-if="Array.isArray(sessions) && sessions.length">
<div v-for="(group, host) in groupedSessions" :key="host" class="session-group" tabindex="0" @keydown="handleGroupKeydown($event, host)">
<div v-for="(group, key) in groupedSessions" :key="key" class="session-group" tabindex="0" @keydown="handleGroupKeydown($event, key)">
<span :class="['session-group-host', { 'is-current-site': group.isCurrentSite }]">
<span class="session-group-icon">🌐</span>
<a v-if="host" :href="hostUrl(host)" tabindex="-1" target="_blank" rel="noopener noreferrer">{{ host }}</a>
<span class="session-group-icon">{{ group.isOIDC ? '🪪' : '🌐' }}</span>
<template v-if="group.isOIDC">{{ group.displayName }}</template>
<a v-else-if="key" :href="hostUrl(key)" tabindex="-1" target="_blank" rel="noopener noreferrer">{{ key }}</a>
<template v-else>Unbound host</template>
</span>
<div class="session-list">
@@ -220,26 +221,19 @@ const isSameHost = ip => currentHostIP.value && hostIP(ip) === currentHostIP.val
const groupedSessions = computed(() => {
const groups = {}
for (const session of props.sessions) {
const host = session.host || ''
if (!groups[host]) {
groups[host] = { sessions: [], isCurrentSite: false }
}
groups[host].sessions.push(session)
if (session.is_current_host) {
groups[host].isCurrentSite = true
const key = session.client || session.host || ''
if (!groups[key]) {
groups[key] = { sessions: [], isCurrentSite: false, isOIDC: !!session.client, displayName: session.client_name || key }
}
groups[key].sessions.push(session)
if (session.is_current_host) groups[key].isCurrentSite = true
}
// Sort sessions within each group by last_renewed descending
for (const host in groups) {
groups[host].sessions.sort((a, b) => new Date(b.last_renewed) - new Date(a.last_renewed))
}
// Sort groups by host name (natural sort)
for (const key in groups) groups[key].sessions.sort((a, b) => new Date(b.last_renewed) - new Date(a.last_renewed))
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
const sortedHosts = Object.keys(groups).sort(collator.compare)
const sortedGroups = {}
for (const host of sortedHosts) {
sortedGroups[host] = groups[host]
}
return sortedGroups
const sorted = Object.entries(groups).sort(([, a], [, b]) => {
if (a.isOIDC !== b.isOIDC) return a.isOIDC ? 1 : -1
return collator.compare(a.displayName, b.displayName) || collator.compare(a.sessions[0]?.client || '', b.sessions[0]?.client || '')
})
return Object.fromEntries(sorted)
})
</script>
+11 -2
View File
@@ -9,6 +9,7 @@ from uuid import UUID
import msgspec
from paskia import db
from paskia.db.structs import Org, Permission, Role, User
from paskia.util import useragent
@@ -75,7 +76,7 @@ class ApiPermission(Permission, kw_only=True):
return cls(uuid=p.uuid, **msgspec.structs.asdict(p))
class ApiSession(msgspec.Struct):
class ApiSession(msgspec.Struct, omit_defaults=True):
"""Session for API responses with computed fields."""
id: str
@@ -86,6 +87,8 @@ class ApiSession(msgspec.Struct):
last_renewed: datetime
is_current: bool = False
is_current_host: bool = False
client_uuid: UUID | None = msgspec.field(name="client", default=None)
client_name: str | None = None
@classmethod
def from_db(
@@ -96,6 +99,10 @@ class ApiSession(msgspec.Struct):
normalized_host: str | None,
expires_delta, # timedelta
) -> "ApiSession":
client_name = None
if s.client_uuid:
c = db.data().oid_clients.get(s.client_uuid)
client_name = c.name if c else str(s.client_uuid)
return cls(
id=s.key,
credential_uuid=s.credential_uuid,
@@ -104,7 +111,9 @@ class ApiSession(msgspec.Struct):
user_agent=useragent.compact_user_agent(s.user_agent),
last_renewed=s.expiry - expires_delta,
is_current=s.key == current_key,
is_current_host=bool(
is_current_host=not s.client_uuid and bool(
normalized_host and s.host and s.host == normalized_host
),
client_uuid=s.client_uuid,
client_name=client_name,
)