Implement complete multi-property architecture: - Properties (groups of spaces) with public/private visibility - Property managers (many-to-many) with role-based permissions - Organizations with member management - Anonymous/guest booking support via public API (/api/public/*) - Property-scoped spaces, bookings, and settings - Frontend: property selector, organization management, public booking views - Migration script and updated seed data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
281 lines
9.7 KiB
Python
281 lines
9.7 KiB
Python
"""Organization management endpoints."""
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.deps import get_current_admin, get_current_user, get_db
|
|
from app.models.organization import Organization
|
|
from app.models.organization_member import OrganizationMember
|
|
from app.models.user import User
|
|
from app.schemas.organization import (
|
|
AddMemberRequest,
|
|
OrganizationCreate,
|
|
OrganizationMemberResponse,
|
|
OrganizationResponse,
|
|
OrganizationUpdate,
|
|
)
|
|
|
|
router = APIRouter(prefix="/organizations", tags=["organizations"])
|
|
admin_router = APIRouter(prefix="/admin/organizations", tags=["organizations-admin"])
|
|
|
|
|
|
@router.get("", response_model=list[OrganizationResponse])
|
|
def list_organizations(
|
|
db: Annotated[Session, Depends(get_db)],
|
|
_: Annotated[User, Depends(get_current_user)],
|
|
) -> list[OrganizationResponse]:
|
|
"""List organizations (authenticated users)."""
|
|
orgs = db.query(Organization).filter(Organization.is_active == True).order_by(Organization.name).all() # noqa: E712
|
|
result = []
|
|
for org in orgs:
|
|
member_count = db.query(OrganizationMember).filter(OrganizationMember.organization_id == org.id).count()
|
|
result.append(OrganizationResponse(
|
|
id=org.id,
|
|
name=org.name,
|
|
description=org.description,
|
|
is_active=org.is_active,
|
|
created_at=org.created_at,
|
|
member_count=member_count,
|
|
))
|
|
return result
|
|
|
|
|
|
@router.get("/{org_id}", response_model=OrganizationResponse)
|
|
def get_organization(
|
|
org_id: int,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
_: Annotated[User, Depends(get_current_user)],
|
|
) -> OrganizationResponse:
|
|
"""Get organization detail."""
|
|
org = db.query(Organization).filter(Organization.id == org_id).first()
|
|
if not org:
|
|
raise HTTPException(status_code=404, detail="Organization not found")
|
|
member_count = db.query(OrganizationMember).filter(OrganizationMember.organization_id == org.id).count()
|
|
return OrganizationResponse(
|
|
id=org.id,
|
|
name=org.name,
|
|
description=org.description,
|
|
is_active=org.is_active,
|
|
created_at=org.created_at,
|
|
member_count=member_count,
|
|
)
|
|
|
|
|
|
@router.get("/{org_id}/members", response_model=list[OrganizationMemberResponse])
|
|
def list_organization_members(
|
|
org_id: int,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> list[OrganizationMemberResponse]:
|
|
"""List organization members (org admin or superadmin)."""
|
|
org = db.query(Organization).filter(Organization.id == org_id).first()
|
|
if not org:
|
|
raise HTTPException(status_code=404, detail="Organization not found")
|
|
|
|
# Check permission: superadmin or org admin
|
|
if current_user.role not in ("admin", "superadmin"):
|
|
membership = db.query(OrganizationMember).filter(
|
|
OrganizationMember.organization_id == org_id,
|
|
OrganizationMember.user_id == current_user.id,
|
|
OrganizationMember.role == "admin",
|
|
).first()
|
|
if not membership:
|
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
|
|
members = db.query(OrganizationMember).filter(OrganizationMember.organization_id == org_id).all()
|
|
result = []
|
|
for m in members:
|
|
u = db.query(User).filter(User.id == m.user_id).first()
|
|
result.append(OrganizationMemberResponse(
|
|
id=m.id,
|
|
organization_id=m.organization_id,
|
|
user_id=m.user_id,
|
|
role=m.role,
|
|
user_name=u.full_name if u else None,
|
|
user_email=u.email if u else None,
|
|
))
|
|
return result
|
|
|
|
|
|
@router.post("/{org_id}/members", response_model=OrganizationMemberResponse, status_code=status.HTTP_201_CREATED)
|
|
def add_organization_member(
|
|
org_id: int,
|
|
data: AddMemberRequest,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> OrganizationMemberResponse:
|
|
"""Add member to organization (org admin or superadmin)."""
|
|
org = db.query(Organization).filter(Organization.id == org_id).first()
|
|
if not org:
|
|
raise HTTPException(status_code=404, detail="Organization not found")
|
|
|
|
# Check permission
|
|
if current_user.role not in ("admin", "superadmin"):
|
|
membership = db.query(OrganizationMember).filter(
|
|
OrganizationMember.organization_id == org_id,
|
|
OrganizationMember.user_id == current_user.id,
|
|
OrganizationMember.role == "admin",
|
|
).first()
|
|
if not membership:
|
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
|
|
# Check if user exists
|
|
user = db.query(User).filter(User.id == data.user_id).first()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
# Check if already member
|
|
existing = db.query(OrganizationMember).filter(
|
|
OrganizationMember.organization_id == org_id,
|
|
OrganizationMember.user_id == data.user_id,
|
|
).first()
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="User is already a member")
|
|
|
|
member = OrganizationMember(
|
|
organization_id=org_id,
|
|
user_id=data.user_id,
|
|
role=data.role,
|
|
)
|
|
db.add(member)
|
|
db.commit()
|
|
db.refresh(member)
|
|
|
|
return OrganizationMemberResponse(
|
|
id=member.id,
|
|
organization_id=member.organization_id,
|
|
user_id=member.user_id,
|
|
role=member.role,
|
|
user_name=user.full_name,
|
|
user_email=user.email,
|
|
)
|
|
|
|
|
|
@router.delete("/{org_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def remove_organization_member(
|
|
org_id: int,
|
|
user_id: int,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> None:
|
|
"""Remove member from organization."""
|
|
if current_user.role not in ("admin", "superadmin"):
|
|
membership = db.query(OrganizationMember).filter(
|
|
OrganizationMember.organization_id == org_id,
|
|
OrganizationMember.user_id == current_user.id,
|
|
OrganizationMember.role == "admin",
|
|
).first()
|
|
if not membership:
|
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
|
|
member = db.query(OrganizationMember).filter(
|
|
OrganizationMember.organization_id == org_id,
|
|
OrganizationMember.user_id == user_id,
|
|
).first()
|
|
if not member:
|
|
raise HTTPException(status_code=404, detail="Member not found")
|
|
|
|
db.delete(member)
|
|
db.commit()
|
|
|
|
|
|
@router.put("/{org_id}/members/{user_id}", response_model=OrganizationMemberResponse)
|
|
def update_member_role(
|
|
org_id: int,
|
|
user_id: int,
|
|
data: AddMemberRequest,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> OrganizationMemberResponse:
|
|
"""Change member role in organization."""
|
|
if current_user.role not in ("admin", "superadmin"):
|
|
membership = db.query(OrganizationMember).filter(
|
|
OrganizationMember.organization_id == org_id,
|
|
OrganizationMember.user_id == current_user.id,
|
|
OrganizationMember.role == "admin",
|
|
).first()
|
|
if not membership:
|
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
|
|
member = db.query(OrganizationMember).filter(
|
|
OrganizationMember.organization_id == org_id,
|
|
OrganizationMember.user_id == user_id,
|
|
).first()
|
|
if not member:
|
|
raise HTTPException(status_code=404, detail="Member not found")
|
|
|
|
member.role = data.role
|
|
db.commit()
|
|
db.refresh(member)
|
|
|
|
u = db.query(User).filter(User.id == user_id).first()
|
|
return OrganizationMemberResponse(
|
|
id=member.id,
|
|
organization_id=member.organization_id,
|
|
user_id=member.user_id,
|
|
role=member.role,
|
|
user_name=u.full_name if u else None,
|
|
user_email=u.email if u else None,
|
|
)
|
|
|
|
|
|
# === Superadmin endpoints ===
|
|
|
|
|
|
@admin_router.post("", response_model=OrganizationResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_organization(
|
|
data: OrganizationCreate,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
_: Annotated[User, Depends(get_current_admin)],
|
|
) -> OrganizationResponse:
|
|
"""Create an organization (superadmin)."""
|
|
existing = db.query(Organization).filter(Organization.name == data.name).first()
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Organization with this name already exists")
|
|
|
|
org = Organization(name=data.name, description=data.description)
|
|
db.add(org)
|
|
db.commit()
|
|
db.refresh(org)
|
|
|
|
return OrganizationResponse(
|
|
id=org.id,
|
|
name=org.name,
|
|
description=org.description,
|
|
is_active=org.is_active,
|
|
created_at=org.created_at,
|
|
member_count=0,
|
|
)
|
|
|
|
|
|
@admin_router.put("/{org_id}", response_model=OrganizationResponse)
|
|
def update_organization(
|
|
org_id: int,
|
|
data: OrganizationUpdate,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
_: Annotated[User, Depends(get_current_admin)],
|
|
) -> OrganizationResponse:
|
|
"""Update an organization (superadmin)."""
|
|
org = db.query(Organization).filter(Organization.id == org_id).first()
|
|
if not org:
|
|
raise HTTPException(status_code=404, detail="Organization not found")
|
|
|
|
if data.name is not None:
|
|
org.name = data.name
|
|
if data.description is not None:
|
|
org.description = data.description
|
|
|
|
db.commit()
|
|
db.refresh(org)
|
|
|
|
member_count = db.query(OrganizationMember).filter(OrganizationMember.organization_id == org.id).count()
|
|
return OrganizationResponse(
|
|
id=org.id,
|
|
name=org.name,
|
|
description=org.description,
|
|
is_active=org.is_active,
|
|
created_at=org.created_at,
|
|
member_count=member_count,
|
|
)
|