Files
rentman-clone/backend/app/api/v1/users.py
T

167 lines
5.1 KiB
Python
Raw Normal View History

"""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 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