feat: Implement admin user management endpoints and user update schema
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.admin import extractions, sounds
|
||||
from app.api.v1.admin import extractions, sounds, users
|
||||
|
||||
router = APIRouter(prefix="/admin")
|
||||
|
||||
# Include all admin sub-routers
|
||||
router.include_router(extractions.router)
|
||||
router.include_router(sounds.router)
|
||||
router.include_router(users.router)
|
||||
|
||||
148
app/api/v1/admin/users.py
Normal file
148
app/api/v1/admin/users.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Admin users endpoints."""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_admin_user
|
||||
from app.models.plan import Plan
|
||||
from app.models.user import User
|
||||
from app.repositories.plan import PlanRepository
|
||||
from app.repositories.user import UserRepository
|
||||
from app.schemas.auth import UserResponse
|
||||
from app.schemas.user import UserUpdate
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/users",
|
||||
tags=["admin-users"],
|
||||
dependencies=[Depends(get_admin_user)],
|
||||
)
|
||||
|
||||
|
||||
def _user_to_response(user: User) -> UserResponse:
|
||||
"""Convert User model to UserResponse."""
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
name=user.name,
|
||||
picture=user.picture,
|
||||
role=user.role,
|
||||
credits=user.credits,
|
||||
is_active=user.is_active,
|
||||
plan={
|
||||
"id": user.plan.id,
|
||||
"name": user.plan.name,
|
||||
"max_credits": user.plan.max_credits,
|
||||
"features": [], # Add features if needed
|
||||
} if user.plan else {},
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_users(
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[UserResponse]:
|
||||
"""Get all users (admin only)."""
|
||||
user_repo = UserRepository(session)
|
||||
users = await user_repo.get_all_with_plan(limit=limit, offset=offset)
|
||||
return [_user_to_response(user) for user in users]
|
||||
|
||||
|
||||
@router.get("/{user_id}")
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> UserResponse:
|
||||
"""Get a specific user by ID (admin only)."""
|
||||
user_repo = UserRepository(session)
|
||||
user = await user_repo.get_by_id_with_plan(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
return _user_to_response(user)
|
||||
|
||||
|
||||
@router.patch("/{user_id}")
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_update: UserUpdate,
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> UserResponse:
|
||||
"""Update a user (admin only)."""
|
||||
user_repo = UserRepository(session)
|
||||
user = await user_repo.get_by_id_with_plan(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
update_data = user_update.model_dump(exclude_unset=True)
|
||||
|
||||
# If plan_id is being updated, validate it exists
|
||||
if "plan_id" in update_data:
|
||||
plan_repo = PlanRepository(session)
|
||||
plan = await plan_repo.get_by_id(update_data["plan_id"])
|
||||
if not plan:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Plan not found",
|
||||
)
|
||||
|
||||
updated_user = await user_repo.update(user, update_data)
|
||||
# Need to refresh the plan relationship after update
|
||||
await session.refresh(updated_user, ["plan"])
|
||||
return _user_to_response(updated_user)
|
||||
|
||||
|
||||
@router.post("/{user_id}/disable")
|
||||
async def disable_user(
|
||||
user_id: int,
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> dict[str, str]:
|
||||
"""Disable a user (admin only)."""
|
||||
user_repo = UserRepository(session)
|
||||
user = await user_repo.get_by_id_with_plan(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
await user_repo.update(user, {"is_active": False})
|
||||
return {"message": "User disabled successfully"}
|
||||
|
||||
|
||||
@router.post("/{user_id}/enable")
|
||||
async def enable_user(
|
||||
user_id: int,
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> dict[str, str]:
|
||||
"""Enable a user (admin only)."""
|
||||
user_repo = UserRepository(session)
|
||||
user = await user_repo.get_by_id_with_plan(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
await user_repo.update(user, {"is_active": True})
|
||||
return {"message": "User enabled successfully"}
|
||||
|
||||
|
||||
@router.get("/plans/list")
|
||||
async def list_plans(
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> list[Plan]:
|
||||
"""Get all plans for user editing (admin only)."""
|
||||
plan_repo = PlanRepository(session)
|
||||
return await plan_repo.get_all()
|
||||
@@ -56,7 +56,7 @@ def create_app() -> FastAPI:
|
||||
# Configure docs URLs for reverse proxy setup
|
||||
docs_url="/api/docs", # Swagger UI at /api/docs
|
||||
redoc_url="/api/redoc", # ReDoc at /api/redoc
|
||||
openapi_url="/api/openapi.json" # OpenAPI schema at /api/openapi.json
|
||||
openapi_url="/api/openapi.json", # OpenAPI schema at /api/openapi.json
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
|
||||
17
app/repositories/plan.py
Normal file
17
app/repositories/plan.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Plan repository."""
|
||||
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.models.plan import Plan
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PlanRepository(BaseRepository[Plan]):
|
||||
"""Repository for plan operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
"""Initialize the plan repository."""
|
||||
super().__init__(Plan, session)
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.models.plan import Plan
|
||||
@@ -20,6 +21,42 @@ class UserRepository(BaseRepository[User]):
|
||||
"""Initialize the user repository."""
|
||||
super().__init__(User, session)
|
||||
|
||||
async def get_all_with_plan(
|
||||
self,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[User]:
|
||||
"""Get all users with plan relationship loaded."""
|
||||
try:
|
||||
statement = (
|
||||
select(User)
|
||||
.options(selectinload(User.plan))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
result = await self.session.exec(statement)
|
||||
return list(result.all())
|
||||
except Exception:
|
||||
logger.exception("Failed to get all users with plan")
|
||||
raise
|
||||
|
||||
async def get_by_id_with_plan(self, entity_id: int) -> User | None:
|
||||
"""Get a user by ID with plan relationship loaded."""
|
||||
try:
|
||||
statement = (
|
||||
select(User)
|
||||
.options(selectinload(User.plan))
|
||||
.where(User.id == entity_id)
|
||||
)
|
||||
result = await self.session.exec(statement)
|
||||
return result.first()
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to get user by ID with plan: %s",
|
||||
entity_id,
|
||||
)
|
||||
raise
|
||||
|
||||
async def get_by_email(self, email: str) -> User | None:
|
||||
"""Get a user by email address."""
|
||||
try:
|
||||
|
||||
14
app/schemas/user.py
Normal file
14
app/schemas/user.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""User schemas."""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""Schema for updating a user."""
|
||||
|
||||
name: str | None = Field(
|
||||
None, min_length=1, max_length=100, description="User full name",
|
||||
)
|
||||
plan_id: int | None = Field(None, description="User plan ID")
|
||||
credits: int | None = Field(None, ge=0, description="User credits")
|
||||
is_active: bool | None = Field(None, description="Whether user is active")
|
||||
Reference in New Issue
Block a user