forked from cardosofelipe/fast-next-template
Add pyproject.toml for consolidated project configuration and replace Black, isort, and Flake8 with Ruff
- Introduced `pyproject.toml` to centralize backend tool configurations (e.g., Ruff, mypy, coverage, pytest). - Replaced Black, isort, and Flake8 with Ruff for linting, formatting, and import sorting. - Updated `requirements.txt` to include Ruff and remove replaced tools. - Added `Makefile` to streamline development workflows with commands for linting, formatting, type-checking, testing, and cleanup.
This commit is contained in:
@@ -5,9 +5,10 @@ Admin-specific endpoints for managing users and organizations.
|
||||
These endpoints require superuser privileges and provide CMS-like functionality
|
||||
for managing the application.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
@@ -16,27 +17,32 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.dependencies.permissions import require_superuser
|
||||
from app.core.database import get_db
|
||||
from app.core.exceptions import NotFoundError, DuplicateError, AuthorizationError, ErrorCode
|
||||
from app.core.exceptions import (
|
||||
AuthorizationError,
|
||||
DuplicateError,
|
||||
ErrorCode,
|
||||
NotFoundError,
|
||||
)
|
||||
from app.crud.organization import organization as organization_crud
|
||||
from app.crud.user import user as user_crud
|
||||
from app.crud.session import session as session_crud
|
||||
from app.crud.user import user as user_crud
|
||||
from app.models.user import User
|
||||
from app.models.user_organization import OrganizationRole
|
||||
from app.schemas.common import (
|
||||
PaginationParams,
|
||||
PaginatedResponse,
|
||||
MessageResponse,
|
||||
PaginatedResponse,
|
||||
PaginationParams,
|
||||
SortParams,
|
||||
create_pagination_meta
|
||||
create_pagination_meta,
|
||||
)
|
||||
from app.schemas.organizations import (
|
||||
OrganizationResponse,
|
||||
OrganizationCreate,
|
||||
OrganizationMemberResponse,
|
||||
OrganizationResponse,
|
||||
OrganizationUpdate,
|
||||
OrganizationMemberResponse
|
||||
)
|
||||
from app.schemas.users import UserResponse, UserCreate, UserUpdate
|
||||
from app.schemas.sessions import AdminSessionResponse
|
||||
from app.schemas.users import UserCreate, UserResponse, UserUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,6 +52,7 @@ router = APIRouter()
|
||||
# Schemas for bulk operations
|
||||
class BulkAction(str, Enum):
|
||||
"""Supported bulk actions."""
|
||||
|
||||
ACTIVATE = "activate"
|
||||
DEACTIVATE = "deactivate"
|
||||
DELETE = "delete"
|
||||
@@ -53,36 +60,41 @@ class BulkAction(str, Enum):
|
||||
|
||||
class BulkUserAction(BaseModel):
|
||||
"""Schema for bulk user actions."""
|
||||
|
||||
action: BulkAction = Field(..., description="Action to perform on selected users")
|
||||
user_ids: List[UUID] = Field(..., min_items=1, max_items=100, description="List of user IDs (max 100)")
|
||||
user_ids: list[UUID] = Field(
|
||||
..., min_items=1, max_items=100, description="List of user IDs (max 100)"
|
||||
)
|
||||
|
||||
|
||||
class BulkActionResult(BaseModel):
|
||||
"""Result of a bulk action."""
|
||||
|
||||
success: bool
|
||||
affected_count: int
|
||||
failed_count: int
|
||||
message: str
|
||||
failed_ids: Optional[List[UUID]] = []
|
||||
failed_ids: list[UUID] | None = []
|
||||
|
||||
|
||||
# ===== User Management Endpoints =====
|
||||
|
||||
|
||||
@router.get(
|
||||
"/users",
|
||||
response_model=PaginatedResponse[UserResponse],
|
||||
summary="Admin: List All Users",
|
||||
description="Get paginated list of all users with filtering and search (admin only)",
|
||||
operation_id="admin_list_users"
|
||||
operation_id="admin_list_users",
|
||||
)
|
||||
async def admin_list_users(
|
||||
pagination: PaginationParams = Depends(),
|
||||
sort: SortParams = Depends(),
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
is_superuser: Optional[bool] = Query(None, description="Filter by superuser status"),
|
||||
search: Optional[str] = Query(None, description="Search by email, name"),
|
||||
is_active: bool | None = Query(None, description="Filter by active status"),
|
||||
is_superuser: bool | None = Query(None, description="Filter by superuser status"),
|
||||
search: str | None = Query(None, description="Search by email, name"),
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
List all users with comprehensive filtering and search.
|
||||
@@ -105,20 +117,20 @@ async def admin_list_users(
|
||||
sort_by=sort.sort_by or "created_at",
|
||||
sort_order=sort.sort_order.value if sort.sort_order else "desc",
|
||||
filters=filters if filters else None,
|
||||
search=search
|
||||
search=search,
|
||||
)
|
||||
|
||||
pagination_meta = create_pagination_meta(
|
||||
total=total,
|
||||
page=pagination.page,
|
||||
limit=pagination.limit,
|
||||
items_count=len(users)
|
||||
items_count=len(users),
|
||||
)
|
||||
|
||||
return PaginatedResponse(data=users, pagination=pagination_meta)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing users (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error listing users (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -128,12 +140,12 @@ async def admin_list_users(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Admin: Create User",
|
||||
description="Create a new user (admin only)",
|
||||
operation_id="admin_create_user"
|
||||
operation_id="admin_create_user",
|
||||
)
|
||||
async def admin_create_user(
|
||||
user_in: UserCreate,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
Create a new user with admin privileges.
|
||||
@@ -145,13 +157,10 @@ async def admin_create_user(
|
||||
logger.info(f"Admin {admin.email} created user {user.email}")
|
||||
return user
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to create user: {str(e)}")
|
||||
raise NotFoundError(
|
||||
message=str(e),
|
||||
error_code=ErrorCode.USER_ALREADY_EXISTS
|
||||
)
|
||||
logger.warning(f"Failed to create user: {e!s}")
|
||||
raise NotFoundError(message=str(e), error_code=ErrorCode.USER_ALREADY_EXISTS)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating user (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error creating user (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -160,19 +169,18 @@ async def admin_create_user(
|
||||
response_model=UserResponse,
|
||||
summary="Admin: Get User Details",
|
||||
description="Get detailed user information (admin only)",
|
||||
operation_id="admin_get_user"
|
||||
operation_id="admin_get_user",
|
||||
)
|
||||
async def admin_get_user(
|
||||
user_id: UUID,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Get detailed information about a specific user."""
|
||||
user = await user_crud.get(db, id=user_id)
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
message=f"User {user_id} not found",
|
||||
error_code=ErrorCode.USER_NOT_FOUND
|
||||
message=f"User {user_id} not found", error_code=ErrorCode.USER_NOT_FOUND
|
||||
)
|
||||
return user
|
||||
|
||||
@@ -182,21 +190,20 @@ async def admin_get_user(
|
||||
response_model=UserResponse,
|
||||
summary="Admin: Update User",
|
||||
description="Update user information (admin only)",
|
||||
operation_id="admin_update_user"
|
||||
operation_id="admin_update_user",
|
||||
)
|
||||
async def admin_update_user(
|
||||
user_id: UUID,
|
||||
user_in: UserUpdate,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Update user information with admin privileges."""
|
||||
try:
|
||||
user = await user_crud.get(db, id=user_id)
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
message=f"User {user_id} not found",
|
||||
error_code=ErrorCode.USER_NOT_FOUND
|
||||
message=f"User {user_id} not found", error_code=ErrorCode.USER_NOT_FOUND
|
||||
)
|
||||
|
||||
updated_user = await user_crud.update(db, db_obj=user, obj_in=user_in)
|
||||
@@ -206,7 +213,7 @@ async def admin_update_user(
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error updating user (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -215,20 +222,19 @@ async def admin_update_user(
|
||||
response_model=MessageResponse,
|
||||
summary="Admin: Delete User",
|
||||
description="Soft delete a user (admin only)",
|
||||
operation_id="admin_delete_user"
|
||||
operation_id="admin_delete_user",
|
||||
)
|
||||
async def admin_delete_user(
|
||||
user_id: UUID,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Soft delete a user (sets deleted_at timestamp)."""
|
||||
try:
|
||||
user = await user_crud.get(db, id=user_id)
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
message=f"User {user_id} not found",
|
||||
error_code=ErrorCode.USER_NOT_FOUND
|
||||
message=f"User {user_id} not found", error_code=ErrorCode.USER_NOT_FOUND
|
||||
)
|
||||
|
||||
# Prevent deleting yourself
|
||||
@@ -236,21 +242,20 @@ async def admin_delete_user(
|
||||
# Use AuthorizationError for permission/operation restrictions
|
||||
raise AuthorizationError(
|
||||
message="Cannot delete your own account",
|
||||
error_code=ErrorCode.OPERATION_FORBIDDEN
|
||||
error_code=ErrorCode.OPERATION_FORBIDDEN,
|
||||
)
|
||||
|
||||
await user_crud.soft_delete(db, id=user_id)
|
||||
logger.info(f"Admin {admin.email} deleted user {user.email}")
|
||||
|
||||
return MessageResponse(
|
||||
success=True,
|
||||
message=f"User {user.email} has been deleted"
|
||||
success=True, message=f"User {user.email} has been deleted"
|
||||
)
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting user (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error deleting user (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -259,34 +264,32 @@ async def admin_delete_user(
|
||||
response_model=MessageResponse,
|
||||
summary="Admin: Activate User",
|
||||
description="Activate a user account (admin only)",
|
||||
operation_id="admin_activate_user"
|
||||
operation_id="admin_activate_user",
|
||||
)
|
||||
async def admin_activate_user(
|
||||
user_id: UUID,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Activate a user account."""
|
||||
try:
|
||||
user = await user_crud.get(db, id=user_id)
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
message=f"User {user_id} not found",
|
||||
error_code=ErrorCode.USER_NOT_FOUND
|
||||
message=f"User {user_id} not found", error_code=ErrorCode.USER_NOT_FOUND
|
||||
)
|
||||
|
||||
await user_crud.update(db, db_obj=user, obj_in={"is_active": True})
|
||||
logger.info(f"Admin {admin.email} activated user {user.email}")
|
||||
|
||||
return MessageResponse(
|
||||
success=True,
|
||||
message=f"User {user.email} has been activated"
|
||||
success=True, message=f"User {user.email} has been activated"
|
||||
)
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating user (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error activating user (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -295,20 +298,19 @@ async def admin_activate_user(
|
||||
response_model=MessageResponse,
|
||||
summary="Admin: Deactivate User",
|
||||
description="Deactivate a user account (admin only)",
|
||||
operation_id="admin_deactivate_user"
|
||||
operation_id="admin_deactivate_user",
|
||||
)
|
||||
async def admin_deactivate_user(
|
||||
user_id: UUID,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Deactivate a user account."""
|
||||
try:
|
||||
user = await user_crud.get(db, id=user_id)
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
message=f"User {user_id} not found",
|
||||
error_code=ErrorCode.USER_NOT_FOUND
|
||||
message=f"User {user_id} not found", error_code=ErrorCode.USER_NOT_FOUND
|
||||
)
|
||||
|
||||
# Prevent deactivating yourself
|
||||
@@ -316,21 +318,20 @@ async def admin_deactivate_user(
|
||||
# Use AuthorizationError for permission/operation restrictions
|
||||
raise AuthorizationError(
|
||||
message="Cannot deactivate your own account",
|
||||
error_code=ErrorCode.OPERATION_FORBIDDEN
|
||||
error_code=ErrorCode.OPERATION_FORBIDDEN,
|
||||
)
|
||||
|
||||
await user_crud.update(db, db_obj=user, obj_in={"is_active": False})
|
||||
logger.info(f"Admin {admin.email} deactivated user {user.email}")
|
||||
|
||||
return MessageResponse(
|
||||
success=True,
|
||||
message=f"User {user.email} has been deactivated"
|
||||
success=True, message=f"User {user.email} has been deactivated"
|
||||
)
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deactivating user (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error deactivating user (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -339,12 +340,12 @@ async def admin_deactivate_user(
|
||||
response_model=BulkActionResult,
|
||||
summary="Admin: Bulk User Action",
|
||||
description="Perform bulk actions on multiple users (admin only)",
|
||||
operation_id="admin_bulk_user_action"
|
||||
operation_id="admin_bulk_user_action",
|
||||
)
|
||||
async def admin_bulk_user_action(
|
||||
bulk_action: BulkUserAction,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
Perform bulk actions on multiple users using optimized bulk operations.
|
||||
@@ -356,22 +357,16 @@ async def admin_bulk_user_action(
|
||||
# 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
|
||||
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
|
||||
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
|
||||
db, user_ids=bulk_action.user_ids, exclude_user_id=admin.id
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported bulk action: {bulk_action.action}")
|
||||
@@ -390,29 +385,30 @@ async def admin_bulk_user_action(
|
||||
affected_count=affected_count,
|
||||
failed_count=failed_count,
|
||||
message=f"Bulk {bulk_action.action.value}: {affected_count} users affected, {failed_count} skipped",
|
||||
failed_ids=None # Bulk operations don't track individual failures
|
||||
failed_ids=None, # Bulk operations don't track individual failures
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in bulk user action: {str(e)}", exc_info=True)
|
||||
logger.error(f"Error in bulk user action: {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
# ===== Organization Management Endpoints =====
|
||||
|
||||
|
||||
@router.get(
|
||||
"/organizations",
|
||||
response_model=PaginatedResponse[OrganizationResponse],
|
||||
summary="Admin: List Organizations",
|
||||
description="Get paginated list of all organizations (admin only)",
|
||||
operation_id="admin_list_organizations"
|
||||
operation_id="admin_list_organizations",
|
||||
)
|
||||
async def admin_list_organizations(
|
||||
pagination: PaginationParams = Depends(),
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
search: Optional[str] = Query(None, description="Search by name, slug, description"),
|
||||
is_active: bool | None = Query(None, description="Filter by active status"),
|
||||
search: str | None = Query(None, description="Search by name, slug, description"),
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""List all organizations with filtering and search."""
|
||||
try:
|
||||
@@ -422,14 +418,14 @@ async def admin_list_organizations(
|
||||
skip=pagination.offset,
|
||||
limit=pagination.limit,
|
||||
is_active=is_active,
|
||||
search=search
|
||||
search=search,
|
||||
)
|
||||
|
||||
# Build response objects from optimized query results
|
||||
orgs_with_count = []
|
||||
for item in orgs_with_data:
|
||||
org = item['organization']
|
||||
member_count = item['member_count']
|
||||
org = item["organization"]
|
||||
member_count = item["member_count"]
|
||||
|
||||
org_dict = {
|
||||
"id": org.id,
|
||||
@@ -440,7 +436,7 @@ async def admin_list_organizations(
|
||||
"settings": org.settings,
|
||||
"created_at": org.created_at,
|
||||
"updated_at": org.updated_at,
|
||||
"member_count": member_count
|
||||
"member_count": member_count,
|
||||
}
|
||||
orgs_with_count.append(OrganizationResponse(**org_dict))
|
||||
|
||||
@@ -448,13 +444,13 @@ async def admin_list_organizations(
|
||||
total=total,
|
||||
page=pagination.page,
|
||||
limit=pagination.limit,
|
||||
items_count=len(orgs_with_count)
|
||||
items_count=len(orgs_with_count),
|
||||
)
|
||||
|
||||
return PaginatedResponse(data=orgs_with_count, pagination=pagination_meta)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing organizations (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error listing organizations (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -464,12 +460,12 @@ async def admin_list_organizations(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Admin: Create Organization",
|
||||
description="Create a new organization (admin only)",
|
||||
operation_id="admin_create_organization"
|
||||
operation_id="admin_create_organization",
|
||||
)
|
||||
async def admin_create_organization(
|
||||
org_in: OrganizationCreate,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Create a new organization."""
|
||||
try:
|
||||
@@ -486,18 +482,15 @@ async def admin_create_organization(
|
||||
"settings": org.settings,
|
||||
"created_at": org.created_at,
|
||||
"updated_at": org.updated_at,
|
||||
"member_count": 0
|
||||
"member_count": 0,
|
||||
}
|
||||
return OrganizationResponse(**org_dict)
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to create organization: {str(e)}")
|
||||
raise NotFoundError(
|
||||
message=str(e),
|
||||
error_code=ErrorCode.ALREADY_EXISTS
|
||||
)
|
||||
logger.warning(f"Failed to create organization: {e!s}")
|
||||
raise NotFoundError(message=str(e), error_code=ErrorCode.ALREADY_EXISTS)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating organization (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error creating organization (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -506,19 +499,18 @@ async def admin_create_organization(
|
||||
response_model=OrganizationResponse,
|
||||
summary="Admin: Get Organization Details",
|
||||
description="Get detailed organization information (admin only)",
|
||||
operation_id="admin_get_organization"
|
||||
operation_id="admin_get_organization",
|
||||
)
|
||||
async def admin_get_organization(
|
||||
org_id: UUID,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Get detailed information about a specific organization."""
|
||||
org = await organization_crud.get(db, id=org_id)
|
||||
if not org:
|
||||
raise NotFoundError(
|
||||
message=f"Organization {org_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND
|
||||
message=f"Organization {org_id} not found", error_code=ErrorCode.NOT_FOUND
|
||||
)
|
||||
|
||||
org_dict = {
|
||||
@@ -530,7 +522,9 @@ async def admin_get_organization(
|
||||
"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": await organization_crud.get_member_count(
|
||||
db, organization_id=org.id
|
||||
),
|
||||
}
|
||||
return OrganizationResponse(**org_dict)
|
||||
|
||||
@@ -540,13 +534,13 @@ async def admin_get_organization(
|
||||
response_model=OrganizationResponse,
|
||||
summary="Admin: Update Organization",
|
||||
description="Update organization information (admin only)",
|
||||
operation_id="admin_update_organization"
|
||||
operation_id="admin_update_organization",
|
||||
)
|
||||
async def admin_update_organization(
|
||||
org_id: UUID,
|
||||
org_in: OrganizationUpdate,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Update organization information."""
|
||||
try:
|
||||
@@ -554,7 +548,7 @@ async def admin_update_organization(
|
||||
if not org:
|
||||
raise NotFoundError(
|
||||
message=f"Organization {org_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
updated_org = await organization_crud.update(db, db_obj=org, obj_in=org_in)
|
||||
@@ -569,14 +563,16 @@ async def admin_update_organization(
|
||||
"settings": updated_org.settings,
|
||||
"created_at": updated_org.created_at,
|
||||
"updated_at": updated_org.updated_at,
|
||||
"member_count": await organization_crud.get_member_count(db, organization_id=updated_org.id)
|
||||
"member_count": await organization_crud.get_member_count(
|
||||
db, organization_id=updated_org.id
|
||||
),
|
||||
}
|
||||
return OrganizationResponse(**org_dict)
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating organization (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error updating organization (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -585,12 +581,12 @@ async def admin_update_organization(
|
||||
response_model=MessageResponse,
|
||||
summary="Admin: Delete Organization",
|
||||
description="Delete an organization (admin only)",
|
||||
operation_id="admin_delete_organization"
|
||||
operation_id="admin_delete_organization",
|
||||
)
|
||||
async def admin_delete_organization(
|
||||
org_id: UUID,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Delete an organization and all its relationships."""
|
||||
try:
|
||||
@@ -598,21 +594,20 @@ async def admin_delete_organization(
|
||||
if not org:
|
||||
raise NotFoundError(
|
||||
message=f"Organization {org_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
await organization_crud.remove(db, id=org_id)
|
||||
logger.info(f"Admin {admin.email} deleted organization {org.name}")
|
||||
|
||||
return MessageResponse(
|
||||
success=True,
|
||||
message=f"Organization {org.name} has been deleted"
|
||||
success=True, message=f"Organization {org.name} has been deleted"
|
||||
)
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting organization (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error deleting organization (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -621,14 +616,14 @@ async def admin_delete_organization(
|
||||
response_model=PaginatedResponse[OrganizationMemberResponse],
|
||||
summary="Admin: List Organization Members",
|
||||
description="Get all members of an organization (admin only)",
|
||||
operation_id="admin_list_organization_members"
|
||||
operation_id="admin_list_organization_members",
|
||||
)
|
||||
async def admin_list_organization_members(
|
||||
org_id: UUID,
|
||||
pagination: PaginationParams = Depends(),
|
||||
is_active: Optional[bool] = Query(True, description="Filter by active status"),
|
||||
is_active: bool | None = Query(True, description="Filter by active status"),
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""List all members of an organization."""
|
||||
try:
|
||||
@@ -636,7 +631,7 @@ async def admin_list_organization_members(
|
||||
if not org:
|
||||
raise NotFoundError(
|
||||
message=f"Organization {org_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
members, total = await organization_crud.get_organization_members(
|
||||
@@ -644,7 +639,7 @@ async def admin_list_organization_members(
|
||||
organization_id=org_id,
|
||||
skip=pagination.offset,
|
||||
limit=pagination.limit,
|
||||
is_active=is_active
|
||||
is_active=is_active,
|
||||
)
|
||||
|
||||
# Convert to response models
|
||||
@@ -654,7 +649,7 @@ async def admin_list_organization_members(
|
||||
total=total,
|
||||
page=pagination.page,
|
||||
limit=pagination.limit,
|
||||
items_count=len(member_responses)
|
||||
items_count=len(member_responses),
|
||||
)
|
||||
|
||||
return PaginatedResponse(data=member_responses, pagination=pagination_meta)
|
||||
@@ -662,14 +657,19 @@ async def admin_list_organization_members(
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing organization members (admin): {str(e)}", exc_info=True)
|
||||
logger.error(
|
||||
f"Error listing organization members (admin): {e!s}", exc_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
class AddMemberRequest(BaseModel):
|
||||
"""Request to add a member to an organization."""
|
||||
|
||||
user_id: UUID = Field(..., description="User ID to add")
|
||||
role: OrganizationRole = Field(OrganizationRole.MEMBER, description="Role in organization")
|
||||
role: OrganizationRole = Field(
|
||||
OrganizationRole.MEMBER, description="Role in organization"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -677,13 +677,13 @@ class AddMemberRequest(BaseModel):
|
||||
response_model=MessageResponse,
|
||||
summary="Admin: Add Member to Organization",
|
||||
description="Add a user to an organization (admin only)",
|
||||
operation_id="admin_add_organization_member"
|
||||
operation_id="admin_add_organization_member",
|
||||
)
|
||||
async def admin_add_organization_member(
|
||||
org_id: UUID,
|
||||
request: AddMemberRequest,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Add a user to an organization."""
|
||||
try:
|
||||
@@ -691,21 +691,18 @@ async def admin_add_organization_member(
|
||||
if not org:
|
||||
raise NotFoundError(
|
||||
message=f"Organization {org_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
user = await user_crud.get(db, id=request.user_id)
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
message=f"User {request.user_id} not found",
|
||||
error_code=ErrorCode.USER_NOT_FOUND
|
||||
error_code=ErrorCode.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
await organization_crud.add_user(
|
||||
db,
|
||||
organization_id=org_id,
|
||||
user_id=request.user_id,
|
||||
role=request.role
|
||||
db, organization_id=org_id, user_id=request.user_id, role=request.role
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -714,22 +711,21 @@ async def admin_add_organization_member(
|
||||
)
|
||||
|
||||
return MessageResponse(
|
||||
success=True,
|
||||
message=f"User {user.email} added to organization {org.name}"
|
||||
success=True, message=f"User {user.email} added to organization {org.name}"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to add user to organization: {str(e)}")
|
||||
logger.warning(f"Failed to add user to organization: {e!s}")
|
||||
# Use DuplicateError for "already exists" scenarios
|
||||
raise DuplicateError(
|
||||
message=str(e),
|
||||
error_code=ErrorCode.USER_ALREADY_EXISTS,
|
||||
field="user_id"
|
||||
message=str(e), error_code=ErrorCode.USER_ALREADY_EXISTS, field="user_id"
|
||||
)
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding member to organization (admin): {str(e)}", exc_info=True)
|
||||
logger.error(
|
||||
f"Error adding member to organization (admin): {e!s}", exc_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -738,13 +734,13 @@ async def admin_add_organization_member(
|
||||
response_model=MessageResponse,
|
||||
summary="Admin: Remove Member from Organization",
|
||||
description="Remove a user from an organization (admin only)",
|
||||
operation_id="admin_remove_organization_member"
|
||||
operation_id="admin_remove_organization_member",
|
||||
)
|
||||
async def admin_remove_organization_member(
|
||||
org_id: UUID,
|
||||
user_id: UUID,
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""Remove a user from an organization."""
|
||||
try:
|
||||
@@ -752,39 +748,40 @@ async def admin_remove_organization_member(
|
||||
if not org:
|
||||
raise NotFoundError(
|
||||
message=f"Organization {org_id} not found",
|
||||
error_code=ErrorCode.NOT_FOUND
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
user = await user_crud.get(db, id=user_id)
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
message=f"User {user_id} not found",
|
||||
error_code=ErrorCode.USER_NOT_FOUND
|
||||
message=f"User {user_id} not found", error_code=ErrorCode.USER_NOT_FOUND
|
||||
)
|
||||
|
||||
success = await organization_crud.remove_user(
|
||||
db,
|
||||
organization_id=org_id,
|
||||
user_id=user_id
|
||||
db, organization_id=org_id, user_id=user_id
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise NotFoundError(
|
||||
message="User is not a member of this organization",
|
||||
error_code=ErrorCode.NOT_FOUND
|
||||
error_code=ErrorCode.NOT_FOUND,
|
||||
)
|
||||
|
||||
logger.info(f"Admin {admin.email} removed user {user.email} from organization {org.name}")
|
||||
logger.info(
|
||||
f"Admin {admin.email} removed user {user.email} from organization {org.name}"
|
||||
)
|
||||
|
||||
return MessageResponse(
|
||||
success=True,
|
||||
message=f"User {user.email} removed from organization {org.name}"
|
||||
message=f"User {user.email} removed from organization {org.name}",
|
||||
)
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing member from organization (admin): {str(e)}", exc_info=True)
|
||||
logger.error(
|
||||
f"Error removing member from organization (admin): {e!s}", exc_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -792,6 +789,7 @@ async def admin_remove_organization_member(
|
||||
# Session Management Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get(
|
||||
"/sessions",
|
||||
response_model=PaginatedResponse[AdminSessionResponse],
|
||||
@@ -802,13 +800,13 @@ async def admin_remove_organization_member(
|
||||
Returns paginated list of sessions with user information.
|
||||
Useful for admin dashboard statistics and session monitoring.
|
||||
""",
|
||||
operation_id="admin_list_sessions"
|
||||
operation_id="admin_list_sessions",
|
||||
)
|
||||
async def admin_list_sessions(
|
||||
pagination: PaginationParams = Depends(),
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
is_active: bool | None = Query(None, description="Filter by active status"),
|
||||
admin: User = Depends(require_superuser),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any:
|
||||
"""List all sessions across all users with filtering and pagination."""
|
||||
try:
|
||||
@@ -818,7 +816,7 @@ async def admin_list_sessions(
|
||||
skip=pagination.offset,
|
||||
limit=pagination.limit,
|
||||
active_only=is_active if is_active is not None else True,
|
||||
with_user=True
|
||||
with_user=True,
|
||||
)
|
||||
|
||||
# Build response objects with user information
|
||||
@@ -847,21 +845,23 @@ async def admin_list_sessions(
|
||||
last_used_at=session.last_used_at,
|
||||
created_at=session.created_at,
|
||||
expires_at=session.expires_at,
|
||||
is_active=session.is_active
|
||||
is_active=session.is_active,
|
||||
)
|
||||
session_responses.append(session_response)
|
||||
|
||||
logger.info(f"Admin {admin.email} listed {len(session_responses)} sessions (total: {total})")
|
||||
logger.info(
|
||||
f"Admin {admin.email} listed {len(session_responses)} sessions (total: {total})"
|
||||
)
|
||||
|
||||
pagination_meta = create_pagination_meta(
|
||||
total=total,
|
||||
page=pagination.page,
|
||||
limit=pagination.limit,
|
||||
items_count=len(session_responses)
|
||||
items_count=len(session_responses),
|
||||
)
|
||||
|
||||
return PaginatedResponse(data=session_responses, pagination=pagination_meta)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing sessions (admin): {str(e)}", exc_info=True)
|
||||
logger.error(f"Error listing sessions (admin): {e!s}", exc_info=True)
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user