import uuid from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.db import get_db from src.models.user import User from src.schemas.auth import CurrentUser from src.schemas.user import UserCreate, UserResponse, UserUpdate from src.services.auth import hash_password from src.services.dependencies import require_role router = APIRouter(prefix="/users", tags=["users"]) @router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED) async def create_user( body: UserCreate, current_user: CurrentUser = Depends(require_role("owner", "admin")), db: AsyncSession = Depends(get_db), ) -> User: """Invite/create a new user within the current organisation.""" # Check email uniqueness existing = await db.execute(select(User).where(User.email == body.email)) if existing.scalar_one_or_none() is not None: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"User with email '{body.email}' already exists", ) user = User( organisation_id=current_user.organisation_id, email=body.email, password_hash=hash_password(body.password), full_name=body.full_name, role=body.role, ) db.add(user) await db.flush() await db.refresh(user) return user @router.get("/", response_model=list[UserResponse]) async def list_users( current_user: CurrentUser = Depends(require_role("owner", "admin", "editor", "viewer")), db: AsyncSession = Depends(get_db), ) -> list[User]: """List all active users in the current organisation.""" result = await db.execute( select(User) .where( User.organisation_id == current_user.organisation_id, User.deleted_at.is_(None), ) .order_by(User.created_at) ) return list(result.scalars().all()) @router.get("/{user_id}", response_model=UserResponse) async def get_user( user_id: uuid.UUID, current_user: CurrentUser = Depends(require_role("owner", "admin", "editor", "viewer")), db: AsyncSession = Depends(get_db), ) -> User: """Get a specific user by ID within the current organisation.""" result = await db.execute( select(User).where( User.id == user_id, User.organisation_id == current_user.organisation_id, User.deleted_at.is_(None), ) ) user = result.scalar_one_or_none() if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") return user @router.patch("/{user_id}", response_model=UserResponse) async def update_user( user_id: uuid.UUID, body: UserUpdate, current_user: CurrentUser = Depends(require_role("owner", "admin")), db: AsyncSession = Depends(get_db), ) -> User: """Update a user's name or role. Requires owner or admin.""" result = await db.execute( select(User).where( User.id == user_id, User.organisation_id == current_user.organisation_id, User.deleted_at.is_(None), ) ) user = result.scalar_one_or_none() if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(user, field, value) await db.flush() await db.refresh(user) return user @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def deactivate_user( user_id: uuid.UUID, current_user: CurrentUser = Depends(require_role("owner", "admin")), db: AsyncSession = Depends(get_db), ) -> None: """Soft-delete (deactivate) a user. Requires owner or admin.""" if user_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot deactivate yourself", ) result = await db.execute( select(User).where( User.id == user_id, User.organisation_id == current_user.organisation_id, User.deleted_at.is_(None), ) ) user = result.scalar_one_or_none() if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") user.deleted_at = datetime.now(UTC) await db.flush()