indie-status-page/app/api/subscribers.py
2026-04-25 12:14:06 +00:00

118 lines
No EOL
3.7 KiB
Python

"""Subscribers API endpoints with tier enforcement.
When adding a subscriber to an organization, the org's subscriber limit
is checked against the org's tier.
"""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, verify_api_key
from app.models.models import Subscriber
from app.models.saas_models import Organization
from app.services.tier_enforcement import enforce_subscriber_limit
router = APIRouter()
class SubscriberCreate(BaseModel):
email: str = Field(..., max_length=255)
organization_id: str | None = None # Optional: for tier enforcement
@router.get("/")
async def list_subscribers(db: AsyncSession = Depends(get_db)):
"""List all subscribers."""
result = await db.execute(select(Subscriber))
subscribers = result.scalars().all()
return [
{
"id": s.id,
"email": s.email,
"organization_id": s.organization_id,
"is_confirmed": s.is_confirmed,
"created_at": s.created_at.isoformat() if s.created_at else None,
}
for s in subscribers
]
@router.post("/", status_code=status.HTTP_201_CREATED)
async def create_subscriber(
data: SubscriberCreate,
db: AsyncSession = Depends(get_db),
api_key: str = Depends(verify_api_key),
):
"""Add a new subscriber.
If organization_id is provided, tier enforcement is applied to ensure
the org hasn't exceeded its subscriber limit.
"""
org = None
if data.organization_id:
result = await db.execute(
select(Organization).where(Organization.id == data.organization_id)
)
org = result.scalar_one_or_none()
if org is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Organization '{data.organization_id}' not found",
)
# Tier enforcement when org context is provided
if org is not None:
await enforce_subscriber_limit(db, org)
import uuid
subscriber = Subscriber(
email=data.email,
confirm_token=str(uuid.uuid4()),
organization_id=data.organization_id,
)
db.add(subscriber)
await db.flush()
await db.refresh(subscriber)
return {
"id": subscriber.id,
"email": subscriber.email,
"organization_id": subscriber.organization_id,
"confirm_token": subscriber.confirm_token,
}
@router.delete("/{subscriber_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_subscriber(
subscriber_id: UUID,
db: AsyncSession = Depends(get_db),
api_key: str = Depends(verify_api_key),
):
"""Remove a subscriber."""
result = await db.execute(select(Subscriber).where(Subscriber.id == str(subscriber_id)))
subscriber = result.scalar_one_or_none()
if not subscriber:
raise HTTPException(status_code=404, detail="Subscriber not found")
await db.delete(subscriber)
@router.post("/{subscriber_id}/confirm")
async def confirm_subscriber(
subscriber_id: UUID,
token: str,
db: AsyncSession = Depends(get_db),
):
"""Confirm a subscriber's email address."""
result = await db.execute(select(Subscriber).where(Subscriber.id == str(subscriber_id)))
subscriber = result.scalar_one_or_none()
if not subscriber:
raise HTTPException(status_code=404, detail="Subscriber not found")
if subscriber.confirm_token != token:
raise HTTPException(status_code=400, detail="Invalid confirmation token")
subscriber.is_confirmed = True
subscriber.confirm_token = None
await db.flush()
return {"message": "Subscriber confirmed", "email": subscriber.email}