organizations.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from uuid import UUID
  2. from fastapi import APIRouter, Depends, HTTPException
  3. from sqlalchemy import select
  4. from sqlalchemy.exc import IntegrityError
  5. from sqlalchemy.orm import Session, selectinload
  6. from app.api import deps
  7. from app.models import GlobalRole, Organization, User, UserOrganization, OrgRole
  8. from app.schemas.requests import (
  9. OrganizationRequest,
  10. OrganizationMembershipCreateRequest,
  11. OrganizationMembershipUpdateRequest,
  12. )
  13. from app.schemas.responses import (
  14. OrganizationDetailResponse,
  15. OrganizationMembershipResponse,
  16. OrganizationResponse,
  17. )
  18. router = APIRouter()
  19. @router.get("/", response_model=list[OrganizationResponse])
  20. async def list_organizations(
  21. current_user: User = Depends(deps.get_current_user),
  22. session: Session = Depends(deps.get_session),
  23. ):
  24. """List organizations. super_admin sees all; others see only orgs they belong to."""
  25. if current_user.global_role == GlobalRole.SUPER_ADMIN:
  26. results = session.execute(select(Organization))
  27. else:
  28. org_ids = [m.organization_id for m in current_user.organizations]
  29. if not org_ids:
  30. return []
  31. results = session.execute(select(Organization).where(Organization.id.in_(org_ids)))
  32. return results.scalars().all()
  33. @router.post("/", response_model=OrganizationResponse)
  34. async def create_organization(
  35. new_org: OrganizationRequest,
  36. current_user: User = Depends(deps.require_super_admin),
  37. session: Session = Depends(deps.get_session),
  38. ):
  39. """Create a new organization. super_admin only."""
  40. org = Organization(**new_org.model_dump())
  41. session.add(org)
  42. try:
  43. session.commit()
  44. except IntegrityError:
  45. raise HTTPException(400, "Organization name already exists")
  46. session.refresh(org)
  47. return org
  48. @router.get("/{organization_id}", response_model=OrganizationDetailResponse)
  49. async def get_organization(
  50. organization_id: UUID,
  51. current_user: User = Depends(deps.get_current_user),
  52. session: Session = Depends(deps.get_session),
  53. ):
  54. """Get organization detail including members.
  55. super_admin, or any member of the organization, may view it."""
  56. org = session.execute(
  57. select(Organization)
  58. .where(Organization.id == str(organization_id))
  59. .options(selectinload(Organization.memberships).selectinload(UserOrganization.user))
  60. ).scalars().first()
  61. if org is None:
  62. raise HTTPException(404, "Organization not found")
  63. if current_user.global_role != GlobalRole.SUPER_ADMIN:
  64. is_member = any(m.organization_id == org.id for m in current_user.organizations)
  65. if not is_member:
  66. raise HTTPException(403, "Not a member of this organization")
  67. return org
  68. @router.patch("/{organization_id}", response_model=OrganizationResponse)
  69. async def update_organization(
  70. organization_id: UUID,
  71. edit_org: OrganizationRequest,
  72. current_user: User = Depends(deps.require_super_admin),
  73. session: Session = Depends(deps.get_session),
  74. ):
  75. """Rename an organization. super_admin only."""
  76. org = session.get(Organization, str(organization_id))
  77. if org is None:
  78. raise HTTPException(404, "Organization not found")
  79. org.name = edit_org.name
  80. try:
  81. session.commit()
  82. except IntegrityError:
  83. raise HTTPException(400, "Organization name already exists")
  84. session.refresh(org)
  85. return org
  86. @router.delete("/{organization_id}")
  87. async def delete_organization(
  88. organization_id: UUID,
  89. current_user: User = Depends(deps.require_super_admin),
  90. session: Session = Depends(deps.get_session),
  91. ):
  92. """Delete an organization. Cascades to every project (and everything under
  93. those projects) in the org — this is intentionally destructive, confirm
  94. client-side before calling this."""
  95. org = session.get(Organization, str(organization_id))
  96. if org is None:
  97. raise HTTPException(404, "Organization not found")
  98. session.delete(org)
  99. session.commit()
  100. # ---- Membership management ----
  101. @router.post("/{organization_id}/members", response_model=OrganizationMembershipResponse)
  102. async def add_member(
  103. organization_id: UUID,
  104. payload: OrganizationMembershipCreateRequest,
  105. current_user: User = Depends(deps.require_super_admin),
  106. session: Session = Depends(deps.get_session),
  107. ):
  108. """Add an existing user to an organization with a role. super_admin only.
  109. Does NOT create user accounts — the user must already have signed up.
  110. (User self-registration / invite-by-email is a separate concern for the
  111. users.py router; this endpoint only manages org membership.)"""
  112. org = session.get(Organization, organization_id)
  113. if org is None:
  114. raise HTTPException(404, "Organization not found")
  115. user = session.get(User, payload.user_id)
  116. if user is None:
  117. raise HTTPException(404, "User not found")
  118. existing = session.get(UserOrganization, (payload.user_id, organization_id))
  119. if existing is not None:
  120. raise HTTPException(400, "User is already a member of this organization")
  121. membership = UserOrganization(user_id=payload.user_id, organization_id=organization_id, role=payload.role)
  122. session.add(membership)
  123. session.commit()
  124. session.refresh(membership)
  125. return membership
  126. @router.patch(
  127. "/{organization_id}/members/{user_id}", response_model=OrganizationMembershipResponse
  128. )
  129. async def update_member_role(
  130. organization_id: UUID,
  131. user_id: UUID,
  132. payload: OrganizationMembershipUpdateRequest,
  133. current_user: User = Depends(deps.require_organization_role(OrgRole.ORG_ADMIN)),
  134. session: Session = Depends(deps.get_session),
  135. ):
  136. """Change a member's role within the organization. super_admin only."""
  137. membership = session.get(UserOrganization, (str(user_id), str(organization_id)))
  138. if membership is None:
  139. raise HTTPException(404, "Membership not found")
  140. membership.role = payload.role
  141. session.commit()
  142. session.refresh(membership)
  143. return membership
  144. @router.delete("/{organization_id}/members/{user_id}")
  145. async def remove_member(
  146. organization_id: UUID,
  147. user_id: UUID,
  148. current_user: User = Depends(deps.require_super_admin),
  149. session: Session = Depends(deps.get_session),
  150. ):
  151. """Remove a user from an organization. super_admin only."""
  152. membership = session.get(UserOrganization, (str(user_id), str(organization_id)))
  153. if membership is None:
  154. raise HTTPException(404, "Membership not found")
  155. session.delete(membership)
  156. session.commit()