Refactor authentication services to async password handling; optimize bulk operations and queries

- Updated `verify_password` and `get_password_hash` to their async counterparts to prevent event loop blocking.
- Replaced N+1 query patterns in `admin.py` and `session_async.py` with optimized bulk operations for improved performance.
- Enhanced `user_async.py` with bulk update and soft delete methods for efficient user management.
- Added eager loading support in CRUD operations to prevent N+1 query issues.
- Updated test cases with stronger password examples for better security representation.
This commit is contained in:
Felipe Cardoso
2025-11-01 03:53:22 +01:00
parent 819f3ba963
commit 3fe5d301f8
17 changed files with 397 additions and 163 deletions

View File

@@ -345,54 +345,50 @@ async def admin_bulk_user_action(
db: AsyncSession = Depends(get_async_db)
) -> Any:
"""
Perform bulk actions on multiple users.
Perform bulk actions on multiple users using optimized bulk operations.
Uses single UPDATE query instead of N individual queries for efficiency.
Supported actions: activate, deactivate, delete
"""
affected_count = 0
failed_count = 0
failed_ids = []
try:
for user_id in bulk_action.user_ids:
try:
user = await user_crud.get(db, id=user_id)
if not user:
failed_count += 1
failed_ids.append(user_id)
continue
# Use efficient bulk operations instead of loop
if bulk_action.action == BulkAction.ACTIVATE:
affected_count = await user_crud.bulk_update_status(
db,
user_ids=bulk_action.user_ids,
is_active=True
)
elif bulk_action.action == BulkAction.DEACTIVATE:
affected_count = await user_crud.bulk_update_status(
db,
user_ids=bulk_action.user_ids,
is_active=False
)
elif bulk_action.action == BulkAction.DELETE:
# bulk_soft_delete automatically excludes the admin user
affected_count = await user_crud.bulk_soft_delete(
db,
user_ids=bulk_action.user_ids,
exclude_user_id=admin.id
)
else:
raise ValueError(f"Unsupported bulk action: {bulk_action.action}")
# Prevent affecting yourself
if user.id == admin.id:
failed_count += 1
failed_ids.append(user_id)
continue
if bulk_action.action == BulkAction.ACTIVATE:
await user_crud.update(db, db_obj=user, obj_in={"is_active": True})
elif bulk_action.action == BulkAction.DEACTIVATE:
await user_crud.update(db, db_obj=user, obj_in={"is_active": False})
elif bulk_action.action == BulkAction.DELETE:
await user_crud.soft_delete(db, id=user_id)
affected_count += 1
except Exception as e:
logger.error(f"Error processing user {user_id} in bulk action: {str(e)}")
failed_count += 1
failed_ids.append(user_id)
# Calculate failed count (requested - affected)
requested_count = len(bulk_action.user_ids)
failed_count = requested_count - affected_count
logger.info(
f"Admin {admin.email} performed bulk {bulk_action.action.value} "
f"on {affected_count} users ({failed_count} failed)"
f"on {affected_count} users ({failed_count} skipped/failed)"
)
return BulkActionResult(
success=failed_count == 0,
affected_count=affected_count,
failed_count=failed_count,
message=f"Bulk {bulk_action.action.value}: {affected_count} users affected, {failed_count} failed",
failed_ids=failed_ids if failed_ids else None
message=f"Bulk {bulk_action.action.value}: {affected_count} users affected, {failed_count} skipped",
failed_ids=None # Bulk operations don't track individual failures
)
except Exception as e:

View File

@@ -51,23 +51,20 @@ async def get_my_organizations(
Get all organizations the current user belongs to.
Returns organizations with member count for each.
Uses optimized single query to avoid N+1 problem.
"""
try:
orgs = await organization_crud.get_user_organizations(
# Get all org data in single query with JOIN and subquery
orgs_data = await organization_crud.get_user_organizations_with_details(
db,
user_id=current_user.id,
is_active=is_active
)
# Add member count and role to each organization
# Transform to response objects
orgs_with_data = []
for org in orgs:
role = await organization_crud.get_user_role_in_org(
db,
user_id=current_user.id,
organization_id=org.id
)
for item in orgs_data:
org = item['organization']
org_dict = {
"id": org.id,
"name": org.name,
@@ -77,7 +74,7 @@ async def get_my_organizations(
"settings": org.settings,
"created_at": org.created_at,
"updated_at": org.updated_at,
"member_count": await organization_crud.get_member_count(db, organization_id=org.id)
"member_count": item['member_count']
}
orgs_with_data.append(OrganizationResponse(**org_dict))