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, OrgRole, User, UserOrganization from app.schemas.requests import ( OrganizationMembershipCreateRequest, OrganizationMembershipUpdateRequest, OrganizationRequest, ) 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()