From 9f7bbbfcbb7a069e975dde769146611ae9cc105f Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Fri, 27 Feb 2026 13:26:32 +0800 Subject: [PATCH] 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 --- backend/app/routers/admin.py | 32 +++++++++++++++++++++++++++----- backend/app/schemas/admin.py | 3 ++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index 1d1228d..0b56415 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -108,12 +108,34 @@ async def list_users( db: AsyncSession = Depends(get_db), _actor: User = Depends(get_current_user), ): - """Return all users with basic stats.""" - result = await db.execute(sa.select(User).order_by(User.created_at)) - users = result.scalars().all() + """Return all users with basic stats including active session counts.""" + active_sub = ( + 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( - users=[UserListItem.model_validate(u) for u in users], - total=len(users), + users=[ + UserListItem( + **UserListItem.model_validate(user).model_dump(exclude={"active_sessions"}), + active_sessions=count, + ) + for user, count in rows + ], + total=len(rows), ) diff --git a/backend/app/schemas/admin.py b/backend/app/schemas/admin.py index 7688bc5..e1af013 100644 --- a/backend/app/schemas/admin.py +++ b/backend/app/schemas/admin.py @@ -27,6 +27,7 @@ class UserListItem(BaseModel): totp_enabled: bool mfa_enforce_pending: bool created_at: datetime + active_sessions: int = 0 model_config = ConfigDict(from_attributes=True) @@ -37,7 +38,7 @@ class UserListResponse(BaseModel): class UserDetailResponse(UserListItem): - active_sessions: int + pass # ---------------------------------------------------------------------------