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