feat: initial public release
ConsentOS — a privacy-first cookie consent management platform. Self-hosted, source-available alternative to OneTrust, Cookiebot, and CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant architecture with role-based access, configuration cascade (system → org → group → site → region), dark-pattern detection in the scanner, and a tamper-evident consent record audit trail. This is the initial public release. Prior development history is retained internally. See README.md for the feature list, architecture overview, and quick-start instructions. Licensed under the Elastic Licence 2.0 — self-host freely; do not resell as a managed service.
This commit is contained in:
210
apps/api/src/main.py
Normal file
210
apps/api/src/main.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from src.config.edition import edition_name
|
||||
from src.config.logging import setup_logging
|
||||
from src.config.settings import get_settings
|
||||
from src.extensions.registry import discover_extensions, get_registry
|
||||
from src.middleware.rate_limit import RateLimitMiddleware
|
||||
from src.middleware.security_headers import SecurityHeadersMiddleware
|
||||
from src.routers import (
|
||||
auth,
|
||||
compliance,
|
||||
config,
|
||||
consent,
|
||||
cookies,
|
||||
org_config,
|
||||
organisations,
|
||||
scanner,
|
||||
site_group_config,
|
||||
site_groups,
|
||||
sites,
|
||||
translations,
|
||||
users,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Application startup and shutdown lifecycle."""
|
||||
settings = get_settings()
|
||||
setup_logging(settings.log_level)
|
||||
yield
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Application factory."""
|
||||
settings = get_settings()
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
version=settings.app_version,
|
||||
description=(
|
||||
"Multi-tenant cookie consent management platform API. "
|
||||
"Provides consent collection, cookie scanning, auto-blocking, "
|
||||
"compliance checking, and analytics across multiple sites."
|
||||
),
|
||||
debug=settings.debug,
|
||||
lifespan=lifespan,
|
||||
openapi_tags=[
|
||||
{
|
||||
"name": "auth",
|
||||
"description": "Authentication — login, token refresh, and current user.",
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"description": (
|
||||
"Site configuration — public endpoints for the banner script "
|
||||
"to fetch config, GeoIP-resolved config, and CDN publishing."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "consent",
|
||||
"description": (
|
||||
"Consent recording and retrieval — public endpoints called "
|
||||
"by the banner script to record visitor consent decisions."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "sites",
|
||||
"description": "Site and site config CRUD — manage domains and settings.",
|
||||
},
|
||||
{
|
||||
"name": "cookies",
|
||||
"description": (
|
||||
"Cookie management — categories, discovered cookies, allow-list, "
|
||||
"known cookies database, and auto-classification."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "scanner",
|
||||
"description": (
|
||||
"Cookie scanner — trigger scans, view results, and receive "
|
||||
"client-side cookie reports from the banner script."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "compliance",
|
||||
"description": (
|
||||
"Compliance checking — run checks against GDPR, CNIL, CCPA, "
|
||||
"ePrivacy, and LGPD frameworks."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "organisations",
|
||||
"description": "Organisation management — multi-tenant root entities.",
|
||||
},
|
||||
{
|
||||
"name": "users",
|
||||
"description": "User management — org-scoped users with role-based access.",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# Security headers
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
# Rate limiting (must be added before CORS to count requests correctly)
|
||||
if settings.rate_limit_enabled:
|
||||
app.add_middleware(
|
||||
RateLimitMiddleware,
|
||||
redis_url=settings.redis_url,
|
||||
requests_per_minute=settings.rate_limit_per_minute,
|
||||
auth_requests_per_minute=10,
|
||||
)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.allowed_origins_list,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Core routers
|
||||
api_prefix = "/api/v1"
|
||||
app.include_router(auth.router, prefix=api_prefix)
|
||||
app.include_router(config.router, prefix=api_prefix)
|
||||
app.include_router(consent.router, prefix=api_prefix)
|
||||
app.include_router(scanner.router, prefix=api_prefix)
|
||||
app.include_router(compliance.router, prefix=api_prefix)
|
||||
app.include_router(organisations.router, prefix=api_prefix)
|
||||
app.include_router(org_config.router, prefix=api_prefix)
|
||||
app.include_router(users.router, prefix=api_prefix)
|
||||
app.include_router(site_groups.router, prefix=api_prefix)
|
||||
app.include_router(site_group_config.router, prefix=api_prefix)
|
||||
app.include_router(sites.router, prefix=api_prefix)
|
||||
app.include_router(cookies.router, prefix=api_prefix)
|
||||
app.include_router(translations.router, prefix=api_prefix)
|
||||
app.include_router(translations.public_router, prefix=api_prefix)
|
||||
|
||||
# Discover and mount enterprise extensions (no-op in CE mode)
|
||||
discover_extensions()
|
||||
registry = get_registry()
|
||||
registry.apply(app)
|
||||
|
||||
@app.get("/health", tags=["health"])
|
||||
async def health() -> dict[str, str]:
|
||||
"""Shallow liveness check.
|
||||
|
||||
Answers "is the process running?". Suitable for orchestrator
|
||||
liveness probes. For deployment readiness, use
|
||||
``/health/ready`` which verifies downstream dependencies.
|
||||
"""
|
||||
return {"status": "ok", "edition": edition_name()}
|
||||
|
||||
@app.get("/health/ready", tags=["health"])
|
||||
async def health_ready() -> dict[str, object]:
|
||||
"""Deep readiness check — verifies database and Redis.
|
||||
|
||||
Returns HTTP 503 if either dependency is unreachable so load
|
||||
balancers route traffic away from broken instances.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.db.session import engine as db_engine
|
||||
|
||||
checks: dict[str, str] = {}
|
||||
overall_ok = True
|
||||
|
||||
# Database
|
||||
try:
|
||||
async with db_engine.connect() as conn:
|
||||
await conn.execute(text("SELECT 1"))
|
||||
checks["database"] = "ok"
|
||||
except Exception as exc:
|
||||
checks["database"] = f"error: {type(exc).__name__}"
|
||||
overall_ok = False
|
||||
|
||||
# Redis
|
||||
try:
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
r = aioredis.from_url(settings.redis_url, decode_responses=True)
|
||||
pong = await r.ping()
|
||||
checks["redis"] = "ok" if pong else "error: ping failed"
|
||||
if not pong:
|
||||
overall_ok = False
|
||||
await r.aclose()
|
||||
except Exception as exc:
|
||||
checks["redis"] = f"error: {type(exc).__name__}"
|
||||
overall_ok = False
|
||||
|
||||
payload = {
|
||||
"status": "ok" if overall_ok else "degraded",
|
||||
"edition": edition_name(),
|
||||
"checks": checks,
|
||||
}
|
||||
if not overall_ok:
|
||||
raise HTTPException(status_code=503, detail=payload)
|
||||
return payload
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
Reference in New Issue
Block a user