- rbac.py: PortalCaller dataclass + get_portal_caller dependency (header-based)
- rbac.py: require_platform_admin (403 for non-platform_admin)
- rbac.py: require_tenant_admin (platform_admin bypasses; customer_admin
checks UserTenantRole; operator always rejected)
- rbac.py: require_tenant_member (platform_admin bypasses; all roles
checked against UserTenantRole)
- invite_token.py: generate_invite_token (HMAC-SHA256, base64url, 48h TTL)
- invite_token.py: validate_invite_token (timing-safe compare_digest, TTL check)
- invite_token.py: token_to_hash (SHA-256 for DB storage)
- email.py: send_invite_email (sync smtplib, skips if smtp_host empty)
- invitations.py: POST /api/portal/invitations (create, requires tenant admin)
- invitations.py: POST /api/portal/invitations/accept (accept invitation)
- invitations.py: POST /api/portal/invitations/{id}/resend (regenerate token)
- invitations.py: GET /api/portal/invitations (list pending)
- portal.py: AuthVerifyResponse now returns role+tenant_ids+active_tenant_id
- portal.py: auth/register gated behind require_platform_admin
- tasks.py: send_invite_email_task Celery task (fire-and-forget)
- gateway/main.py: invitations_router mounted
171 lines
6.0 KiB
Python
171 lines
6.0 KiB
Python
"""
|
|
Channel Gateway — FastAPI application.
|
|
|
|
Mounts the slack-bolt AsyncApp as a sub-application at /slack/events.
|
|
Registers the WhatsApp webhook router at /whatsapp/webhook.
|
|
Serves all Phase 3 portal API routes under /api/portal/* and /api/webhooks/*.
|
|
|
|
Port: 8001
|
|
|
|
Endpoints:
|
|
POST /slack/events — Slack Events API webhook (handled by slack-bolt)
|
|
GET /whatsapp/webhook — WhatsApp hub challenge verification
|
|
POST /whatsapp/webhook — WhatsApp inbound message webhook
|
|
GET /api/portal/* — Portal management API (tenants, agents, billing, etc.)
|
|
GET /api/portal/billing/* — Stripe billing endpoints
|
|
GET /api/portal/channels/* — Channel connection endpoints (Slack OAuth, WhatsApp)
|
|
GET /api/portal/tenants/{id}/llm-keys — BYO LLM key management
|
|
GET /api/portal/usage/* — Usage and cost analytics
|
|
POST /api/webhooks/* — Stripe webhook receiver
|
|
GET /health — Health check
|
|
|
|
Startup sequence:
|
|
1. Create Redis connection
|
|
2. Create slack-bolt AsyncApp (signing_secret=...)
|
|
3. Register Slack event handlers
|
|
4. Mount slack-bolt request handler at /slack/events
|
|
5. Include WhatsApp router
|
|
6. Include Phase 3 portal API routers
|
|
7. Expose /health
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import FastAPI, Request, Response
|
|
from redis.asyncio import Redis
|
|
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
|
|
from slack_bolt.async_app import AsyncApp
|
|
|
|
from gateway.channels.slack import register_slack_handlers
|
|
from gateway.channels.whatsapp import whatsapp_router
|
|
from shared.api import (
|
|
billing_router,
|
|
channels_router,
|
|
invitations_router,
|
|
llm_keys_router,
|
|
portal_router,
|
|
usage_router,
|
|
webhook_router,
|
|
)
|
|
from shared.config import settings
|
|
from shared.db import async_session_factory
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FastAPI app
|
|
# ---------------------------------------------------------------------------
|
|
app = FastAPI(
|
|
title="Konstruct Channel Gateway",
|
|
description="Unified ingress for all messaging platforms",
|
|
version="0.1.0",
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CORS — allow portal origin to call gateway API
|
|
# ---------------------------------------------------------------------------
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[
|
|
"http://localhost:3000",
|
|
"http://127.0.0.1:3000",
|
|
"http://100.64.0.10:3000",
|
|
],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slack bolt app — initialized at module import time.
|
|
# signing_secret="" is safe for local dev/testing; set via env in production.
|
|
# ---------------------------------------------------------------------------
|
|
slack_app: AsyncApp | None = None
|
|
if settings.slack_bot_token and settings.slack_signing_secret:
|
|
slack_app = AsyncApp(
|
|
token=settings.slack_bot_token,
|
|
signing_secret=settings.slack_signing_secret,
|
|
)
|
|
else:
|
|
import logging
|
|
logging.getLogger(__name__).warning(
|
|
"SLACK_BOT_TOKEN or SLACK_SIGNING_SECRET not set — Slack adapter disabled"
|
|
)
|
|
|
|
# Async Redis client — shared across all request handlers
|
|
_redis: Redis | None = None # type: ignore[type-arg]
|
|
|
|
|
|
def _get_redis() -> Redis: # type: ignore[type-arg]
|
|
"""Return the module-level Redis client, creating it if necessary."""
|
|
global _redis
|
|
if _redis is None:
|
|
_redis = Redis.from_url(settings.redis_url, decode_responses=True)
|
|
return _redis
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Register Slack event handlers
|
|
# ---------------------------------------------------------------------------
|
|
slack_handler: AsyncSlackRequestHandler | None = None
|
|
if slack_app is not None:
|
|
register_slack_handlers(
|
|
slack_app=slack_app,
|
|
redis=_get_redis(),
|
|
get_session=async_session_factory,
|
|
)
|
|
slack_handler = AsyncSlackRequestHandler(slack_app)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Register channel routers
|
|
# ---------------------------------------------------------------------------
|
|
app.include_router(whatsapp_router)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Register Phase 3 portal API routers
|
|
# ---------------------------------------------------------------------------
|
|
app.include_router(portal_router)
|
|
app.include_router(billing_router)
|
|
app.include_router(channels_router)
|
|
app.include_router(llm_keys_router)
|
|
app.include_router(usage_router)
|
|
app.include_router(webhook_router)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Register Phase 4 RBAC routers
|
|
# ---------------------------------------------------------------------------
|
|
app.include_router(invitations_router)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Routes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@app.post("/slack/events")
|
|
async def slack_events(request: Request) -> Response:
|
|
"""
|
|
Slack Events API webhook endpoint.
|
|
|
|
slack-bolt's AsyncSlackRequestHandler handles:
|
|
- Slack signature verification (X-Slack-Signature)
|
|
- URL verification challenge (type=url_verification)
|
|
- Event routing to registered handlers
|
|
|
|
CRITICAL: This endpoint MUST return HTTP 200 within 3 seconds.
|
|
All LLM/heavy work is dispatched to Celery inside the event handlers.
|
|
"""
|
|
if slack_handler is None:
|
|
return Response(content='{"error":"Slack not configured"}', status_code=503, media_type="application/json")
|
|
return await slack_handler.handle(request)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health() -> dict[str, str]:
|
|
"""Health check endpoint."""
|
|
return {"status": "ok", "service": "gateway"}
|