| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- from uuid import UUID
- from fastapi import APIRouter, Depends, HTTPException
- from sqlalchemy import select
- from sqlalchemy.exc import IntegrityError
- from sqlalchemy.orm import Session, selectinload
- from app.api import deps
- from app.models import GlobalRole, Organization, User, UserOrganization, OrgRole
- from app.schemas.requests import (
- OrganizationRequest,
- OrganizationMembershipCreateRequest,
- OrganizationMembershipUpdateRequest,
- )
- from app.schemas.responses import (
- OrganizationDetailResponse,
- OrganizationMembershipResponse,
- OrganizationResponse,
- )
- router = APIRouter()
- @router.get("/", response_model=list[OrganizationResponse])
- async def list_organizations(
- current_user: User = Depends(deps.get_current_user),
- session: Session = Depends(deps.get_session),
- ):
- """List organizations. super_admin sees all; others see only orgs they belong to."""
- if current_user.global_role == GlobalRole.SUPER_ADMIN:
- results = session.execute(select(Organization))
- else:
- org_ids = [m.organization_id for m in current_user.organizations]
- if not org_ids:
- return []
- results = session.execute(select(Organization).where(Organization.id.in_(org_ids)))
- return results.scalars().all()
- @router.post("/", response_model=OrganizationResponse)
- async def create_organization(
- new_org: OrganizationRequest,
- current_user: User = Depends(deps.require_super_admin),
- session: Session = Depends(deps.get_session),
- ):
- """Create a new organization. super_admin only."""
- org = Organization(**new_org.model_dump())
- session.add(org)
- try:
- session.commit()
- except IntegrityError:
- raise HTTPException(400, "Organization name already exists")
- session.refresh(org)
- return org
- @router.get("/{organization_id}", response_model=OrganizationDetailResponse)
- async def get_organization(
- organization_id: UUID,
- current_user: User = Depends(deps.get_current_user),
- session: Session = Depends(deps.get_session),
- ):
- """Get organization detail including members.
- super_admin, or any member of the organization, may view it."""
- org = session.execute(
- select(Organization)
- .where(Organization.id == str(organization_id))
- .options(selectinload(Organization.memberships).selectinload(UserOrganization.user))
- ).scalars().first()
- if org is None:
- raise HTTPException(404, "Organization not found")
- if current_user.global_role != GlobalRole.SUPER_ADMIN:
- is_member = any(m.organization_id == org.id for m in current_user.organizations)
- if not is_member:
- raise HTTPException(403, "Not a member of this organization")
- return org
- @router.patch("/{organization_id}", response_model=OrganizationResponse)
- async def update_organization(
- organization_id: UUID,
- edit_org: OrganizationRequest,
- current_user: User = Depends(deps.require_super_admin),
- session: Session = Depends(deps.get_session),
- ):
- """Rename an organization. super_admin only."""
- org = session.get(Organization, str(organization_id))
- if org is None:
- raise HTTPException(404, "Organization not found")
- org.name = edit_org.name
- try:
- session.commit()
- except IntegrityError:
- raise HTTPException(400, "Organization name already exists")
- session.refresh(org)
- return org
- @router.delete("/{organization_id}")
- async def delete_organization(
- organization_id: UUID,
- current_user: User = Depends(deps.require_super_admin),
- session: Session = Depends(deps.get_session),
- ):
- """Delete an organization. Cascades to every project (and everything under
- those projects) in the org — this is intentionally destructive, confirm
- client-side before calling this."""
- org = session.get(Organization, str(organization_id))
- if org is None:
- raise HTTPException(404, "Organization not found")
- session.delete(org)
- session.commit()
- # ---- Membership management ----
- @router.post("/{organization_id}/members", response_model=OrganizationMembershipResponse)
- async def add_member(
- organization_id: UUID,
- payload: OrganizationMembershipCreateRequest,
- current_user: User = Depends(deps.require_super_admin),
- session: Session = Depends(deps.get_session),
- ):
- """Add an existing user to an organization with a role. super_admin only.
- Does NOT create user accounts — the user must already have signed up.
- (User self-registration / invite-by-email is a separate concern for the
- users.py router; this endpoint only manages org membership.)"""
- org = session.get(Organization, organization_id)
- if org is None:
- raise HTTPException(404, "Organization not found")
- user = session.get(User, payload.user_id)
- if user is None:
- raise HTTPException(404, "User not found")
- existing = session.get(UserOrganization, (payload.user_id, organization_id))
- if existing is not None:
- raise HTTPException(400, "User is already a member of this organization")
- membership = UserOrganization(user_id=payload.user_id, organization_id=organization_id, role=payload.role)
- session.add(membership)
- session.commit()
- session.refresh(membership)
- return membership
- @router.patch(
- "/{organization_id}/members/{user_id}", response_model=OrganizationMembershipResponse
- )
- async def update_member_role(
- organization_id: UUID,
- user_id: UUID,
- payload: OrganizationMembershipUpdateRequest,
- current_user: User = Depends(deps.require_organization_role(OrgRole.ORG_ADMIN)),
- session: Session = Depends(deps.get_session),
- ):
- """Change a member's role within the organization. super_admin only."""
- membership = session.get(UserOrganization, (str(user_id), str(organization_id)))
- if membership is None:
- raise HTTPException(404, "Membership not found")
- membership.role = payload.role
- session.commit()
- session.refresh(membership)
- return membership
- @router.delete("/{organization_id}/members/{user_id}")
- async def remove_member(
- organization_id: UUID,
- user_id: UUID,
- current_user: User = Depends(deps.require_super_admin),
- session: Session = Depends(deps.get_session),
- ):
- """Remove a user from an organization. super_admin only."""
- membership = session.get(UserOrganization, (str(user_id), str(organization_id)))
- if membership is None:
- raise HTTPException(404, "Membership not found")
- session.delete(membership)
- session.commit()
|