feat(server): admin users list surfaces is_test_account
UserDetails carries the new column, search_users selects and optionally filters on it, and the /api/admin/users route accepts ?include_test=false to hide soak-harness accounts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ class UserDetails:
|
||||
is_active: bool
|
||||
games_played: int
|
||||
games_won: int
|
||||
is_test_account: bool = False
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -55,6 +56,7 @@ class UserDetails:
|
||||
"is_active": self.is_active,
|
||||
"games_played": self.games_played,
|
||||
"games_won": self.games_won,
|
||||
"is_test_account": self.is_test_account,
|
||||
}
|
||||
|
||||
|
||||
@@ -318,6 +320,7 @@ class AdminService:
|
||||
offset: int = 0,
|
||||
include_banned: bool = True,
|
||||
include_deleted: bool = False,
|
||||
include_test: bool = True,
|
||||
) -> List[UserDetails]:
|
||||
"""
|
||||
Search users by username or email.
|
||||
@@ -328,6 +331,10 @@ class AdminService:
|
||||
offset: Number of results to skip.
|
||||
include_banned: Include banned users.
|
||||
include_deleted: Include soft-deleted users.
|
||||
include_test: Include soak-harness test accounts (default True).
|
||||
Admins see all accounts by default; the admin UI provides a
|
||||
toggle to hide test accounts. Public stats endpoints use the
|
||||
opposite default (False) so real users never see soak traffic.
|
||||
|
||||
Returns:
|
||||
List of user details.
|
||||
@@ -338,6 +345,7 @@ class AdminService:
|
||||
u.email_verified, u.is_banned, u.ban_reason,
|
||||
u.force_password_reset, u.created_at, u.last_login,
|
||||
u.last_seen_at, u.is_active,
|
||||
COALESCE(u.is_test_account, FALSE) as is_test_account,
|
||||
COALESCE(s.games_played, 0) as games_played,
|
||||
COALESCE(s.games_won, 0) as games_won
|
||||
FROM users_v2 u
|
||||
@@ -358,6 +366,9 @@ class AdminService:
|
||||
if not include_deleted:
|
||||
sql += " AND u.deleted_at IS NULL"
|
||||
|
||||
if not include_test:
|
||||
sql += " AND (u.is_test_account = FALSE OR u.is_test_account IS NULL)"
|
||||
|
||||
sql += f" ORDER BY u.created_at DESC LIMIT ${param_num} OFFSET ${param_num + 1}"
|
||||
params.extend([limit, offset])
|
||||
|
||||
@@ -379,6 +390,7 @@ class AdminService:
|
||||
is_active=row["is_active"],
|
||||
games_played=row["games_played"] or 0,
|
||||
games_won=row["games_won"] or 0,
|
||||
is_test_account=row["is_test_account"],
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
@@ -387,6 +399,10 @@ class AdminService:
|
||||
"""
|
||||
Get detailed user info by ID.
|
||||
|
||||
Note: Returns the user regardless of is_test_account status.
|
||||
Filtering by test-account only applies to list views
|
||||
(search_users). If you know the ID, you get the row.
|
||||
|
||||
Args:
|
||||
user_id: User UUID.
|
||||
|
||||
@@ -400,6 +416,7 @@ class AdminService:
|
||||
u.email_verified, u.is_banned, u.ban_reason,
|
||||
u.force_password_reset, u.created_at, u.last_login,
|
||||
u.last_seen_at, u.is_active,
|
||||
COALESCE(u.is_test_account, FALSE) as is_test_account,
|
||||
COALESCE(s.games_played, 0) as games_played,
|
||||
COALESCE(s.games_won, 0) as games_won
|
||||
FROM users_v2 u
|
||||
@@ -427,6 +444,7 @@ class AdminService:
|
||||
is_active=row["is_active"],
|
||||
games_played=row["games_played"] or 0,
|
||||
games_won=row["games_won"] or 0,
|
||||
is_test_account=row["is_test_account"],
|
||||
)
|
||||
|
||||
async def ban_user(
|
||||
|
||||
Reference in New Issue
Block a user