162 lines
5.1 KiB
Python
162 lines
5.1 KiB
Python
|
|
"""User management endpoints (admin)."""
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||
|
|
from sqlalchemy import select, func
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.api.deps import get_current_user, require_permission
|
||
|
|
from app.core.security import get_password_hash
|
||
|
|
from app.db.session import get_async_session
|
||
|
|
from app.models import User
|
||
|
|
from app.schemas.user import UserCreateRequest, UserUpdateRequest, UserResponse, UserListResponse
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/users", tags=["users"])
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("", response_model=UserListResponse)
|
||
|
|
async def list_users(
|
||
|
|
page: int = Query(1, ge=1),
|
||
|
|
size: int = Query(20, ge=1, le=100),
|
||
|
|
current_user: User = Depends(require_permission("users:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""List all users within the current account."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
# Count total
|
||
|
|
count_q = select(func.count(User.id)).where(User.account_id == account_id)
|
||
|
|
total = (await session.execute(count_q)).scalar() or 0
|
||
|
|
|
||
|
|
# Fetch page
|
||
|
|
q = (
|
||
|
|
select(User)
|
||
|
|
.where(User.account_id == account_id)
|
||
|
|
.order_by(User.created_at.desc())
|
||
|
|
.offset((page - 1) * size)
|
||
|
|
.limit(size)
|
||
|
|
)
|
||
|
|
result = await session.execute(q)
|
||
|
|
users = result.scalars().all()
|
||
|
|
|
||
|
|
return UserListResponse(
|
||
|
|
items=[UserResponse.model_validate(u) for u in users],
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
size=size,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_user(
|
||
|
|
body: UserCreateRequest,
|
||
|
|
current_user: User = Depends(require_permission("users:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Create a new user within the current account."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
# Check email uniqueness
|
||
|
|
existing = await session.execute(select(User).where(User.email == body.email))
|
||
|
|
if existing.scalars().first():
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_409_CONFLICT,
|
||
|
|
detail="A user with this email already exists",
|
||
|
|
)
|
||
|
|
|
||
|
|
# If role_id is provided, verify it belongs to same account (optional)
|
||
|
|
# (We'll skip role validation for now; can be added later)
|
||
|
|
|
||
|
|
user = User(
|
||
|
|
account_id=account_id,
|
||
|
|
email=body.email,
|
||
|
|
full_name=body.full_name,
|
||
|
|
password_hash=get_password_hash(body.password),
|
||
|
|
role_id=body.role_id,
|
||
|
|
)
|
||
|
|
session.add(user)
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(user)
|
||
|
|
|
||
|
|
return UserResponse.model_validate(user)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{user_id}", response_model=UserResponse)
|
||
|
|
async def get_user(
|
||
|
|
user_id: str,
|
||
|
|
current_user: User = Depends(require_permission("users:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Get a specific user by ID."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(User).where(User.id == user_id, User.account_id == account_id)
|
||
|
|
)
|
||
|
|
user = result.scalars().first()
|
||
|
|
if not user:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
||
|
|
detail="User not found",
|
||
|
|
)
|
||
|
|
return UserResponse.model_validate(user)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{user_id}", response_model=UserResponse)
|
||
|
|
async def update_user(
|
||
|
|
user_id: str,
|
||
|
|
body: UserUpdateRequest,
|
||
|
|
current_user: User = Depends(require_permission("users:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Update a user's role, active status, or name."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(User).where(User.id == user_id, User.account_id == account_id)
|
||
|
|
)
|
||
|
|
user = result.scalars().first()
|
||
|
|
if not user:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
||
|
|
detail="User not found",
|
||
|
|
)
|
||
|
|
|
||
|
|
if body.full_name is not None:
|
||
|
|
user.full_name = body.full_name
|
||
|
|
if body.role_id is not None:
|
||
|
|
user.role_id = body.role_id
|
||
|
|
if body.is_active is not None:
|
||
|
|
user.is_active = body.is_active
|
||
|
|
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(user)
|
||
|
|
return UserResponse.model_validate(user)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
|
|
async def delete_user(
|
||
|
|
user_id: str,
|
||
|
|
current_user: User = Depends(require_permission("users:delete")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Soft-delete (deactivate) a user. Does not actually delete."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(User).where(User.id == user_id, User.account_id == account_id)
|
||
|
|
)
|
||
|
|
user = result.scalars().first()
|
||
|
|
if not user:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
||
|
|
detail="User not found",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Prevent self-deactivation
|
||
|
|
if user.id == current_user.id:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||
|
|
detail="You cannot deactivate your own account",
|
||
|
|
)
|
||
|
|
|
||
|
|
user.is_active = False
|
||
|
|
await session.commit()
|
||
|
|
return None
|