| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- import secrets
- from uuid import UUID
- from fastapi import APIRouter, Depends, HTTPException
- from sqlalchemy import select
- from sqlalchemy.orm import Session, selectinload
- from app.api import deps
- from app.api.utils import assert_project_exists_or_404, issue_reset_token
- from app.core.config import settings
- from app.core.email import EmailSender, get_email_sender
- from app.core.security import get_password_hash
- from app.models import Commission, OrgRole, Project, User, UserOrganization
- from app.schemas.requests import (
- CommissionCreateRequest,
- CommissionMembershipRequest,
- CommissionUpdateRequest,
- UserInviteRequest,
- )
- from app.schemas.responses import CommissionResponse
- router = APIRouter(prefix="/project/{project_id}", tags=["commissions"])
- READ_COMMISSIONS = (OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE, OrgRole.RESPO_COMMISSION)
- MANAGE_COMMISSIONS = (OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE)
- def _get_commission_or_404(session: Session, project_id: UUID, commission_id: UUID) -> Commission:
- commission = session.scalar(
- select(Commission)
- .where(Commission.id == commission_id)
- .options(selectinload(Commission.members))
- )
- if commission is None or str(commission.project_id) != str(project_id):
- raise HTTPException(status_code=404, detail="Commission not found")
- return commission
- @router.get("/commissions", response_model=list[CommissionResponse])
- async def list_project_commissions(
- project_id: UUID,
- current_user: User = Depends(deps.require_org_role(*READ_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- ):
- """List all commissions for a specific project"""
- assert_project_exists_or_404(session, project_id)
- results = session.execute(select(Commission).where(Commission.project_id == project_id))
- return results.scalars().all()
- @router.post("/commission", response_model=CommissionResponse)
- async def create_commission(
- project_id: UUID,
- new_commission: CommissionCreateRequest,
- current_user: User = Depends(deps.require_org_role(*MANAGE_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- ):
- """Create a new commission in the project"""
- assert_project_exists_or_404(session, project_id)
- commission = Commission(project_id=project_id, **new_commission.model_dump())
- session.add(commission)
- session.commit()
- session.refresh(commission)
- return commission
- @router.get("/commission/{commission_id}", response_model=CommissionResponse)
- async def get_commission(
- project_id: UUID,
- commission_id: UUID,
- current_user: User = Depends(deps.require_org_role(*READ_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- ):
- """Get a single commission"""
- return _get_commission_or_404(session, project_id, commission_id)
- @router.post("/commission/{commission_id}", response_model=CommissionResponse)
- async def update_commission(
- project_id: UUID,
- commission_id: UUID,
- edit_commission: CommissionUpdateRequest,
- current_user: User = Depends(deps.require_org_role(*MANAGE_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- ):
- """Edit a commission's details"""
- commission = _get_commission_or_404(session, project_id, commission_id)
- commission.name = edit_commission.name
- session.commit()
- session.refresh(commission)
- return commission
- @router.delete("/commission/{commission_id}")
- async def delete_commission(
- project_id: UUID,
- commission_id: UUID,
- current_user: User = Depends(deps.require_org_role(*MANAGE_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- ):
- """Delete a commission (automatically deletes CommissionMember links via cascade)"""
- commission = _get_commission_or_404(session, project_id, commission_id)
- session.delete(commission)
- session.commit()
- @router.post("/commission/{commission_id}/members", response_model=CommissionResponse)
- async def add_members_to_commission(
- project_id: UUID,
- commission_id: UUID,
- payload: CommissionMembershipRequest,
- current_user: User = Depends(deps.require_org_role(*MANAGE_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- ):
- """Bulk-add users as members to a commission (idempotent)"""
- commission = _get_commission_or_404(session, project_id, commission_id)
- # Verify users exist
- valid_users = session.scalars(select(User).where(User.id.in_(payload.user_ids))).all()
- if len(valid_users) != len(payload.user_ids):
- raise HTTPException(status_code=400, detail="One or more invalid user IDs")
- existing_ids = {u.id for u in commission.members}
- for user in valid_users:
- if user.id not in existing_ids:
- commission.members.append(user)
- session.commit()
- return _get_commission_or_404(session, project_id, commission_id)
- @router.delete("/commission/{commission_id}/member/{user_id}", response_model=CommissionResponse)
- async def remove_member_from_commission(
- project_id: UUID,
- commission_id: UUID,
- user_id: UUID,
- current_user: User = Depends(deps.require_org_role(*MANAGE_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- ):
- """Remove a single user from a commission"""
- commission = _get_commission_or_404(session, project_id, commission_id)
- member_to_remove = next((m for m in commission.members if str(m.id) == str(user_id)), None)
- if member_to_remove:
- session.delete(member_to_remove)
- session.commit()
- return _get_commission_or_404(session, project_id, commission_id)
- @router.post(
- "/commission/{commission_id}/invite-member",
- response_model=CommissionResponse,
- )
- async def invite_commission_member(
- project_id: UUID,
- commission_id: UUID,
- payload: UserInviteRequest,
- current_user: User = Depends(deps.require_org_role(*MANAGE_COMMISSIONS)),
- session: Session = Depends(deps.get_session),
- email_sender: EmailSender = Depends(get_email_sender),
- ):
- commission = _get_commission_or_404(session, project_id, commission_id)
- project = session.get(Project, project_id)
- user = session.scalar(select(User).where(User.email == payload.email))
- is_new_user = user is None
- if payload.org_role == OrgRole.ORG_ADMIN:
- caller_membership = session.get(
- UserOrganization, (current_user.id, project.organization_id)
- )
- is_caller_org_admin = (
- caller_membership is not None and caller_membership.role == OrgRole.ORG_ADMIN
- )
- if not (is_caller_org_admin or current_user.global_role == "super_admin"):
- raise HTTPException(status_code=403, detail="Only an org_admin can grant org_admin")
- if is_new_user:
- user = User(
- email=payload.email,
- name=payload.name,
- phone_number=payload.phone_number,
- hashed_password=get_password_hash(secrets.token_urlsafe(32)), # unusable placeholder
- must_change_password=True,
- )
- session.add(user)
- session.flush()
- existing_membership = session.get(UserOrganization, (user.id, project.organization_id))
- if existing_membership is None:
- session.add(
- UserOrganization(
- user_id=user.id,
- organization_id=project.organization_id,
- role=payload.org_role,
- )
- )
- if user not in commission.members:
- commission.members.append(user)
- session.commit()
- if is_new_user:
- await issue_reset_token(session, user, "invite", email_sender, settings.FRONT_END_URL)
- return _get_commission_or_404(session, project_id, commission_id)
|