organizations.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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, OrgRole, User, UserOrganization
  8. from app.schemas.requests import (
  9. OrganizationMembershipCreateRequest,
  10. OrganizationMembershipUpdateRequest,
  11. OrganizationRequest,
  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 = (
  57. session.execute(
  58. select(Organization)
  59. .where(Organization.id == str(organization_id))
  60. .options(selectinload(Organization.memberships).selectinload(UserOrganization.user))
  61. )
  62. .scalars()
  63. .first()
  64. )
  65. if org is None:
  66. raise HTTPException(404, "Organization not found")
  67. if current_user.global_role != GlobalRole.SUPER_ADMIN:
  68. is_member = any(m.organization_id == org.id for m in current_user.organizations)
  69. if not is_member:
  70. raise HTTPException(403, "Not a member of this organization")
  71. return org
  72. @router.post("/{organization_id}", response_model=OrganizationResponse)
  73. async def update_organization(
  74. organization_id: UUID,
  75. edit_org: OrganizationRequest,
  76. current_user: User = Depends(deps.require_super_admin),
  77. session: Session = Depends(deps.get_session),
  78. ):
  79. """Rename an organization. super_admin only."""
  80. org = session.get(Organization, str(organization_id))
  81. if org is None:
  82. raise HTTPException(404, "Organization not found")
  83. org.name = edit_org.name
  84. try:
  85. session.commit()
  86. except IntegrityError:
  87. raise HTTPException(400, "Organization name already exists")
  88. session.refresh(org)
  89. return org
  90. @router.delete("/{organization_id}")
  91. async def delete_organization(
  92. organization_id: UUID,
  93. current_user: User = Depends(deps.require_super_admin),
  94. session: Session = Depends(deps.get_session),
  95. ):
  96. """Delete an organization. Cascades to every project (and everything under
  97. those projects) in the org — this is intentionally destructive, confirm
  98. client-side before calling this."""
  99. org = session.get(Organization, str(organization_id))
  100. if org is None:
  101. raise HTTPException(404, "Organization not found")
  102. session.delete(org)
  103. session.commit()
  104. @router.post("/{organization_id}/members", response_model=OrganizationMembershipResponse)
  105. async def add_member(
  106. organization_id: UUID,
  107. payload: OrganizationMembershipCreateRequest,
  108. current_user: User = Depends(deps.require_super_admin),
  109. session: Session = Depends(deps.get_session),
  110. ):
  111. """Add an existing user to an organization with a role. super_admin only.
  112. Does NOT create user accounts — the user must already have signed up.
  113. (User self-registration / invite-by-email is a separate concern for the
  114. users.py router; this endpoint only manages org membership.)"""
  115. org = session.get(Organization, organization_id)
  116. if org is None:
  117. raise HTTPException(404, "Organization not found")
  118. user = session.get(User, payload.user_id)
  119. if user is None:
  120. raise HTTPException(404, "User not found")
  121. existing = session.get(UserOrganization, (payload.user_id, organization_id))
  122. if existing is not None:
  123. raise HTTPException(400, "User is already a member of this organization")
  124. membership = UserOrganization(
  125. user_id=payload.user_id, organization_id=organization_id, role=payload.role
  126. )
  127. session.add(membership)
  128. session.commit()
  129. session.refresh(membership)
  130. return membership
  131. @router.post("/{organization_id}/members/{user_id}", response_model=OrganizationMembershipResponse)
  132. async def update_member_role(
  133. organization_id: UUID,
  134. user_id: UUID,
  135. payload: OrganizationMembershipUpdateRequest,
  136. current_user: User = Depends(deps.require_organization_role(OrgRole.ORG_ADMIN)),
  137. session: Session = Depends(deps.get_session),
  138. ):
  139. """Change a member's role within the organization. super_admin only."""
  140. membership = session.get(UserOrganization, (str(user_id), str(organization_id)))
  141. if membership is None:
  142. raise HTTPException(404, "Membership not found")
  143. membership.role = payload.role
  144. session.commit()
  145. session.refresh(membership)
  146. return membership
  147. @router.delete("/{organization_id}/members/{user_id}")
  148. async def remove_member(
  149. organization_id: UUID,
  150. user_id: UUID,
  151. current_user: User = Depends(deps.require_super_admin),
  152. session: Session = Depends(deps.get_session),
  153. ):
  154. """Remove a user from an organization. super_admin only."""
  155. membership = session.get(UserOrganization, (str(user_id), str(organization_id)))
  156. if membership is None:
  157. raise HTTPException(404, "Membership not found")
  158. session.delete(membership)
  159. session.commit()