Refactor error handling, validation, and schema logic; improve query performance and add shared validators

- Added reusable validation functions (`validate_password_strength`, `validate_phone_number`, etc.) to centralize schema validation in `validators.py`.
- Updated `schemas/users.py` to use shared validators, simplifying and unifying validation logic.
- Introduced new error codes (`AUTH_007`, `SYS_005`) for enhanced error specificity.
- Refactored exception handling in admin routes to use more appropriate error types (`AuthorizationError`, `DuplicateError`).
- Improved organization query performance by replacing N+1 queries with optimized methods for member counts and data aggregation.
- Strengthened security in JWT decoding to prevent algorithm confusion attacks, with strict validation of required claims and algorithm enforcement.
This commit is contained in:
Felipe Cardoso
2025-11-01 01:31:10 +01:00
parent c58cce358f
commit 9ae89a20b3
6 changed files with 378 additions and 85 deletions

View File

@@ -34,7 +34,7 @@ from app.schemas.common import (
SortParams,
create_pagination_meta
)
from app.core.exceptions import NotFoundError, ErrorCode
from app.core.exceptions import NotFoundError, DuplicateError, AuthorizationError, ErrorCode
logger = logging.getLogger(__name__)
@@ -231,8 +231,9 @@ async def admin_delete_user(
# Prevent deleting yourself
if user.id == admin.id:
raise NotFoundError(
detail="Cannot delete your own account",
# Use AuthorizationError for permission/operation restrictions
raise AuthorizationError(
message="Cannot delete your own account",
error_code=ErrorCode.OPERATION_FORBIDDEN
)
@@ -310,8 +311,9 @@ async def admin_deactivate_user(
# Prevent deactivating yourself
if user.id == admin.id:
raise NotFoundError(
detail="Cannot deactivate your own account",
# Use AuthorizationError for permission/operation restrictions
raise AuthorizationError(
message="Cannot deactivate your own account",
error_code=ErrorCode.OPERATION_FORBIDDEN
)
@@ -416,19 +418,21 @@ async def admin_list_organizations(
) -> Any:
"""List all organizations with filtering and search."""
try:
orgs, total = await organization_crud.get_multi_with_filters(
# Use optimized method that gets member counts in single query (no N+1)
orgs_with_data, total = await organization_crud.get_multi_with_member_counts(
db,
skip=pagination.offset,
limit=pagination.limit,
is_active=is_active,
search=search,
sort_by="created_at",
sort_order="desc"
search=search
)
# Add member count to each organization
# Build response objects from optimized query results
orgs_with_count = []
for org in orgs:
for item in orgs_with_data:
org = item['organization']
member_count = item['member_count']
org_dict = {
"id": org.id,
"name": org.name,
@@ -438,7 +442,7 @@ async def admin_list_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": member_count
}
orgs_with_count.append(OrganizationResponse(**org_dict))
@@ -718,7 +722,12 @@ async def admin_add_organization_member(
except ValueError as e:
logger.warning(f"Failed to add user to organization: {str(e)}")
raise NotFoundError(detail=str(e), error_code=ErrorCode.ALREADY_EXISTS)
# Use DuplicateError for "already exists" scenarios
raise DuplicateError(
message=str(e),
error_code=ErrorCode.USER_ALREADY_EXISTS,
field="user_id"
)
except NotFoundError:
raise
except Exception as e: