users.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. """
  2. Client endpoints for managing users within their organization.
  3. """
  4. from typing import Annotated
  5. from fastapi import APIRouter, Depends, HTTPException, Query, status
  6. from pydantic import BaseModel
  7. from sqlalchemy.ext.asyncio import AsyncSession
  8. from app.api.deps import get_current_owner, get_current_user
  9. from app.core.database import get_db
  10. from app.models.user import User
  11. from app.schemas.user import UserCreate, UserListResponse, UserResponse, UserUpdate
  12. from app.services import user_service
  13. router = APIRouter()
  14. class ChangePasswordRequest(BaseModel):
  15. """Request schema for changing user password."""
  16. new_password: str
  17. @router.get("", response_model=UserListResponse)
  18. async def list_organization_users(
  19. db: Annotated[AsyncSession, Depends(get_db)],
  20. current_user: Annotated[User, Depends(get_current_user)],
  21. skip: int = Query(0, ge=0, description="Number of records to skip"),
  22. limit: int = Query(100, ge=1, le=1000, description="Max records to return"),
  23. role: str | None = Query(None, description="Filter by role"),
  24. status: str | None = Query(None, description="Filter by status"),
  25. ):
  26. """
  27. List users in current user's organization.
  28. All authenticated users can view users in their organization.
  29. """
  30. if not current_user.organization_id:
  31. raise HTTPException(
  32. status_code=status.HTTP_400_BAD_REQUEST,
  33. detail="User is not assigned to any organization",
  34. )
  35. users, total = await user_service.list_users(
  36. db,
  37. skip=skip,
  38. limit=limit,
  39. organization_id=current_user.organization_id,
  40. role=role,
  41. status=status,
  42. )
  43. return UserListResponse(
  44. users=users,
  45. total=total,
  46. )
  47. @router.get("/{user_id}", response_model=UserResponse)
  48. async def get_organization_user(
  49. user_id: int,
  50. db: Annotated[AsyncSession, Depends(get_db)],
  51. current_user: Annotated[User, Depends(get_current_user)],
  52. ):
  53. """
  54. Get user details from current organization.
  55. Users can view other users in their organization.
  56. """
  57. user = await user_service.get_user(db, user_id)
  58. if not user:
  59. raise HTTPException(
  60. status_code=status.HTTP_404_NOT_FOUND,
  61. detail="User not found",
  62. )
  63. # Check if user belongs to same organization
  64. if user.organization_id != current_user.organization_id:
  65. raise HTTPException(
  66. status_code=status.HTTP_403_FORBIDDEN,
  67. detail="Cannot view users from other organizations",
  68. )
  69. return user
  70. @router.post(
  71. "", response_model=UserResponse, status_code=status.HTTP_201_CREATED
  72. )
  73. async def create_organization_user(
  74. data: UserCreate,
  75. db: Annotated[AsyncSession, Depends(get_db)],
  76. current_user: Annotated[User, Depends(get_current_owner)],
  77. ):
  78. """
  79. Create a new user in current organization (owner/admin only).
  80. Only owners and admins can create users.
  81. User will be created in the same organization as the current user.
  82. """
  83. if not current_user.organization_id:
  84. raise HTTPException(
  85. status_code=status.HTTP_400_BAD_REQUEST,
  86. detail="User is not assigned to any organization",
  87. )
  88. # Override organization_id to current user's organization
  89. data.organization_id = current_user.organization_id
  90. # Check if email already exists
  91. existing_user = await user_service.get_user_by_email(db, data.email)
  92. if existing_user:
  93. raise HTTPException(
  94. status_code=status.HTTP_400_BAD_REQUEST,
  95. detail="Email already registered",
  96. )
  97. user = await user_service.create_user(db, data)
  98. return user
  99. @router.patch("/{user_id}", response_model=UserResponse)
  100. async def update_organization_user(
  101. user_id: int,
  102. data: UserUpdate,
  103. db: Annotated[AsyncSession, Depends(get_db)],
  104. current_user: Annotated[User, Depends(get_current_owner)],
  105. ):
  106. """
  107. Update user in current organization (owner/admin only).
  108. Only owners and admins can update users.
  109. Cannot update users from other organizations.
  110. """
  111. user = await user_service.get_user(db, user_id)
  112. if not user:
  113. raise HTTPException(
  114. status_code=status.HTTP_404_NOT_FOUND,
  115. detail="User not found",
  116. )
  117. # Check if user belongs to same organization
  118. if user.organization_id != current_user.organization_id:
  119. raise HTTPException(
  120. status_code=status.HTTP_403_FORBIDDEN,
  121. detail="Cannot update users from other organizations",
  122. )
  123. # Cannot update yourself via this endpoint
  124. if user_id == current_user.id:
  125. raise HTTPException(
  126. status_code=status.HTTP_400_BAD_REQUEST,
  127. detail="Cannot update yourself, use profile endpoint",
  128. )
  129. updated_user = await user_service.update_user(db, user_id, data)
  130. return updated_user
  131. @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
  132. async def delete_organization_user(
  133. user_id: int,
  134. db: Annotated[AsyncSession, Depends(get_db)],
  135. current_user: Annotated[User, Depends(get_current_owner)],
  136. ):
  137. """
  138. Delete user from current organization (owner/admin only).
  139. Only owners and admins can delete users.
  140. Cannot delete users from other organizations.
  141. Cannot delete yourself.
  142. """
  143. user = await user_service.get_user(db, user_id)
  144. if not user:
  145. raise HTTPException(
  146. status_code=status.HTTP_404_NOT_FOUND,
  147. detail="User not found",
  148. )
  149. # Check if user belongs to same organization
  150. if user.organization_id != current_user.organization_id:
  151. raise HTTPException(
  152. status_code=status.HTTP_403_FORBIDDEN,
  153. detail="Cannot delete users from other organizations",
  154. )
  155. # Cannot delete yourself
  156. if user_id == current_user.id:
  157. raise HTTPException(
  158. status_code=status.HTTP_400_BAD_REQUEST,
  159. detail="Cannot delete yourself",
  160. )
  161. await user_service.delete_user(db, user_id)
  162. @router.post("/{user_id}/change-password", response_model=UserResponse)
  163. async def change_organization_user_password(
  164. user_id: int,
  165. data: ChangePasswordRequest,
  166. db: Annotated[AsyncSession, Depends(get_db)],
  167. current_user: Annotated[User, Depends(get_current_owner)],
  168. ):
  169. """
  170. Change password for user in current organization (owner/admin only).
  171. Only owners and admins can change passwords for other users.
  172. """
  173. user = await user_service.get_user(db, user_id)
  174. if not user:
  175. raise HTTPException(
  176. status_code=status.HTTP_404_NOT_FOUND,
  177. detail="User not found",
  178. )
  179. # Check if user belongs to same organization
  180. if user.organization_id != current_user.organization_id:
  181. raise HTTPException(
  182. status_code=status.HTTP_403_FORBIDDEN,
  183. detail="Cannot change password for users from other organizations",
  184. )
  185. updated_user = await user_service.change_user_password(
  186. db, user_id, data.new_password
  187. )
  188. return updated_user