dograh/api/routes/main.py

182 lines
7.1 KiB
Python
Raw Normal View History

import secrets
from typing import Annotated
from fastapi import APIRouter, Header, HTTPException, status
2025-09-09 14:37:32 +05:30
from loguru import logger
from pydantic import BaseModel
2025-09-09 14:37:32 +05:30
from api.routes.agent_stream import router as agent_stream_router
from api.routes.auth import router as auth_router
2025-09-09 14:37:32 +05:30
from api.routes.campaign import router as campaign_router
from api.routes.credentials import router as credentials_router
from api.routes.folder import router as folder_router
from api.routes.knowledge_base import router as knowledge_base_router
from api.routes.node_types import router as node_types_router
from api.routes.organization import router as organization_router
2025-09-09 14:37:32 +05:30
from api.routes.organization_usage import router as organization_usage_router
from api.routes.public_agent import router as public_agent_router
from api.routes.public_download import router as public_download_router
from api.routes.public_embed import router as public_embed_router
2025-09-09 14:37:32 +05:30
from api.routes.reports import router as reports_router
from api.routes.s3_signed_url import router as s3_router
from api.routes.service_keys import router as service_keys_router
from api.routes.superuser import router as superuser_router
from api.routes.telephony import router as telephony_router
from api.routes.tool import router as tool_router
from api.routes.turn_credentials import router as turn_credentials_router
2025-09-09 14:37:32 +05:30
from api.routes.user import router as user_router
from api.routes.webrtc_signaling import router as webrtc_signaling_router
2025-09-09 14:37:32 +05:30
from api.routes.workflow import router as workflow_router
from api.routes.workflow_embed import router as workflow_embed_router
from api.routes.workflow_recording import router as workflow_recording_router
from api.routes.workflow_text_chat import router as workflow_text_chat_router
from api.services.integrations import all_routers
2025-09-09 14:37:32 +05:30
router = APIRouter(
tags=["main"],
responses={404: {"description": "Not found"}},
)
router.include_router(telephony_router)
2025-09-09 14:37:32 +05:30
router.include_router(superuser_router)
router.include_router(workflow_router)
router.include_router(workflow_text_chat_router)
2025-09-09 14:37:32 +05:30
router.include_router(user_router)
router.include_router(campaign_router)
router.include_router(credentials_router)
router.include_router(tool_router)
router.include_router(organization_router)
2025-09-09 14:37:32 +05:30
router.include_router(s3_router)
router.include_router(service_keys_router)
router.include_router(organization_usage_router)
router.include_router(reports_router)
router.include_router(webrtc_signaling_router)
router.include_router(turn_credentials_router)
router.include_router(public_embed_router)
router.include_router(public_agent_router)
router.include_router(public_download_router)
router.include_router(workflow_embed_router)
router.include_router(knowledge_base_router)
router.include_router(workflow_recording_router)
router.include_router(folder_router)
router.include_router(auth_router)
router.include_router(node_types_router)
router.include_router(agent_stream_router)
2025-09-09 14:37:32 +05:30
for _integration_router in all_routers():
router.include_router(_integration_router)
2025-09-09 14:37:32 +05:30
class HealthResponse(BaseModel):
status: str
version: str
backend_api_endpoint: str
# Public URL the deployment is reachable at when it sits behind a Cloudflare
# tunnel (the host has no public IP). null for a directly-reachable deployment.
# The UI shows this so operators know the URL telephony providers should call.
tunnel_url: str | None = None
deployment_mode: str
auth_provider: str
turn_enabled: bool
force_turn_relay: bool
feat(auth): gate OSS signup behind ENABLE_SIGNUP flag (#514) * feat(auth): gate OSS signup behind ENABLE_SIGNUP flag ## Problem The `POST /api/v1/auth/signup` endpoint is unconditionally exposed on every OSS install. Operators running an invite-only deployment (private customer instances, staging environments, internal-only tenants) have no way to disable public account creation without patching the codebase. The UI also shows the "Sign up" link on `/auth/login` regardless of whether signup is available, so a locked-down deployment leaves broken navigation on the login page. ## Fix Introduce a single `ENABLE_SIGNUP` env var (default `true` — no behavior change for existing installs) that controls signup end-to-end: - **Backend** — `api/constants.ENABLE_SIGNUP` is read at module load. The signup handler returns 403 when it's false. Also exposed on `GET /api/v1/health` as `signup_enabled: bool` so the UI can mirror the operator's choice at runtime instead of at bundle-build time. - **UI** — `getSignupEnabled()` in `lib/auth/config.ts` proxies the health field, `/api/config/auth` surfaces it to the browser, the login page conditionally renders the "Sign up" link via a one-shot `fetch("/api/config/auth")` in `useEffect`, and the middleware redirects `/auth/signup` → `/auth/login` when disabled (fires before Next.js can serve the statically-prerendered signup page). - **Helm** — `config.enableSignup` (default `true`) is rendered into the ConfigMap as `ENABLE_SIGNUP` so operators can flip it via `--set config.enableSignup=false` at install/upgrade time. Fallbacks default to `signupEnabled: true` in every layer so a fresh install "just works" and matches the backend default. * address review: rollout on ConfigMap change, cache TTL, no signup-link flash Four review points on #514: **P1 — ConfigMap Change Skips Rollout** (`configmap.yaml`). `helm upgrade --set config.enableSignup=false` updated the ConfigMap but did NOT roll the api pods, so running processes kept the ENABLE_SIGNUP env from startup and continued serving the old signup behavior — including divergence between replicas mid-upgrade. Fix: add the standard `checksum/config` pod-template annotation on the four backend Deployments that `envFrom` the ConfigMap (`web`, `arq-worker`, `ari-manager`, `campaign-orchestrator`). Verified with `helm template`: all four Deployments share the same checksum on any given render, and flipping `config.enableSignup` changes the checksum uniformly so kubectl sees a pod-template diff and rolls all four. **P1 — Signup Flag Stays Cached (server)** (`ui/src/lib/auth/config.ts`). Module-scoped cache had no TTL. `revalidate: 300` was passed on the underlying `fetch()` but the in-memory short-circuit above ran first, so the value never refreshed until the UI pod restarted. Fix: add `AUTH_CONFIG_TTL_MS = 5 * 60 * 1000` (matching the fetch revalidate hint) so the module cache and the Next fetch cache stay in sync. Backend flag flips propagate within 5 minutes without a pod restart. **P1 — Middleware Redirect Uses Stale State** (`ui/src/middleware.ts`). Same shape as above — a separate module cache with no expiry could keep redirecting `/auth/signup → /auth/login` after signup was re-enabled, or keep serving the statically-prerendered signup page after lockdown. Fix: same `SERVER_CONFIG_TTL_MS = 5 * 60 * 1000` TTL on the middleware cache. **P2 — Signup link flash on login page** (`ui/src/app/auth/login/page.tsx`). Initial `signupEnabled` state was `null`, so `{signupEnabled && ...}` hid the link on first paint and it popped in after the fetch resolved — a CLS on every login-page load on stock installs where signup is enabled. Fix: initialise the state to `true` (matches the backend default). The fetch still overrides to `false` when the operator has actually disabled signup, so the lockdown UI behavior is unchanged; only the happy-path flash is gone. * simplify signup flag: drop TTL caches and middleware redirect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * resolve signup flag server-side to avoid signup link flicker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: prabhat pankaj <prabhatiitbhu@gmail.com> Co-authored-by: Abhishek Kumar <abhishek@a6k.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:08:25 +05:30
signup_enabled: bool
# Public Stack Auth client config — only populated when auth_provider == "stack".
# The UI reads these at runtime to initialize Stack, so they no longer need to
# be baked into the browser bundle at build time. Both are public values.
stack_project_id: str | None = None
stack_publishable_client_key: str | None = None
@router.get("/health", response_model=HealthResponse)
async def health() -> HealthResponse:
from api.constants import (
APP_VERSION,
AUTH_PROVIDER,
BACKEND_API_ENDPOINT,
DEPLOYMENT_MODE,
feat(auth): gate OSS signup behind ENABLE_SIGNUP flag (#514) * feat(auth): gate OSS signup behind ENABLE_SIGNUP flag ## Problem The `POST /api/v1/auth/signup` endpoint is unconditionally exposed on every OSS install. Operators running an invite-only deployment (private customer instances, staging environments, internal-only tenants) have no way to disable public account creation without patching the codebase. The UI also shows the "Sign up" link on `/auth/login` regardless of whether signup is available, so a locked-down deployment leaves broken navigation on the login page. ## Fix Introduce a single `ENABLE_SIGNUP` env var (default `true` — no behavior change for existing installs) that controls signup end-to-end: - **Backend** — `api/constants.ENABLE_SIGNUP` is read at module load. The signup handler returns 403 when it's false. Also exposed on `GET /api/v1/health` as `signup_enabled: bool` so the UI can mirror the operator's choice at runtime instead of at bundle-build time. - **UI** — `getSignupEnabled()` in `lib/auth/config.ts` proxies the health field, `/api/config/auth` surfaces it to the browser, the login page conditionally renders the "Sign up" link via a one-shot `fetch("/api/config/auth")` in `useEffect`, and the middleware redirects `/auth/signup` → `/auth/login` when disabled (fires before Next.js can serve the statically-prerendered signup page). - **Helm** — `config.enableSignup` (default `true`) is rendered into the ConfigMap as `ENABLE_SIGNUP` so operators can flip it via `--set config.enableSignup=false` at install/upgrade time. Fallbacks default to `signupEnabled: true` in every layer so a fresh install "just works" and matches the backend default. * address review: rollout on ConfigMap change, cache TTL, no signup-link flash Four review points on #514: **P1 — ConfigMap Change Skips Rollout** (`configmap.yaml`). `helm upgrade --set config.enableSignup=false` updated the ConfigMap but did NOT roll the api pods, so running processes kept the ENABLE_SIGNUP env from startup and continued serving the old signup behavior — including divergence between replicas mid-upgrade. Fix: add the standard `checksum/config` pod-template annotation on the four backend Deployments that `envFrom` the ConfigMap (`web`, `arq-worker`, `ari-manager`, `campaign-orchestrator`). Verified with `helm template`: all four Deployments share the same checksum on any given render, and flipping `config.enableSignup` changes the checksum uniformly so kubectl sees a pod-template diff and rolls all four. **P1 — Signup Flag Stays Cached (server)** (`ui/src/lib/auth/config.ts`). Module-scoped cache had no TTL. `revalidate: 300` was passed on the underlying `fetch()` but the in-memory short-circuit above ran first, so the value never refreshed until the UI pod restarted. Fix: add `AUTH_CONFIG_TTL_MS = 5 * 60 * 1000` (matching the fetch revalidate hint) so the module cache and the Next fetch cache stay in sync. Backend flag flips propagate within 5 minutes without a pod restart. **P1 — Middleware Redirect Uses Stale State** (`ui/src/middleware.ts`). Same shape as above — a separate module cache with no expiry could keep redirecting `/auth/signup → /auth/login` after signup was re-enabled, or keep serving the statically-prerendered signup page after lockdown. Fix: same `SERVER_CONFIG_TTL_MS = 5 * 60 * 1000` TTL on the middleware cache. **P2 — Signup link flash on login page** (`ui/src/app/auth/login/page.tsx`). Initial `signupEnabled` state was `null`, so `{signupEnabled && ...}` hid the link on first paint and it popped in after the fetch resolved — a CLS on every login-page load on stock installs where signup is enabled. Fix: initialise the state to `true` (matches the backend default). The fetch still overrides to `false` when the operator has actually disabled signup, so the lockdown UI behavior is unchanged; only the happy-path flash is gone. * simplify signup flag: drop TTL caches and middleware redirect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * resolve signup flag server-side to avoid signup link flicker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: prabhat pankaj <prabhatiitbhu@gmail.com> Co-authored-by: Abhishek Kumar <abhishek@a6k.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:08:25 +05:30
ENABLE_SIGNUP,
FORCE_TURN_RELAY,
STACK_AUTH_PROJECT_ID,
STACK_PUBLISHABLE_CLIENT_KEY,
TURN_SECRET,
)
from api.utils.common import get_backend_endpoints, is_local_or_private_url
2025-09-09 14:37:32 +05:30
logger.debug("Health endpoint called")
backend_endpoint, _ = await get_backend_endpoints()
# tunnel_url is set only when a Cloudflare tunnel was actually resolved: the
# configured address isn't publicly reachable, but get_backend_endpoints found
# a public tunnel URL for it. This is the URL the UI shows for inbound webhooks.
# It stays null for a directly-reachable (public IP / domain) deployment, where
# backend_api_endpoint itself is the public URL.
tunnel_url = (
backend_endpoint
if is_local_or_private_url(BACKEND_API_ENDPOINT)
and not is_local_or_private_url(backend_endpoint)
else None
)
is_stack = AUTH_PROVIDER == "stack"
return HealthResponse(
status="ok",
version=APP_VERSION,
backend_api_endpoint=BACKEND_API_ENDPOINT,
tunnel_url=tunnel_url,
deployment_mode=DEPLOYMENT_MODE,
auth_provider=AUTH_PROVIDER,
turn_enabled=bool(TURN_SECRET),
force_turn_relay=FORCE_TURN_RELAY,
feat(auth): gate OSS signup behind ENABLE_SIGNUP flag (#514) * feat(auth): gate OSS signup behind ENABLE_SIGNUP flag ## Problem The `POST /api/v1/auth/signup` endpoint is unconditionally exposed on every OSS install. Operators running an invite-only deployment (private customer instances, staging environments, internal-only tenants) have no way to disable public account creation without patching the codebase. The UI also shows the "Sign up" link on `/auth/login` regardless of whether signup is available, so a locked-down deployment leaves broken navigation on the login page. ## Fix Introduce a single `ENABLE_SIGNUP` env var (default `true` — no behavior change for existing installs) that controls signup end-to-end: - **Backend** — `api/constants.ENABLE_SIGNUP` is read at module load. The signup handler returns 403 when it's false. Also exposed on `GET /api/v1/health` as `signup_enabled: bool` so the UI can mirror the operator's choice at runtime instead of at bundle-build time. - **UI** — `getSignupEnabled()` in `lib/auth/config.ts` proxies the health field, `/api/config/auth` surfaces it to the browser, the login page conditionally renders the "Sign up" link via a one-shot `fetch("/api/config/auth")` in `useEffect`, and the middleware redirects `/auth/signup` → `/auth/login` when disabled (fires before Next.js can serve the statically-prerendered signup page). - **Helm** — `config.enableSignup` (default `true`) is rendered into the ConfigMap as `ENABLE_SIGNUP` so operators can flip it via `--set config.enableSignup=false` at install/upgrade time. Fallbacks default to `signupEnabled: true` in every layer so a fresh install "just works" and matches the backend default. * address review: rollout on ConfigMap change, cache TTL, no signup-link flash Four review points on #514: **P1 — ConfigMap Change Skips Rollout** (`configmap.yaml`). `helm upgrade --set config.enableSignup=false` updated the ConfigMap but did NOT roll the api pods, so running processes kept the ENABLE_SIGNUP env from startup and continued serving the old signup behavior — including divergence between replicas mid-upgrade. Fix: add the standard `checksum/config` pod-template annotation on the four backend Deployments that `envFrom` the ConfigMap (`web`, `arq-worker`, `ari-manager`, `campaign-orchestrator`). Verified with `helm template`: all four Deployments share the same checksum on any given render, and flipping `config.enableSignup` changes the checksum uniformly so kubectl sees a pod-template diff and rolls all four. **P1 — Signup Flag Stays Cached (server)** (`ui/src/lib/auth/config.ts`). Module-scoped cache had no TTL. `revalidate: 300` was passed on the underlying `fetch()` but the in-memory short-circuit above ran first, so the value never refreshed until the UI pod restarted. Fix: add `AUTH_CONFIG_TTL_MS = 5 * 60 * 1000` (matching the fetch revalidate hint) so the module cache and the Next fetch cache stay in sync. Backend flag flips propagate within 5 minutes without a pod restart. **P1 — Middleware Redirect Uses Stale State** (`ui/src/middleware.ts`). Same shape as above — a separate module cache with no expiry could keep redirecting `/auth/signup → /auth/login` after signup was re-enabled, or keep serving the statically-prerendered signup page after lockdown. Fix: same `SERVER_CONFIG_TTL_MS = 5 * 60 * 1000` TTL on the middleware cache. **P2 — Signup link flash on login page** (`ui/src/app/auth/login/page.tsx`). Initial `signupEnabled` state was `null`, so `{signupEnabled && ...}` hid the link on first paint and it popped in after the fetch resolved — a CLS on every login-page load on stock installs where signup is enabled. Fix: initialise the state to `true` (matches the backend default). The fetch still overrides to `false` when the operator has actually disabled signup, so the lockdown UI behavior is unchanged; only the happy-path flash is gone. * simplify signup flag: drop TTL caches and middleware redirect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * resolve signup flag server-side to avoid signup link flicker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: prabhat pankaj <prabhatiitbhu@gmail.com> Co-authored-by: Abhishek Kumar <abhishek@a6k.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:08:25 +05:30
signup_enabled=ENABLE_SIGNUP,
stack_project_id=STACK_AUTH_PROJECT_ID if is_stack else None,
stack_publishable_client_key=(
STACK_PUBLISHABLE_CLIENT_KEY if is_stack else None
),
)
class ActiveCallsResponse(BaseModel):
active_calls: int
DOGRAH_DEVOPS_SECRET_HEADER = "X-Dograh-Devops-Secret"
def _verify_devops_secret(
configured_secret: str | None,
provided_secret: str | None,
) -> None:
if not configured_secret:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Devops secret is not configured",
)
if not provided_secret or not secrets.compare_digest(
provided_secret,
configured_secret,
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden",
)
@router.get("/health/active-calls", response_model=ActiveCallsResponse)
async def active_calls(
x_dograh_devops_secret: Annotated[
str | None,
Header(alias=DOGRAH_DEVOPS_SECRET_HEADER),
] = None,
) -> ActiveCallsResponse:
"""In-flight call count for THIS worker — the drain signal for deploys.
A deploy orchestrator polls this per worker and waits for zero before
sending SIGTERM, because uvicorn force-closes live call WebSockets (close
code 1012) on SIGTERM and would cut calls mid-conversation otherwise. The
count is per-process: one uvicorn per VM port (scripts/rolling_update.sh)
or per Kubernetes pod (preStop hook). See api/services/pipecat/active_calls.py.
"""
from api.constants import DOGRAH_DEVOPS_SECRET
from api.services.pipecat.active_calls import active_call_count
_verify_devops_secret(DOGRAH_DEVOPS_SECRET, x_dograh_devops_secret)
return ActiveCallsResponse(active_calls=active_call_count())