- 🌐
- {{ host }}
+ {{ group.isOIDC ? '🪪' : '🌐' }}
+ {{ group.displayName }}
+ {{ key }}
Unbound host
@@ -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)
})
diff --git a/paskia/util/apistructs.py b/paskia/util/apistructs.py
index 5cc31d6..514d954 100644
--- a/paskia/util/apistructs.py
+++ b/paskia/util/apistructs.py
@@ -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,
)