From 6d6e2c1e27e6f4b89da4007b852e278c88b2a537 Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Wed, 3 Jun 2026 23:52:12 +0000 Subject: [PATCH] Upload app/api/v1/users.py --- app/api/v1/users.py | 155 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 app/api/v1/users.py diff --git a/app/api/v1/users.py b/app/api/v1/users.py new file mode 100644 index 0000000..1fc3f06 --- /dev/null +++ b/app/api/v1/users.py @@ -0,0 +1,155 @@ +"""User API endpoints: me, list, create, update, soft-delete.""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, Query, Response, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.db import get_db +from app.core.deps import get_current_admin_user, get_current_user +from app.models.user import User, UserRole +from app.schemas.user import ( + UserCreateRequest, + UserListResponse, + UserOut, + UserUpdate, +) +from app.services import user_service + +router = APIRouter(prefix="/users", tags=["users"]) + + +@router.get( + "/me", + response_model=UserOut, + summary="Get the currently authenticated user", +) +async def get_me( + current_user: User = Depends(get_current_user), +) -> UserOut: + """Returns the user from the JWT. Used by the frontend for auth checks and profile display.""" + return UserOut.model_validate(current_user) + + +@router.patch( + "/me", + response_model=UserOut, + summary="Update own profile (name, avatar, notification prefs)", +) +async def update_me( + payload: UserUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> UserOut: + """A user can update their own profile, but not their role.""" + updated = await user_service.update_user_profile( + db, current_user, payload, is_admin=False + ) + return UserOut.model_validate(updated) + + +@router.get( + "/", + response_model=UserListResponse, + summary="List all users in the current org (admin only)", +) +async def list_users( + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=100), + current_user: User = Depends(get_current_admin_user), + db: AsyncSession = Depends(get_db), +) -> UserListResponse: + """Paginated list of active users. Restricted to admin role.""" + skip = (page - 1) * page_size + users = await user_service.list_users( + db, org_id=current_user.org_id, skip=skip, limit=page_size + ) + total = await user_service.count_users_in_org(db, current_user.org_id) + return UserListResponse( + items=[UserOut.model_validate(u) for u in users], + total=total, + page=page, + page_size=page_size, + ) + + +@router.post( + "/", + response_model=UserOut, + status_code=status.HTTP_201_CREATED, + summary="Create a new user in the current org (admin only)", +) +async def create_user( + payload: UserCreateRequest, + current_user: User = Depends(get_current_admin_user), + db: AsyncSession = Depends(get_db), +) -> UserOut: + """Admin creates a new user in the same org.""" + try: + new_user = await user_service.create_user_as_admin( + db, payload, org_id=current_user.org_id + ) + except user_service.EmailAlreadyTaken as e: + raise HTTPException(status_code=409, detail=str(e)) from e + return UserOut.model_validate(new_user) + + +@router.patch( + "/{user_id}", + response_model=UserOut, + summary="Update a user (admin or self for non-role fields)", +) +async def update_user( + user_id: int, + payload: UserUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> UserOut: + """Admin can update any user (including role); non-admin can update only their own profile fields.""" + is_admin = ( + current_user.role == UserRole.admin + if hasattr(current_user.role, "__eq__") and not isinstance(current_user.role, str) + else str(current_user.role) == UserRole.admin.value + ) + if not is_admin and current_user.id != user_id: + raise HTTPException( + status_code=403, + detail="You can only update your own profile", + ) + + target = await user_service.get_user_by_id(db, user_id) + if target is None: + raise HTTPException(status_code=404, detail="User not found") + + if target.org_id != current_user.org_id: + raise HTTPException(status_code=404, detail="User not found") + + updated = await user_service.update_user_profile( + db, target, payload, is_admin=is_admin + ) + return UserOut.model_validate(updated) + + +@router.delete( + "/{user_id}", + status_code=status.HTTP_204_NO_CONTENT, + response_class=Response, + summary="Soft-delete a user (admin only)", +) +async def delete_user( + user_id: int, + current_user: User = Depends(get_current_admin_user), + db: AsyncSession = Depends(get_db), +) -> Response: + """Sets deleted_at timestamp. The record stays in the DB for audit purposes.""" + if current_user.id == user_id: + raise HTTPException( + status_code=400, detail="You cannot delete your own account" + ) + + target = await user_service.get_user_by_id(db, user_id) + if target is None or target.org_id != current_user.org_id: + raise HTTPException(status_code=404, detail="User not found") + + await user_service.soft_delete_user(db, target) + return Response(status_code=status.HTTP_204_NO_CONTENT)