Files
syndarix/backend/app/api/routes/organizations.py
Felipe Cardoso 98b455fdc3 refactor(backend): enforce route→service→repo layered architecture
- introduce custom repository exception hierarchy (DuplicateEntryError,
  IntegrityConstraintError, InvalidInputError) replacing raw ValueError
- eliminate all direct repository imports and raw SQL from route layer
- add UserService, SessionService, OrganizationService to service layer
- add get_stats/get_org_distribution service methods replacing admin inline SQL
- fix timing side-channel in authenticate_user via dummy bcrypt check
- replace SHA-256 client secret fallback with explicit InvalidClientError
- replace assert with InvalidGrantError in authorization code exchange
- replace N+1 token revocation loops with bulk UPDATE statements
- rename oauth account token fields (drop misleading 'encrypted' suffix)
- add Alembic migration 0003 for token field column rename
- add 45 new service/repository tests; 975 passing, 94% coverage
2026-02-27 09:32:57 +01:00

212 lines
6.6 KiB
Python
Executable File

# app/api/routes/organizations.py
"""
Organization endpoints for regular users.
These endpoints allow users to view and manage organizations they belong to.
"""
import logging
from typing import Any
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.auth import get_current_user
from app.api.dependencies.permissions import require_org_admin, require_org_membership
from app.core.database import get_db
from app.models.user import User
from app.services.organization_service import organization_service
from app.schemas.common import (
PaginatedResponse,
PaginationParams,
create_pagination_meta,
)
from app.schemas.organizations import (
OrganizationMemberResponse,
OrganizationResponse,
OrganizationUpdate,
)
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get(
"/me",
response_model=list[OrganizationResponse],
summary="Get My Organizations",
description="Get all organizations the current user belongs to",
operation_id="get_my_organizations",
)
async def get_my_organizations(
is_active: bool = Query(True, description="Filter by active membership"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Get all organizations the current user belongs to.
Returns organizations with member count for each.
Uses optimized single query to avoid N+1 problem.
"""
try:
# Get all org data in single query with JOIN and subquery
orgs_data = await organization_service.get_user_organizations_with_details(
db, user_id=current_user.id, is_active=is_active
)
# Transform to response objects
orgs_with_data = []
for item in orgs_data:
org = item["organization"]
org_dict = {
"id": org.id,
"name": org.name,
"slug": org.slug,
"description": org.description,
"is_active": org.is_active,
"settings": org.settings,
"created_at": org.created_at,
"updated_at": org.updated_at,
"member_count": item["member_count"],
}
orgs_with_data.append(OrganizationResponse(**org_dict))
return orgs_with_data
except Exception as e:
logger.error(f"Error getting user organizations: {e!s}", exc_info=True)
raise
@router.get(
"/{organization_id}",
response_model=OrganizationResponse,
summary="Get Organization Details",
description="Get details of an organization the user belongs to",
operation_id="get_organization",
)
async def get_organization(
organization_id: UUID,
current_user: User = Depends(require_org_membership),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Get details of a specific organization.
User must be a member of the organization.
"""
try:
org = await organization_service.get_organization(db, str(organization_id))
org_dict = {
"id": org.id,
"name": org.name,
"slug": org.slug,
"description": org.description,
"is_active": org.is_active,
"settings": org.settings,
"created_at": org.created_at,
"updated_at": org.updated_at,
"member_count": await organization_service.get_member_count(
db, organization_id=org.id
),
}
return OrganizationResponse(**org_dict)
except Exception as e:
logger.error(f"Error getting organization: {e!s}", exc_info=True)
raise
@router.get(
"/{organization_id}/members",
response_model=PaginatedResponse[OrganizationMemberResponse],
summary="Get Organization Members",
description="Get all members of an organization (members can view)",
operation_id="get_organization_members",
)
async def get_organization_members(
organization_id: UUID,
pagination: PaginationParams = Depends(),
is_active: bool = Query(True, description="Filter by active status"),
current_user: User = Depends(require_org_membership),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Get all members of an organization.
User must be a member of the organization to view members.
"""
try:
members, total = await organization_service.get_organization_members(
db,
organization_id=organization_id,
skip=pagination.offset,
limit=pagination.limit,
is_active=is_active,
)
member_responses = [OrganizationMemberResponse(**member) for member in members]
pagination_meta = create_pagination_meta(
total=total,
page=pagination.page,
limit=pagination.limit,
items_count=len(member_responses),
)
return PaginatedResponse(data=member_responses, pagination=pagination_meta)
except Exception as e:
logger.error(f"Error getting organization members: {e!s}", exc_info=True)
raise
@router.put(
"/{organization_id}",
response_model=OrganizationResponse,
summary="Update Organization",
description="Update organization details (admin/owner only)",
operation_id="update_organization",
)
async def update_organization(
organization_id: UUID,
org_in: OrganizationUpdate,
current_user: User = Depends(require_org_admin),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Update organization details.
Requires owner or admin role in the organization.
"""
try:
org = await organization_service.get_organization(db, str(organization_id))
updated_org = await organization_service.update_organization(
db, org=org, obj_in=org_in
)
logger.info(
f"User {current_user.email} updated organization {updated_org.name}"
)
org_dict = {
"id": updated_org.id,
"name": updated_org.name,
"slug": updated_org.slug,
"description": updated_org.description,
"is_active": updated_org.is_active,
"settings": updated_org.settings,
"created_at": updated_org.created_at,
"updated_at": updated_org.updated_at,
"member_count": await organization_service.get_member_count(
db, organization_id=updated_org.id
),
}
return OrganizationResponse(**org_dict)
except Exception as e:
logger.error(f"Error updating organization: {e!s}", exc_info=True)
raise