feat: indie status page MVP -- FastAPI + SQLite
- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
This commit is contained in:
commit
902133edd3
4655 changed files with 1342691 additions and 0 deletions
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
195
app/api/incidents.py
Normal file
195
app/api/incidents.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
"""Incidents API endpoints."""
|
||||
|
||||
from datetime import datetime
|
||||
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 Incident, IncidentUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class IncidentCreate(BaseModel):
|
||||
service_id: UUID
|
||||
title: str = Field(..., max_length=200)
|
||||
status: str = Field(..., pattern=r"^(investigating|identified|monitoring|resolved)$")
|
||||
severity: str = Field(..., pattern=r"^(minor|major|outage)$")
|
||||
started_at: datetime | None = None
|
||||
|
||||
|
||||
class IncidentUpdateCreate(BaseModel):
|
||||
status: str = Field(..., pattern=r"^(investigating|identified|monitoring|resolved)$")
|
||||
body: str
|
||||
|
||||
|
||||
class IncidentPatch(BaseModel):
|
||||
title: str | None = None
|
||||
status: str | None = Field(None, pattern=r"^(investigating|identified|monitoring|resolved)$")
|
||||
severity: str | None = Field(None, pattern=r"^(minor|major|outage)$")
|
||||
|
||||
|
||||
def serialize_incident(i: Incident) -> dict:
|
||||
return {
|
||||
"id": i.id,
|
||||
"service_id": i.service_id,
|
||||
"title": i.title,
|
||||
"status": i.status,
|
||||
"severity": i.severity,
|
||||
"started_at": i.started_at.isoformat() if i.started_at else None,
|
||||
"resolved_at": i.resolved_at.isoformat() if i.resolved_at else None,
|
||||
"created_at": i.created_at.isoformat() if i.created_at else None,
|
||||
"updated_at": i.updated_at.isoformat() if i.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
async def serialize_incident_detail(i: Incident, db: AsyncSession) -> dict:
|
||||
"""Serialize incident with its updates, querying explicitly to avoid lazy-load issues."""
|
||||
data = serialize_incident(i)
|
||||
# Explicitly query updates instead of relying on lazy-loaded relationship
|
||||
result = await db.execute(
|
||||
select(IncidentUpdate)
|
||||
.where(IncidentUpdate.incident_id == i.id)
|
||||
.order_by(IncidentUpdate.created_at)
|
||||
)
|
||||
updates = result.scalars().all()
|
||||
data["updates"] = [
|
||||
{
|
||||
"id": u.id,
|
||||
"status": u.status,
|
||||
"body": u.body,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
}
|
||||
for u in updates
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_incidents(
|
||||
service_id: UUID | None = None,
|
||||
status: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List incidents with optional filtering."""
|
||||
query = select(Incident).order_by(Incident.started_at.desc())
|
||||
if service_id:
|
||||
query = query.where(Incident.service_id == str(service_id))
|
||||
if status:
|
||||
query = query.where(Incident.status == status)
|
||||
query = query.offset(offset).limit(limit)
|
||||
result = await db.execute(query)
|
||||
incidents = result.scalars().all()
|
||||
return [serialize_incident(i) for i in incidents]
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||
async def create_incident(
|
||||
data: IncidentCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Create a new incident."""
|
||||
incident = Incident(
|
||||
service_id=str(data.service_id),
|
||||
title=data.title,
|
||||
status=data.status,
|
||||
severity=data.severity,
|
||||
started_at=data.started_at or datetime.utcnow(),
|
||||
)
|
||||
db.add(incident)
|
||||
await db.flush()
|
||||
await db.refresh(incident)
|
||||
return serialize_incident(incident)
|
||||
|
||||
|
||||
@router.get("/{incident_id}")
|
||||
async def get_incident(incident_id: UUID, db: AsyncSession = Depends(get_db)):
|
||||
"""Get an incident with its updates."""
|
||||
result = await db.execute(select(Incident).where(Incident.id == str(incident_id)))
|
||||
incident = result.scalar_one_or_none()
|
||||
if not incident:
|
||||
raise HTTPException(status_code=404, detail="Incident not found")
|
||||
return await serialize_incident_detail(incident, db)
|
||||
|
||||
|
||||
@router.patch("/{incident_id}")
|
||||
async def update_incident(
|
||||
incident_id: UUID,
|
||||
data: IncidentPatch,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Update incident fields (title, status, severity)."""
|
||||
result = await db.execute(select(Incident).where(Incident.id == str(incident_id)))
|
||||
incident = result.scalar_one_or_none()
|
||||
if not incident:
|
||||
raise HTTPException(status_code=404, detail="Incident not found")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(incident, field, value)
|
||||
|
||||
# If status changed to resolved, set resolved_at
|
||||
if data.status == "resolved" and "status" in update_data:
|
||||
incident.resolved_at = datetime.utcnow()
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(incident)
|
||||
return await serialize_incident_detail(incident, db)
|
||||
|
||||
|
||||
@router.delete("/{incident_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_incident(
|
||||
incident_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Delete an incident."""
|
||||
result = await db.execute(select(Incident).where(Incident.id == str(incident_id)))
|
||||
incident = result.scalar_one_or_none()
|
||||
if not incident:
|
||||
raise HTTPException(status_code=404, detail="Incident not found")
|
||||
await db.delete(incident)
|
||||
|
||||
|
||||
@router.post("/{incident_id}/updates", status_code=status.HTTP_201_CREATED)
|
||||
async def create_incident_update(
|
||||
incident_id: UUID,
|
||||
data: IncidentUpdateCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Add an update to an incident."""
|
||||
result = await db.execute(select(Incident).where(Incident.id == str(incident_id)))
|
||||
incident = result.scalar_one_or_none()
|
||||
if not incident:
|
||||
raise HTTPException(status_code=404, detail="Incident not found")
|
||||
|
||||
update = IncidentUpdate(
|
||||
incident_id=str(incident_id),
|
||||
status=data.status,
|
||||
body=data.body,
|
||||
)
|
||||
db.add(update)
|
||||
# Also update incident status
|
||||
incident.status = data.status
|
||||
# If resolved, set resolved_at
|
||||
if data.status == "resolved":
|
||||
incident.resolved_at = datetime.utcnow()
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(update)
|
||||
return {
|
||||
"id": update.id,
|
||||
"incident_id": update.incident_id,
|
||||
"status": update.status,
|
||||
"body": update.body,
|
||||
"created_at": update.created_at.isoformat() if update.created_at else None,
|
||||
}
|
||||
166
app/api/monitors.py
Normal file
166
app/api/monitors.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
"""Monitors API endpoints."""
|
||||
|
||||
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 Monitor, MonitorResult
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class MonitorCreate(BaseModel):
|
||||
service_id: UUID
|
||||
url: str = Field(..., max_length=500)
|
||||
method: str = Field("GET", pattern=r"^(GET|POST|HEAD)$")
|
||||
expected_status: int = 200
|
||||
timeout_seconds: int = Field(10, ge=1, le=60)
|
||||
interval_seconds: int = Field(60, ge=30, le=3600)
|
||||
|
||||
|
||||
class MonitorUpdate(BaseModel):
|
||||
url: str | None = Field(None, max_length=500)
|
||||
method: str | None = Field(None, pattern=r"^(GET|POST|HEAD)$")
|
||||
expected_status: int | None = None
|
||||
timeout_seconds: int | None = Field(None, ge=1, le=60)
|
||||
interval_seconds: int | None = Field(None, ge=30, le=3600)
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
def serialize_monitor(m: Monitor) -> dict:
|
||||
return {
|
||||
"id": m.id,
|
||||
"service_id": m.service_id,
|
||||
"url": m.url,
|
||||
"method": m.method,
|
||||
"expected_status": m.expected_status,
|
||||
"timeout_seconds": m.timeout_seconds,
|
||||
"interval_seconds": m.interval_seconds,
|
||||
"is_active": m.is_active,
|
||||
"created_at": m.created_at.isoformat() if m.created_at else None,
|
||||
"updated_at": m.updated_at.isoformat() if m.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_monitors(db: AsyncSession = Depends(get_db)):
|
||||
"""List all monitors."""
|
||||
result = await db.execute(select(Monitor))
|
||||
monitors = result.scalars().all()
|
||||
return [serialize_monitor(m) for m in monitors]
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||
async def create_monitor(
|
||||
data: MonitorCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Create a new monitor."""
|
||||
monitor = Monitor(
|
||||
service_id=str(data.service_id),
|
||||
url=data.url,
|
||||
method=data.method,
|
||||
expected_status=data.expected_status,
|
||||
timeout_seconds=data.timeout_seconds,
|
||||
interval_seconds=data.interval_seconds,
|
||||
)
|
||||
db.add(monitor)
|
||||
await db.flush()
|
||||
await db.refresh(monitor)
|
||||
return serialize_monitor(monitor)
|
||||
|
||||
|
||||
@router.get("/{monitor_id}")
|
||||
async def get_monitor(monitor_id: UUID, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a monitor with recent results."""
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
|
||||
# Query recent results separately
|
||||
results_query = (
|
||||
select(MonitorResult)
|
||||
.where(MonitorResult.monitor_id == str(monitor_id))
|
||||
.order_by(MonitorResult.checked_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
results_result = await db.execute(results_query)
|
||||
recent_results = results_result.scalars().all()
|
||||
|
||||
data = serialize_monitor(monitor)
|
||||
data["recent_results"] = [
|
||||
{
|
||||
"id": r.id,
|
||||
"status": r.status,
|
||||
"response_time_ms": r.response_time_ms,
|
||||
"status_code": r.status_code,
|
||||
"error_message": r.error_message,
|
||||
"checked_at": r.checked_at.isoformat() if r.checked_at else None,
|
||||
}
|
||||
for r in recent_results
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
@router.patch("/{monitor_id}")
|
||||
async def update_monitor(
|
||||
monitor_id: UUID,
|
||||
data: MonitorUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Update a monitor."""
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(monitor, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(monitor)
|
||||
return serialize_monitor(monitor)
|
||||
|
||||
|
||||
@router.delete("/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_monitor(
|
||||
monitor_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Delete a monitor."""
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
await db.delete(monitor)
|
||||
|
||||
|
||||
@router.post("/{monitor_id}/check")
|
||||
async def trigger_check(
|
||||
monitor_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Trigger a manual uptime check for this monitor."""
|
||||
from app.services.uptime import check_monitor
|
||||
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
|
||||
monitor_result = await check_monitor(monitor, db)
|
||||
return {
|
||||
"status": monitor_result.status,
|
||||
"response_time_ms": monitor_result.response_time_ms,
|
||||
"status_code": monitor_result.status_code,
|
||||
}
|
||||
15
app/api/router.py
Normal file
15
app/api/router.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from fastapi import APIRouter
|
||||
|
||||
from app.api.services import router as services_router
|
||||
from app.api.incidents import router as incidents_router
|
||||
from app.api.monitors import router as monitors_router
|
||||
from app.api.subscribers import router as subscribers_router
|
||||
from app.api.settings import router as settings_router
|
||||
|
||||
api_v1_router = APIRouter()
|
||||
|
||||
api_v1_router.include_router(services_router, prefix="/services", tags=["services"])
|
||||
api_v1_router.include_router(incidents_router, prefix="/incidents", tags=["incidents"])
|
||||
api_v1_router.include_router(monitors_router, prefix="/monitors", tags=["monitors"])
|
||||
api_v1_router.include_router(subscribers_router, prefix="/subscribers", tags=["subscribers"])
|
||||
api_v1_router.include_router(settings_router, prefix="/settings", tags=["settings"])
|
||||
120
app/api/services.py
Normal file
120
app/api/services.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Services API endpoints."""
|
||||
|
||||
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 Service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ServiceCreate(BaseModel):
|
||||
name: str = Field(..., max_length=100)
|
||||
slug: str = Field(..., max_length=50, pattern=r"^[a-z0-9-]+$")
|
||||
description: str | None = None
|
||||
group_name: str | None = Field(None, max_length=50)
|
||||
position: int = 0
|
||||
is_visible: bool = True
|
||||
|
||||
|
||||
class ServiceUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
slug: str | None = Field(None, max_length=50, pattern=r"^[a-z0-9-]+$")
|
||||
description: str | None = None
|
||||
group_name: str | None = None
|
||||
position: int | None = None
|
||||
is_visible: bool | None = None
|
||||
|
||||
|
||||
def serialize_service(s: Service) -> dict:
|
||||
return {
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"slug": s.slug,
|
||||
"description": s.description,
|
||||
"group_name": s.group_name,
|
||||
"position": s.position,
|
||||
"is_visible": s.is_visible,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_services(db: AsyncSession = Depends(get_db)):
|
||||
"""List all services."""
|
||||
result = await db.execute(select(Service).order_by(Service.position, Service.name))
|
||||
services = result.scalars().all()
|
||||
return [serialize_service(s) for s in services]
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||
async def create_service(
|
||||
data: ServiceCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Create a new service."""
|
||||
service = Service(
|
||||
name=data.name,
|
||||
slug=data.slug,
|
||||
description=data.description,
|
||||
group_name=data.group_name,
|
||||
position=data.position,
|
||||
is_visible=data.is_visible,
|
||||
)
|
||||
db.add(service)
|
||||
await db.flush()
|
||||
await db.refresh(service)
|
||||
return serialize_service(service)
|
||||
|
||||
|
||||
@router.get("/{service_id}")
|
||||
async def get_service(service_id: UUID, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a service by ID."""
|
||||
result = await db.execute(select(Service).where(Service.id == str(service_id)))
|
||||
service = result.scalar_one_or_none()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
return serialize_service(service)
|
||||
|
||||
|
||||
@router.patch("/{service_id}")
|
||||
async def update_service(
|
||||
service_id: UUID,
|
||||
data: ServiceUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Update a service."""
|
||||
result = await db.execute(select(Service).where(Service.id == str(service_id)))
|
||||
service = result.scalar_one_or_none()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(service, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(service)
|
||||
return serialize_service(service)
|
||||
|
||||
|
||||
@router.delete("/{service_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_service(
|
||||
service_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Delete a service."""
|
||||
result = await db.execute(select(Service).where(Service.id == str(service_id)))
|
||||
service = result.scalar_one_or_none()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
await db.delete(service)
|
||||
36
app/api/settings.py
Normal file
36
app/api/settings.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""Site settings API endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, verify_api_key
|
||||
from app.models.models import SiteSetting
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_settings(db: AsyncSession = Depends(get_db)):
|
||||
"""List all site settings."""
|
||||
result = await db.execute(select(SiteSetting))
|
||||
settings = result.scalars().all()
|
||||
return {s.key: s.value for s in settings}
|
||||
|
||||
|
||||
@router.patch("/")
|
||||
async def update_settings(
|
||||
updates: dict[str, str],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Update site settings (key-value pairs)."""
|
||||
for key, value in updates.items():
|
||||
result = await db.execute(select(SiteSetting).where(SiteSetting.key == key))
|
||||
setting = result.scalar_one_or_none()
|
||||
if setting:
|
||||
setting.value = value
|
||||
else:
|
||||
db.add(SiteSetting(key=key, value=value))
|
||||
await db.flush()
|
||||
return {"message": "Settings updated"}
|
||||
84
app/api/subscribers.py
Normal file
84
app/api/subscribers.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""Subscribers API endpoints."""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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,
|
||||
"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(
|
||||
email: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Add a new subscriber."""
|
||||
import uuid
|
||||
|
||||
subscriber = Subscriber(
|
||||
email=email,
|
||||
confirm_token=str(uuid.uuid4()),
|
||||
)
|
||||
db.add(subscriber)
|
||||
await db.flush()
|
||||
await db.refresh(subscriber)
|
||||
return {
|
||||
"id": subscriber.id,
|
||||
"email": subscriber.email,
|
||||
"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}
|
||||
Loading…
Add table
Add a link
Reference in a new issue