Add per-user active session counts to IAM user list

Move active_sessions field from UserDetailResponse into UserListItem
so GET /admin/users returns session counts. Uses a correlated subquery
to count non-revoked, non-expired sessions per user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-27 13:26:32 +08:00
parent a128005ae5
commit 9f7bbbfcbb
2 changed files with 29 additions and 6 deletions

View File

@ -108,12 +108,34 @@ async def list_users(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_actor: User = Depends(get_current_user), _actor: User = Depends(get_current_user),
): ):
"""Return all users with basic stats.""" """Return all users with basic stats including active session counts."""
result = await db.execute(sa.select(User).order_by(User.created_at)) active_sub = (
users = result.scalars().all() sa.select(sa.func.count())
.select_from(UserSession)
.where(
UserSession.user_id == User.id,
UserSession.revoked == False,
UserSession.expires_at > datetime.now(),
)
.correlate(User)
.scalar_subquery()
)
result = await db.execute(
sa.select(User, active_sub.label("active_sessions"))
.order_by(User.created_at)
)
rows = result.all()
return UserListResponse( return UserListResponse(
users=[UserListItem.model_validate(u) for u in users], users=[
total=len(users), UserListItem(
**UserListItem.model_validate(user).model_dump(exclude={"active_sessions"}),
active_sessions=count,
)
for user, count in rows
],
total=len(rows),
) )

View File

@ -27,6 +27,7 @@ class UserListItem(BaseModel):
totp_enabled: bool totp_enabled: bool
mfa_enforce_pending: bool mfa_enforce_pending: bool
created_at: datetime created_at: datetime
active_sessions: int = 0
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@ -37,7 +38,7 @@ class UserListResponse(BaseModel):
class UserDetailResponse(UserListItem): class UserDetailResponse(UserListItem):
active_sessions: int pass
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------