Files
rentman-clone/backend/app/api/v1/users.py
T
Agent Zero 7f7da15965 Initial commit: Rentman Clone - Phase 0-6 (T001-T023)
Completed:
- Phase 0: Project Setup (T001-T003) - Docker Compose, FastAPI skeleton, React SPA
- Phase 1: Auth System (T004-T008) - DB models, JWT auth, RBAC middleware, user management
- Phase 2: Contacts & Tags (T009-T011) - CRUD API + UI
- Phase 3: Equipment Catalog (T012-T014) - Models, API, UI with barcode/QR
- Phase 4: Crew Management (T015-T017) - Models, availability, UI
- Phase 5: Vehicle Fleet (T018-T020) - Models, assignments, UI
- Phase 6: Projects (T021-T023) - Project hierarchy models, CRUD API, list/detail UI
2026-05-31 20:36:42 +00:00

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