Fix user emails use userout (#2511)

Follow-up to #2495, actually ensure org subscription data is in included
in admin email response

---------

Co-authored-by: Tessa Walsh <tessa@bitarchivist.net>
This commit is contained in:
Ilya Kreymer 2025-03-24 12:04:39 -07:00 committed by GitHub
parent 46be6a0cf6
commit 21a372057b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 36 deletions

View File

@ -212,28 +212,6 @@ class UserOrgInfoOut(BaseModel):
role: UserRole role: UserRole
# ============================================================================
class UserOut(BaseModel):
"""Output User model"""
id: UUID
name: str = ""
email: EmailStr
is_superuser: bool = False
is_verified: bool = False
orgs: List[UserOrgInfoOut]
# ============================================================================
class UserEmailWithOrgInfo(BaseModel):
"""Output model for getting user email list with org info for each"""
email: EmailStr
orgs: List[UserOrgInfoOut]
# ============================================================================ # ============================================================================
### CRAWL STATES ### CRAWL STATES
@ -1833,6 +1811,8 @@ class SubscriptionCanceledResponse(BaseModel):
canceled: bool canceled: bool
# ============================================================================
# User Org Info With Subs
# ============================================================================ # ============================================================================
class UserOrgInfoOutWithSubs(UserOrgInfoOut): class UserOrgInfoOutWithSubs(UserOrgInfoOut):
"""org per user with sub info""" """org per user with sub info"""
@ -1843,6 +1823,24 @@ class UserOrgInfoOutWithSubs(UserOrgInfoOut):
subscription: Optional[Subscription] = None subscription: Optional[Subscription] = None
# ============================================================================
class UserOutNoId(BaseModel):
"""Output User Model, no ID"""
name: str = ""
email: EmailStr
orgs: List[UserOrgInfoOut | UserOrgInfoOutWithSubs]
is_verified: bool = False
# ============================================================================
class UserOut(UserOutNoId):
"""Output User Model"""
id: UUID
is_superuser: bool = False
# ============================================================================ # ============================================================================
# ORGS # ORGS
# ============================================================================ # ============================================================================
@ -2890,10 +2888,10 @@ class PaginatedCrawlErrorResponse(PaginatedResponse):
# ============================================================================ # ============================================================================
class PaginatedUserEmailsResponse(PaginatedResponse): class PaginatedUserOutResponse(PaginatedResponse):
"""Response model for user emails with org info""" """Response model for user emails with org info"""
items: List[UserEmailWithOrgInfo] items: List[UserOutNoId]
# ============================================================================ # ============================================================================

View File

@ -28,6 +28,7 @@ from .models import (
UserOrgInfoOut, UserOrgInfoOut,
UserOrgInfoOutWithSubs, UserOrgInfoOutWithSubs,
UserOut, UserOut,
UserOutNoId,
UserRole, UserRole,
InvitePending, InvitePending,
InviteOut, InviteOut,
@ -35,8 +36,7 @@ from .models import (
FailedLogin, FailedLogin,
UpdatedResponse, UpdatedResponse,
SuccessResponse, SuccessResponse,
UserEmailWithOrgInfo, PaginatedUserOutResponse,
PaginatedUserEmailsResponse,
) )
from .pagination import DEFAULT_PAGE_SIZE, paginated_format from .pagination import DEFAULT_PAGE_SIZE, paginated_format
from .utils import is_bool, dt_now from .utils import is_bool, dt_now
@ -166,8 +166,11 @@ class UserManager:
return user return user
async def get_user_info_with_orgs( async def get_user_info_with_orgs(
self, user: User, info_out_cls: Type[UserOrgInfoOut] = UserOrgInfoOut self,
) -> UserOut: user: User,
info_out_cls: Type[UserOrgInfoOut | UserOrgInfoOutWithSubs] = UserOrgInfoOut,
user_out_cls: Type[UserOut | UserOutNoId] = UserOut,
) -> UserOut | UserOutNoId:
"""return User info""" """return User info"""
user_orgs, _ = await self.org_ops.get_orgs_for_user( user_orgs, _ = await self.org_ops.get_orgs_for_user(
user, user,
@ -196,7 +199,7 @@ class UserManager:
else: else:
orgs = [] orgs = []
return UserOut( return user_out_cls(
id=user.id, id=user.id,
email=user.email, email=user.email,
name=user.name, name=user.name,
@ -558,23 +561,23 @@ class UserManager:
self, self,
page_size: int = DEFAULT_PAGE_SIZE, page_size: int = DEFAULT_PAGE_SIZE,
page: int = 1, page: int = 1,
) -> Tuple[List[UserEmailWithOrgInfo], int]: ) -> Tuple[List[UserOutNoId], int]:
"""Get user emails with org info for each for paginated endpoint""" """Get user emails with org info for each for paginated endpoint"""
# Zero-index page for query # Zero-index page for query
page = page - 1 page = page - 1
skip = page_size * page skip = page_size * page
emails: List[UserEmailWithOrgInfo] = [] emails: List[UserOutNoId] = []
total = await self.users.count_documents({"is_superuser": False}) total = await self.users.count_documents({"is_superuser": False})
async for res in self.users.find( async for res in self.users.find(
{"is_superuser": False}, skip=skip, limit=page_size {"is_superuser": False}, skip=skip, limit=page_size
): ):
user = User(**res) user = User(**res)
user_out = await self.get_user_info_with_orgs(user, UserOrgInfoOutWithSubs) user_out = await self.get_user_info_with_orgs(
emails.append( user, UserOrgInfoOutWithSubs, UserOutNoId
UserEmailWithOrgInfo(email=user_out.email, orgs=user_out.orgs)
) )
emails.append(user_out)
return emails, total return emails, total
@ -739,7 +742,7 @@ def init_users_router(
return paginated_format(pending_invites, total, page, pageSize) return paginated_format(pending_invites, total, page, pageSize)
@users_router.get( @users_router.get(
"/emails", tags=["users"], response_model=PaginatedUserEmailsResponse "/emails", tags=["users"], response_model=PaginatedUserOutResponse
) )
async def get_user_emails( async def get_user_emails(
user: User = Depends(current_active_user), user: User = Depends(current_active_user),

View File

@ -59,7 +59,6 @@ def test_me_with_orgs(crawler_auth_headers, default_org_id):
data = r.json() data = r.json()
assert data["email"] == CRAWLER_USERNAME_LOWERCASE assert data["email"] == CRAWLER_USERNAME_LOWERCASE
assert data["id"] assert data["id"]
# assert data["is_active"]
assert data["is_superuser"] is False assert data["is_superuser"] is False
assert data["is_verified"] is True assert data["is_verified"] is True
assert data["name"] == "new-crawler" assert data["name"] == "new-crawler"
@ -801,6 +800,9 @@ def test_user_emails_endpoint_superuser(admin_auth_headers, default_org_id):
for user in user_emails: for user in user_emails:
assert user["email"] assert user["email"]
assert "id" not in user
assert "is_superuser" not in user
assert user["is_verified"] == True
orgs = user.get("orgs") orgs = user.get("orgs")
if orgs == []: if orgs == []:
continue continue
@ -810,6 +812,9 @@ def test_user_emails_endpoint_superuser(admin_auth_headers, default_org_id):
assert org["name"] assert org["name"]
assert org["slug"] assert org["slug"]
assert org["default"] in (True, False) assert org["default"] in (True, False)
assert "readOnly" in org
assert "readOnlyReason" in org
assert "subscription" in org
role = org["role"] role = org["role"]
assert role assert role
assert isinstance(role, int) assert isinstance(role, int)