diff --git a/api/alembic/versions/6fd8fac02883_add_user_email_and_password.py b/api/alembic/versions/6fd8fac02883_add_user_email_and_password.py
new file mode 100644
index 0000000..8986547
--- /dev/null
+++ b/api/alembic/versions/6fd8fac02883_add_user_email_and_password.py
@@ -0,0 +1,34 @@
+"""add user email and password
+
+Revision ID: 6fd8fac02883
+Revises: 6d2f94baf4b7
+Create Date: 2026-02-20 11:43:47.679075
+
+"""
+
+from typing import Sequence, Union
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "6fd8fac02883"
+down_revision: Union[str, None] = "6d2f94baf4b7"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column("users", sa.Column("email", sa.String(), nullable=True))
+ op.add_column("users", sa.Column("password_hash", sa.String(), nullable=True))
+ op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f("ix_users_email"), table_name="users")
+ op.drop_column("users", "password_hash")
+ op.drop_column("users", "email")
+ # ### end Alembic commands ###
diff --git a/api/constants.py b/api/constants.py
index 6f55e8d..e856116 100644
--- a/api/constants.py
+++ b/api/constants.py
@@ -25,6 +25,7 @@ DATABASE_URL = os.environ["DATABASE_URL"]
REDIS_URL = os.environ["REDIS_URL"]
DEPLOYMENT_MODE = os.getenv("DEPLOYMENT_MODE", "oss")
+AUTH_PROVIDER = os.getenv("AUTH_PROVIDER", "local")
DOGRAH_MPS_SECRET_KEY = os.getenv("DOGRAH_MPS_SECRET_KEY", None)
MPS_API_URL = os.getenv("MPS_API_URL", "https://services.dograh.com")
@@ -118,3 +119,7 @@ TURN_HOST = os.getenv("TURN_HOST", "localhost")
TURN_PORT = int(os.getenv("TURN_PORT", "3478"))
TURN_TLS_PORT = int(os.getenv("TURN_TLS_PORT", "5349"))
TURN_CREDENTIAL_TTL = int(os.getenv("TURN_CREDENTIAL_TTL", "86400"))
+
+# OSS Email/Password Auth
+OSS_JWT_SECRET = os.getenv("OSS_JWT_SECRET", "change-me-in-production")
+OSS_JWT_EXPIRY_HOURS = int(os.getenv("OSS_JWT_EXPIRY_HOURS", "720")) # 30 days
diff --git a/api/db/models.py b/api/db/models.py
index 409db0c..4a752d4 100644
--- a/api/db/models.py
+++ b/api/db/models.py
@@ -69,6 +69,8 @@ class UserModel(Base):
back_populates="users",
)
is_superuser = Column(Boolean, default=False)
+ email = Column(String, unique=True, index=True, nullable=True)
+ password_hash = Column(String, nullable=True)
class UserConfigurationModel(Base):
diff --git a/api/db/user_client.py b/api/db/user_client.py
index ab57f50..6c98164 100644
--- a/api/db/user_client.py
+++ b/api/db/user_client.py
@@ -1,3 +1,4 @@
+import uuid
from datetime import datetime, timezone
from loguru import logger
@@ -148,3 +149,35 @@ class UserClient(BaseDBClient):
raise ValueError(f"User with ID {user_id} not found")
await session.commit()
+
+ async def update_user_email(self, user_id: int, email: str) -> None:
+ """Update the user's email address."""
+ async with self.async_session() as session:
+ from sqlalchemy import update
+
+ stmt = update(UserModel).where(UserModel.id == user_id).values(email=email)
+ await session.execute(stmt)
+ await session.commit()
+
+ async def get_user_by_email(self, email: str) -> UserModel | None:
+ """Fetch a user by their email address."""
+ async with self.async_session() as session:
+ result = await session.execute(
+ select(UserModel).where(UserModel.email == email)
+ )
+ return result.scalars().first()
+
+ async def create_user_with_email(
+ self, email: str, password_hash: str, name: str | None = None
+ ) -> UserModel:
+ """Create a new user with email and password hash."""
+ async with self.async_session() as session:
+ user = UserModel(
+ provider_id=f"oss_{int(datetime.now(timezone.utc).timestamp())}_{uuid.uuid4()}",
+ email=email,
+ password_hash=password_hash,
+ )
+ session.add(user)
+ await session.commit()
+ await session.refresh(user)
+ return user
diff --git a/api/requirements.txt b/api/requirements.txt
index 2aca2b6..8852613 100644
--- a/api/requirements.txt
+++ b/api/requirements.txt
@@ -15,3 +15,5 @@ sqlalchemy[asyncio]==2.0.43
msgpack==1.1.2
docling[rapidocr]==2.68.0
pgvector==0.4.2
+bcrypt==5.0.0
+email-validator==2.3.0
diff --git a/api/routes/auth.py b/api/routes/auth.py
new file mode 100644
index 0000000..9f22ee5
--- /dev/null
+++ b/api/routes/auth.py
@@ -0,0 +1,97 @@
+from fastapi import APIRouter, Depends, HTTPException
+from loguru import logger
+
+from api.db import db_client
+from api.db.models import UserModel
+from api.schemas.auth import AuthResponse, LoginRequest, SignupRequest, UserResponse
+from api.services.auth.depends import create_user_configuration_with_mps_key, get_user
+from api.utils.auth import create_jwt_token, hash_password, verify_password
+
+router = APIRouter(
+ prefix="/auth",
+ tags=["auth"],
+)
+
+
+@router.post("/signup", response_model=AuthResponse)
+async def signup(request: SignupRequest):
+ # Check if email is already taken
+ existing_user = await db_client.get_user_by_email(request.email)
+ if existing_user:
+ raise HTTPException(status_code=409, detail="Email already registered")
+
+ # Hash password and create user
+ hashed = hash_password(request.password)
+ user = await db_client.create_user_with_email(
+ email=request.email,
+ password_hash=hashed,
+ name=request.name,
+ )
+
+ # Create organization for the user
+ org_provider_id = f"org_{user.provider_id}"
+ organization, _ = await db_client.get_or_create_organization_by_provider_id(
+ org_provider_id=org_provider_id, user_id=user.id
+ )
+
+ # Link user to organization
+ await db_client.add_user_to_organization(user.id, organization.id)
+ await db_client.update_user_selected_organization(user.id, organization.id)
+
+ # Create default service configuration
+ try:
+ mps_config = await create_user_configuration_with_mps_key(
+ user.id, organization.id, user.provider_id
+ )
+ if mps_config:
+ await db_client.update_user_configuration(user.id, mps_config)
+ except Exception:
+ logger.warning(
+ "Failed to create default configuration for OSS user", exc_info=True
+ )
+
+ # Create JWT token
+ token = create_jwt_token(user.id, request.email)
+
+ return AuthResponse(
+ token=token,
+ user=UserResponse(
+ id=user.id,
+ email=user.email,
+ name=request.name,
+ organization_id=organization.id,
+ ),
+ )
+
+
+@router.post("/login", response_model=AuthResponse)
+async def login(request: LoginRequest):
+ # Look up user by email
+ user = await db_client.get_user_by_email(request.email)
+ if not user or not user.password_hash:
+ raise HTTPException(status_code=401, detail="Invalid email or password")
+
+ # Verify password
+ if not verify_password(request.password, user.password_hash):
+ raise HTTPException(status_code=401, detail="Invalid email or password")
+
+ # Create JWT token
+ token = create_jwt_token(user.id, user.email)
+
+ return AuthResponse(
+ token=token,
+ user=UserResponse(
+ id=user.id,
+ email=user.email,
+ organization_id=user.selected_organization_id,
+ ),
+ )
+
+
+@router.get("/me", response_model=UserResponse)
+async def get_current_user(user: UserModel = Depends(get_user)):
+ return UserResponse(
+ id=user.id,
+ email=user.email,
+ organization_id=user.selected_organization_id,
+ )
diff --git a/api/routes/main.py b/api/routes/main.py
index a53e8aa..cb86a71 100644
--- a/api/routes/main.py
+++ b/api/routes/main.py
@@ -2,6 +2,7 @@ from fastapi import APIRouter
from loguru import logger
from pydantic import BaseModel
+from api.routes.auth import router as auth_router
from api.routes.campaign import router as campaign_router
from api.routes.credentials import router as credentials_router
from api.routes.integration import router as integration_router
@@ -50,17 +51,20 @@ 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(auth_router)
class HealthResponse(BaseModel):
status: str
version: str
backend_api_endpoint: str
+ deployment_mode: str
+ auth_provider: str
@router.get("/health", response_model=HealthResponse)
async def health() -> HealthResponse:
- from api.constants import APP_VERSION
+ from api.constants import APP_VERSION, AUTH_PROVIDER, DEPLOYMENT_MODE
from api.utils.common import get_backend_endpoints
logger.debug("Health endpoint called")
@@ -69,4 +73,6 @@ async def health() -> HealthResponse:
status="ok",
version=APP_VERSION,
backend_api_endpoint=backend_endpoint,
+ deployment_mode=DEPLOYMENT_MODE,
+ auth_provider=AUTH_PROVIDER,
)
diff --git a/api/schemas/auth.py b/api/schemas/auth.py
new file mode 100644
index 0000000..467940f
--- /dev/null
+++ b/api/schemas/auth.py
@@ -0,0 +1,31 @@
+from pydantic import BaseModel, EmailStr, field_validator
+
+
+class SignupRequest(BaseModel):
+ email: EmailStr
+ password: str
+ name: str | None = None
+
+ @field_validator("password")
+ @classmethod
+ def password_min_length(cls, v: str) -> str:
+ if len(v) < 8:
+ raise ValueError("Password must be at least 8 characters")
+ return v
+
+
+class LoginRequest(BaseModel):
+ email: EmailStr
+ password: str
+
+
+class UserResponse(BaseModel):
+ id: int
+ email: str | None
+ name: str | None = None
+ organization_id: int | None = None
+
+
+class AuthResponse(BaseModel):
+ token: str
+ user: UserResponse
diff --git a/api/services/auth/depends.py b/api/services/auth/depends.py
index 79a3fcd..1470e27 100644
--- a/api/services/auth/depends.py
+++ b/api/services/auth/depends.py
@@ -5,12 +5,13 @@ from fastapi import Header, HTTPException, Query, WebSocket
from loguru import logger
from pydantic import ValidationError
-from api.constants import DEPLOYMENT_MODE, DOGRAH_MPS_SECRET_KEY, MPS_API_URL
+from api.constants import AUTH_PROVIDER, DOGRAH_MPS_SECRET_KEY, MPS_API_URL
from api.db import db_client
from api.db.models import UserModel
from api.schemas.user_configuration import UserConfiguration
from api.services.auth.stack_auth import stackauth
from api.services.configuration.registry import ServiceProviders
+from api.utils.auth import decode_jwt_token
async def get_user(
@@ -24,9 +25,9 @@ async def get_user(
return await _handle_api_key_auth(x_api_key)
# ------------------------------------------------------------------
- # Check if we're in OSS deployment mode
+ # Check if we're using local (email/password) auth
# ------------------------------------------------------------------
- if DEPLOYMENT_MODE == "oss":
+ if AUTH_PROVIDER == "local":
return await _handle_oss_auth(authorization)
# ------------------------------------------------------------------
@@ -54,6 +55,14 @@ async def get_user(
try:
user_model = await db_client.get_or_create_user_by_provider_id(stack_user["id"])
+
+ # Sync email from Stack Auth if available and not already set
+ stack_email = stack_user.get("primary_email_verified") and stack_user.get(
+ "primary_email"
+ )
+ if stack_email and user_model.email != stack_email:
+ await db_client.update_user_email(user_model.id, stack_email)
+ user_model.email = stack_email
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Error while creating user from database {e}"
@@ -125,7 +134,7 @@ async def get_user_optional(
async def _handle_oss_auth(authorization: str | None) -> UserModel:
"""
Handle authentication for OSS deployment mode.
- Uses the authorization token as provider_id and creates user/org if needed.
+ Validates JWT tokens issued by the email/password auth flow.
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header required")
@@ -141,49 +150,15 @@ async def _handle_oss_auth(authorization: str | None) -> UserModel:
raise HTTPException(status_code=401, detail="Invalid authorization token")
try:
- # Use token as provider_id for OSS mode
- user_model = await db_client.get_or_create_user_by_provider_id(
- provider_id=token
- )
-
- # Create or get organization for OSS user
- # Each OSS user gets their own organization using their token as org ID
- (
- organization,
- org_was_created,
- ) = await db_client.get_or_create_organization_by_provider_id(
- org_provider_id=f"org_{token}", user_id=user_model.id
- )
-
- # Ensure user is mapped to their organization
- if user_model.selected_organization_id != organization.id:
- # add_user_to_organization now handles race conditions with ON CONFLICT DO NOTHING
- await db_client.add_user_to_organization(user_model.id, organization.id)
- await db_client.update_user_selected_organization(
- user_model.id, organization.id
- )
- user_model.selected_organization_id = organization.id
-
- # Only create default configuration if organization was just created
- # This prevents race conditions where multiple concurrent requests
- # might try to create configurations
- if org_was_created:
- existing_cfg = await db_client.get_user_configurations(user_model.id)
- if not (existing_cfg.llm or existing_cfg.tts or existing_cfg.stt):
- mps_config = await create_user_configuration_with_mps_key(
- user_model.id, organization.id, token
- )
- if mps_config:
- await db_client.update_user_configuration(
- user_model.id, mps_config
- )
-
- return user_model
-
- except Exception as e:
- raise HTTPException(
- status_code=500, detail=f"Error while handling OSS authentication: {e}"
- )
+ payload = decode_jwt_token(token)
+ user = await db_client.get_user_by_id(int(payload["sub"]))
+ if user:
+ return user
+ raise HTTPException(status_code=401, detail="User not found")
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=401, detail="Invalid or expired token")
async def _handle_api_key_auth(api_key: str) -> UserModel:
@@ -233,8 +208,8 @@ async def create_user_configuration_with_mps_key(
async with httpx.AsyncClient() as client:
# Use MPS API URL from constants
- if DEPLOYMENT_MODE == "oss":
- # For OSS mode, create a temporary service key without authentication
+ if AUTH_PROVIDER == "local":
+ # For local auth mode, create a temporary service key without authentication
response = await client.post(
f"{MPS_API_URL}/api/v1/service-keys/",
json={
diff --git a/api/utils/auth.py b/api/utils/auth.py
new file mode 100644
index 0000000..6a9298c
--- /dev/null
+++ b/api/utils/auth.py
@@ -0,0 +1,28 @@
+from datetime import UTC, datetime, timedelta
+
+import bcrypt
+import jwt
+
+from api.constants import OSS_JWT_EXPIRY_HOURS, OSS_JWT_SECRET
+
+
+def hash_password(password: str) -> str:
+ return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
+
+
+def verify_password(password: str, password_hash: str) -> bool:
+ return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("utf-8"))
+
+
+def create_jwt_token(user_id: int, email: str) -> str:
+ payload = {
+ "sub": str(user_id),
+ "email": email,
+ "exp": datetime.now(UTC) + timedelta(hours=OSS_JWT_EXPIRY_HOURS),
+ "iat": datetime.now(UTC),
+ }
+ return jwt.encode(payload, OSS_JWT_SECRET, algorithm="HS256")
+
+
+def decode_jwt_token(token: str) -> dict:
+ return jwt.decode(token, OSS_JWT_SECRET, algorithms=["HS256"])
diff --git a/api/utils/common.py b/api/utils/common.py
index b843d13..2770c5b 100644
--- a/api/utils/common.py
+++ b/api/utils/common.py
@@ -119,10 +119,6 @@ async def get_backend_endpoints() -> tuple[str, str]:
_validate_url(BACKEND_API_ENDPOINT)
if BACKEND_API_ENDPOINT:
- logger.debug(
- f"Processing BACKEND_API_ENDPOINT from environment: {BACKEND_API_ENDPOINT}"
- )
-
# Handle localhost/127.0.0.1 special case - use tunnel URL if available
if "localhost" in BACKEND_API_ENDPOINT or "127.0.0.1" in BACKEND_API_ENDPOINT:
logger.debug(
diff --git a/api/utils/tunnel.py b/api/utils/tunnel.py
index 8e2a521..930d7a2 100644
--- a/api/utils/tunnel.py
+++ b/api/utils/tunnel.py
@@ -27,7 +27,6 @@ class TunnelURLProvider:
# Try to get URL from cloudflared metrics
urls = await cls._get_cloudflared_urls()
if urls:
- logger.info(f"Retrieved tunnel URLs from cloudflared: {urls}")
return urls
except Exception as e:
logger.warning(f"Failed to get tunnel URL from cloudflared: {e}")
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 21402f7..e02384d 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -103,6 +103,9 @@ services:
MINIO_BUCKET: "voice-audio"
MINIO_SECURE: "false"
+ # FastAPI workers count
+ FASTAPI_WORKERS: 1
+
# Langfuse
ENABLE_TRACING: "false"
# LANGFUSE_SECRET_KEY: ""
@@ -118,6 +121,8 @@ services:
TURN_HOST: "${TURN_HOST:-}"
TURN_SECRET: "${TURN_SECRET:-}"
+ OSS_JWT_SECRET: "${OSS_JWT_SECRET:-ChangeMeInProduction}"
+
ports:
- "8000:8000"
depends_on:
@@ -148,7 +153,6 @@ services:
# Server-side URL (SSR, internal Docker network)
BACKEND_URL: "http://api:8000"
NODE_ENV: "oss"
-
# Flag to enable/ disable posthog and sentry
ENABLE_TELEMETRY: "${ENABLE_TELEMETRY:-true}"
@@ -214,4 +218,4 @@ volumes:
networks:
app-network:
- driver: bridge
\ No newline at end of file
+ driver: bridge
diff --git a/scripts/setup_remote.sh b/scripts/setup_remote.sh
index d1e71c5..a734fc6 100755
--- a/scripts/setup_remote.sh
+++ b/scripts/setup_remote.sh
@@ -167,6 +167,8 @@ TURN_EOF
echo -e "${GREEN}✓ turnserver.conf created${NC}"
echo -e "${BLUE}[6/6] Creating environment file...${NC}"
+OSS_JWT_SECRET=$(openssl rand -hex 32)
+
cat > .env << ENV_EOF
# Backend API endpoint (for remote deployment)
BACKEND_API_ENDPOINT=https://$SERVER_IP
@@ -175,6 +177,9 @@ BACKEND_API_ENDPOINT=https://$SERVER_IP
TURN_HOST=$SERVER_IP
TURN_SECRET=$TURN_SECRET
+# JWT secret for OSS authentication
+OSS_JWT_SECRET=$OSS_JWT_SECRET
+
# Telemetry (set to false to disable)
ENABLE_TELEMETRY=true
ENV_EOF
diff --git a/ui/.env.example b/ui/.env.example
index 284108b..e6e901e 100644
--- a/ui/.env.example
+++ b/ui/.env.example
@@ -1,6 +1,2 @@
-NEXT_PUBLIC_BACKEND_URL=http://localhost:8000
BACKEND_URL=http://localhost:8000
-
NEXT_PUBLIC_NODE_ENV=development
-NEXT_PUBLIC_DEPLOYMENT_MODE: "oss"
-NEXT_PUBLIC_AUTH_PROVIDER="local"
diff --git a/ui/Dockerfile b/ui/Dockerfile
index 91acdb5..314cea7 100644
--- a/ui/Dockerfile
+++ b/ui/Dockerfile
@@ -37,12 +37,10 @@ COPY ui/src ./src
# Set build-time environment variables (needed for Next.js build)
ENV NEXT_PUBLIC_NODE_ENV="oss"
-ENV NEXT_PUBLIC_AUTH_PROVIDER="local"
-ENV NEXT_PUBLIC_DEPLOYMENT_MODE="oss"
-ENV BACKEND_URL="http://api:8000"
ENV NEXT_TELEMETRY_DISABLED="1"
ENV NEXT_PUBLIC_CHATWOOT_URL="https://chat.dograh.com"
ENV NEXT_PUBLIC_CHATWOOT_TOKEN="3fkFx2mCEjNHjM9gaNc4A82X"
+ENV BACKEND_URL="http://api:8000"
# Build the application with standalone mode
# Increase Node.js heap size to prevent out-of-memory errors during build
diff --git a/ui/src/app/after-sign-in/page.tsx b/ui/src/app/after-sign-in/page.tsx
index 39796e9..90a886c 100644
--- a/ui/src/app/after-sign-in/page.tsx
+++ b/ui/src/app/after-sign-in/page.tsx
@@ -1,3 +1,4 @@
+import { isNextRouterError } from "next/dist/client/components/is-next-router-error";
import { redirect } from "next/navigation";
import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen";
@@ -9,7 +10,7 @@ export const dynamic = 'force-dynamic';
export default async function AfterSignInPage() {
logger.debug('[AfterSignInPage] Starting after-sign-in page');
- const authProvider = getServerAuthProvider();
+ const authProvider = await getServerAuthProvider();
logger.debug('[AfterSignInPage] Auth provider:', authProvider);
logger.debug('[AfterSignInPage] Getting server user...');
const user = await getServerUser();
@@ -54,6 +55,9 @@ export default async function AfterSignInPage() {
}
}
} catch (error) {
+ if (isNextRouterError(error)) {
+ throw error;
+ }
logger.error('[AfterSignInPage] Error checking workflows:', error);
}
diff --git a/ui/src/app/api-keys/page.tsx b/ui/src/app/api-keys/page.tsx
index 61aefaf..2558889 100644
--- a/ui/src/app/api-keys/page.tsx
+++ b/ui/src/app/api-keys/page.tsx
@@ -20,12 +20,14 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
+import { useAppConfig } from '@/context/AppConfigContext';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
-import { isOSSMode } from '@/lib/utils';
export default function APIKeysPage() {
const { user, getAccessToken, redirectToLogin, loading } = useAuth();
+ const { config } = useAppConfig();
+ const isOSS = config?.deploymentMode === 'oss';
logger.debug('[APIKeysPage] Component render', {
loading,
@@ -313,8 +315,8 @@ export default function APIKeysPage() {
// In OSS mode, check if there's already an active service key
const activeServiceKeys = serviceKeys.filter(key => !key.archived_at);
- const canCreateServiceKey = !isOSSMode() || activeServiceKeys.length === 0;
- const showServiceKeyArchiveControls = !isOSSMode();
+ const canCreateServiceKey = !isOSS || activeServiceKeys.length === 0;
+ const showServiceKeyArchiveControls = !isOSS;
return (
diff --git a/ui/src/app/api/auth/logout/route.ts b/ui/src/app/api/auth/logout/route.ts
new file mode 100644
index 0000000..be7267f
--- /dev/null
+++ b/ui/src/app/api/auth/logout/route.ts
@@ -0,0 +1,27 @@
+import { cookies } from 'next/headers';
+import { NextResponse } from 'next/server';
+
+const OSS_TOKEN_COOKIE = 'dograh_auth_token';
+const OSS_USER_COOKIE = 'dograh_auth_user';
+
+export async function POST() {
+ const cookieStore = await cookies();
+
+ cookieStore.set(OSS_TOKEN_COOKIE, '', {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ maxAge: 0,
+ path: '/',
+ });
+
+ cookieStore.set(OSS_USER_COOKIE, '', {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ maxAge: 0,
+ path: '/',
+ });
+
+ return NextResponse.json({ success: true });
+}
diff --git a/ui/src/app/api/auth/oss/route.ts b/ui/src/app/api/auth/oss/route.ts
index 6679fb4..6436b6c 100644
--- a/ui/src/app/api/auth/oss/route.ts
+++ b/ui/src/app/api/auth/oss/route.ts
@@ -1,19 +1,18 @@
/*
Provides authentication token to LocalProviderWrapper once loaded
- in the browser
+ in the browser.
+ Returns 401 if no token cookie exists (user needs to log in).
*/
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
-const OSS_TOKEN_COOKIE = 'dograh_oss_token';
-const OSS_USER_COOKIE = 'dograh_oss_user';
+import { getAuthProvider } from '@/lib/auth/config';
-function generateOSSToken(): string {
- return `oss_${Date.now()}_${crypto.randomUUID()}`;
-}
+const OSS_TOKEN_COOKIE = 'dograh_auth_token';
+const OSS_USER_COOKIE = 'dograh_auth_user';
export async function GET() {
- const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
+ const authProvider = await getAuthProvider();
// Only handle OSS mode
if (authProvider !== 'local') {
@@ -21,40 +20,17 @@ export async function GET() {
}
const cookieStore = await cookies();
- let token = cookieStore.get(OSS_TOKEN_COOKIE)?.value;
- let user = cookieStore.get(OSS_USER_COOKIE)?.value;
+ const token = cookieStore.get(OSS_TOKEN_COOKIE)?.value;
+ const user = cookieStore.get(OSS_USER_COOKIE)?.value;
- // If no token exists, create one
+ // If no token exists, return 401 (user needs to sign up or log in)
if (!token) {
- token = generateOSSToken();
- user = JSON.stringify({
- id: token,
- name: 'Local User',
- provider: 'local',
- organizationId: `org_${token}`,
- });
-
- // Set cookies
- cookieStore.set(OSS_TOKEN_COOKIE, token, {
- httpOnly: true,
- secure: process.env.NODE_ENV === 'production',
- sameSite: 'lax',
- maxAge: 60 * 60 * 24 * 30, // 30 days
- path: '/',
- });
-
- cookieStore.set(OSS_USER_COOKIE, user, {
- httpOnly: true,
- secure: process.env.NODE_ENV === 'production',
- sameSite: 'lax',
- maxAge: 60 * 60 * 24 * 30, // 30 days
- path: '/',
- });
+ return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
- // Return the auth info as JSON (safe to expose to client)
+ // Return the auth info as JSON
return NextResponse.json({
token,
- user: JSON.parse(user!),
+ user: user ? JSON.parse(user) : { id: token, name: 'Local User', provider: 'local' },
});
}
diff --git a/ui/src/app/api/auth/session/route.ts b/ui/src/app/api/auth/session/route.ts
new file mode 100644
index 0000000..fbc35ca
--- /dev/null
+++ b/ui/src/app/api/auth/session/route.ts
@@ -0,0 +1,33 @@
+import { cookies } from 'next/headers';
+import { NextRequest, NextResponse } from 'next/server';
+
+const OSS_TOKEN_COOKIE = 'dograh_auth_token';
+const OSS_USER_COOKIE = 'dograh_auth_user';
+
+export async function POST(request: NextRequest) {
+ const { token, user } = await request.json();
+
+ if (!token) {
+ return NextResponse.json({ error: 'Missing token' }, { status: 400 });
+ }
+
+ const cookieStore = await cookies();
+
+ cookieStore.set(OSS_TOKEN_COOKIE, token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ maxAge: 60 * 60 * 24 * 30,
+ path: '/',
+ });
+
+ cookieStore.set(OSS_USER_COOKIE, JSON.stringify(user), {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ maxAge: 60 * 60 * 24 * 30,
+ path: '/',
+ });
+
+ return NextResponse.json({ success: true });
+}
diff --git a/ui/src/app/api/config/auth/route.ts b/ui/src/app/api/config/auth/route.ts
new file mode 100644
index 0000000..f0a8d96
--- /dev/null
+++ b/ui/src/app/api/config/auth/route.ts
@@ -0,0 +1,8 @@
+import { NextResponse } from 'next/server';
+
+import { getAuthProvider } from '@/lib/auth/config';
+
+export async function GET() {
+ const provider = await getAuthProvider();
+ return NextResponse.json({ provider });
+}
diff --git a/ui/src/app/api/config/version/route.ts b/ui/src/app/api/config/version/route.ts
index 241085f..a039360 100644
--- a/ui/src/app/api/config/version/route.ts
+++ b/ui/src/app/api/config/version/route.ts
@@ -6,28 +6,50 @@ import type { HealthResponse } from "@/client/types.gen";
// Import version from package.json at build time
import packageJson from "../../../../../package.json";
+// Internal/local URLs that are not reachable from the browser
+const INTERNAL_HOST_RE = /^https?:\/\/(localhost|127\.0\.0\.1|api)(:\d+)?(\/|$)/;
+
+function isInternalUrl(url: string | undefined | null): boolean {
+ return !url || INTERNAL_HOST_RE.test(url);
+}
+
export async function GET() {
const uiVersion = packageJson.version || "dev";
// Fetch backend version and config from health endpoint
let apiVersion = "unknown";
let backendApiEndpoint: string | null = null;
+ let deploymentMode = "oss";
+ let authProvider = "local";
try {
const response = await healthApiV1HealthGet();
if (response.data) {
const data = response.data as HealthResponse;
apiVersion = data.version;
+ // Pass through the backend's own endpoint for display purposes
backendApiEndpoint = data.backend_api_endpoint;
+ deploymentMode = data.deployment_mode;
+ authProvider = data.auth_provider;
}
} catch {
// Backend might not be reachable during build or in some deployments
apiVersion = "unavailable";
}
+ // For the API client base URL: prefer BACKEND_URL env, fall back to
+ // health endpoint value. Skip internal/Docker-only URLs (e.g. http://api:8000)
+ // that aren't reachable from the browser — the client will keep using
+ // window.location.origin via the Next.js proxy instead.
+ const clientCandidate = process.env.BACKEND_URL || backendApiEndpoint;
+ const clientApiBaseUrl = isInternalUrl(clientCandidate) ? 'http://localhost:8000' : clientCandidate;
+
return NextResponse.json({
ui: uiVersion,
api: apiVersion,
backendApiEndpoint,
+ clientApiBaseUrl,
+ deploymentMode,
+ authProvider,
});
}
diff --git a/ui/src/app/auth/login/page.tsx b/ui/src/app/auth/login/page.tsx
new file mode 100644
index 0000000..39c6ceb
--- /dev/null
+++ b/ui/src/app/auth/login/page.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+import { toast } from "sonner";
+
+import { loginApiV1AuthLoginPost } from "@/client/sdk.gen";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export default function LoginPage() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+
+ try {
+ const res = await loginApiV1AuthLoginPost({
+ body: { email, password },
+ });
+
+ if (res.error || !res.data) {
+ const detail = (res.error as { detail?: string })?.detail;
+ toast.error(detail || "Login failed");
+ return;
+ }
+
+ // Set httpOnly cookies via server route
+ await fetch("/api/auth/session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token: res.data.token, user: res.data.user }),
+ });
+
+ window.location.href = "/after-sign-in";
+ } catch {
+ toast.error("An error occurred. Please try again.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Sign in
+ Enter your email and password to continue
+
+
+
+
+ Don't have an account?{" "}
+
+ Sign up
+
+
+
+
+
+ );
+}
diff --git a/ui/src/app/auth/signup/page.tsx b/ui/src/app/auth/signup/page.tsx
new file mode 100644
index 0000000..d9d98c1
--- /dev/null
+++ b/ui/src/app/auth/signup/page.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+import { toast } from "sonner";
+
+import { signupApiV1AuthSignupPost } from "@/client/sdk.gen";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export default function SignupPage() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (password.length < 8) {
+ toast.error("Password must be at least 8 characters");
+ return;
+ }
+
+ if (password !== confirmPassword) {
+ toast.error("Passwords do not match");
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ const res = await signupApiV1AuthSignupPost({
+ body: { email, password },
+ });
+
+ if (res.error || !res.data) {
+ const detail = (res.error as { detail?: string })?.detail;
+ toast.error(detail || "Signup failed");
+ return;
+ }
+
+ // Set httpOnly cookies via server route
+ await fetch("/api/auth/session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token: res.data.token, user: res.data.user }),
+ });
+
+ window.location.href = "/after-sign-in";
+ } catch {
+ toast.error("An error occurred. Please try again.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Create an account
+ Enter your details to get started
+
+
+
+
+ Already have an account?{" "}
+
+ Sign in
+
+
+
+
+
+ );
+}
diff --git a/ui/src/app/handler/[...stack]/page.tsx b/ui/src/app/handler/[...stack]/page.tsx
index 0c0a5fe..486d1fd 100644
--- a/ui/src/app/handler/[...stack]/page.tsx
+++ b/ui/src/app/handler/[...stack]/page.tsx
@@ -1,12 +1,11 @@
import { StackHandler } from "@stackframe/stack";
-import { stackServerApp } from "../../../stack";
+import { getAuthProvider } from "@/lib/auth/config";
-const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER;
+export default async function Handler(props: unknown) {
+ const authProvider = await getAuthProvider();
-export default function Handler(props: unknown) {
if (authProvider === "local") {
- // Return a simple message when using local auth
return (
Local Auth Mode
@@ -14,9 +13,14 @@ export default function Handler(props: unknown) {
);
}
+
+ // Lazily import the real StackServerApp only when needed
+ const { getStackServerApp } = await import("@/lib/auth/server");
+ const app = await getStackServerApp();
+
return
;
}
diff --git a/ui/src/app/integrations/page.tsx b/ui/src/app/integrations/page.tsx
index 264b7fd..1277833 100644
--- a/ui/src/app/integrations/page.tsx
+++ b/ui/src/app/integrations/page.tsx
@@ -11,7 +11,7 @@ export const dynamic = 'force-dynamic';
// Server component for integration list
async function IntegrationList() {
- const authProvider = getServerAuthProvider();
+ const authProvider = await getServerAuthProvider();
const accessToken = await getServerAccessToken();
if (!accessToken) {
diff --git a/ui/src/app/looptalk/[id]/page.tsx b/ui/src/app/looptalk/[id]/page.tsx
index cd76eb4..8bbfff7 100644
--- a/ui/src/app/looptalk/[id]/page.tsx
+++ b/ui/src/app/looptalk/[id]/page.tsx
@@ -21,7 +21,7 @@ interface PageProps {
}
async function PageContent({ params }: PageProps) {
- const authProvider = getServerAuthProvider();
+ const authProvider = await getServerAuthProvider();
const accessToken = await getServerAccessToken();
if (!accessToken) {
diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx
index 60a175f..e0f8107 100644
--- a/ui/src/app/page.tsx
+++ b/ui/src/app/page.tsx
@@ -11,7 +11,7 @@ export const dynamic = 'force-dynamic';
export default async function Home() {
logger.debug('[HomePage] Starting Home page render');
- const authProvider = getServerAuthProvider();
+ const authProvider = await getServerAuthProvider();
logger.debug('[HomePage] Auth provider:', authProvider);
// For local/OSS provider, check if user has workflows
@@ -39,6 +39,8 @@ export default async function Home() {
logger.debug('[HomePage] Redirecting to /workflow/create - no workflows found');
redirect('/workflow/create');
}
+ } else {
+ redirect('/auth/login');
}
} catch (error) {
// Re-throw navigation errors (redirects, not found, etc.) - they're intentional
diff --git a/ui/src/app/workflow/page.tsx b/ui/src/app/workflow/page.tsx
index 36dc683..a5f9353 100644
--- a/ui/src/app/workflow/page.tsx
+++ b/ui/src/app/workflow/page.tsx
@@ -13,7 +13,7 @@ export const dynamic = 'force-dynamic';
// Server component for workflow list
async function WorkflowList() {
- const authProvider = getServerAuthProvider();
+ const authProvider = await getServerAuthProvider();
const accessToken = await getServerAccessToken();
if (!accessToken) {
diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts
index 339e0bc..9fe7253 100644
--- a/ui/src/client/sdk.gen.ts
+++ b/ui/src/client/sdk.gen.ts
@@ -3,7 +3,7 @@
import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch';
import { client as _heyApiClient } from './client.gen';
-import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostData, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetData, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetError, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetTurnCredentialsApiV1TurnCredentialsGetData, GetTurnCredentialsApiV1TurnCredentialsGetError, GetTurnCredentialsApiV1TurnCredentialsGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, InitiateCallTransferApiV1TelephonyCallTransferPostData, InitiateCallTransferApiV1TelephonyCallTransferPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCampaignApiV1CampaignCampaignIdPatchData, UpdateCampaignApiV1CampaignCampaignIdPatchError, UpdateCampaignApiV1CampaignCampaignIdPatchResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen';
+import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostData, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetCurrentUserApiV1AuthMeGetData, GetCurrentUserApiV1AuthMeGetError, GetCurrentUserApiV1AuthMeGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetData, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetError, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetTurnCredentialsApiV1TurnCredentialsGetData, GetTurnCredentialsApiV1TurnCredentialsGetError, GetTurnCredentialsApiV1TurnCredentialsGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, InitiateCallTransferApiV1TelephonyCallTransferPostData, InitiateCallTransferApiV1TelephonyCallTransferPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, SignupApiV1AuthSignupPostData, SignupApiV1AuthSignupPostError, SignupApiV1AuthSignupPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCampaignApiV1CampaignCampaignIdPatchData, UpdateCampaignApiV1CampaignCampaignIdPatchError, UpdateCampaignApiV1CampaignCampaignIdPatchResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen';
export type Options
= ClientOptions & {
/**
@@ -1628,6 +1628,44 @@ export const searchChunksApiV1KnowledgeBaseSearchPost = (options: Options) => {
+ return (options.client ?? _heyApiClient).post({
+ url: '/api/v1/auth/signup',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers
+ }
+ });
+};
+
+/**
+ * Login
+ */
+export const loginApiV1AuthLoginPost = (options: Options) => {
+ return (options.client ?? _heyApiClient).post({
+ url: '/api/v1/auth/login',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers
+ }
+ });
+};
+
+/**
+ * Get Current User
+ */
+export const getCurrentUserApiV1AuthMeGet = (options?: Options) => {
+ return (options?.client ?? _heyApiClient).get({
+ url: '/api/v1/auth/me',
+ ...options
+ });
+};
+
/**
* Health
*/
diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts
index 2ecfad1..4256804 100644
--- a/ui/src/client/types.gen.ts
+++ b/ui/src/client/types.gen.ts
@@ -80,6 +80,11 @@ export type AdminCommentResponse = {
admin_comment_ts: string;
};
+export type AuthResponse = {
+ token: string;
+ user: UserResponse;
+};
+
export type AuthUserResponse = {
id: number;
is_superuser: boolean;
@@ -599,6 +604,8 @@ export type HealthResponse = {
status: string;
version: string;
backend_api_endpoint: string;
+ deployment_mode: string;
+ auth_provider: string;
};
/**
@@ -721,6 +728,11 @@ export type LoadTestStatsResponse = {
}>;
};
+export type LoginRequest = {
+ email: string;
+ password: string;
+};
+
export type PresignedUploadUrlRequest = {
/**
* CSV filename
@@ -808,6 +820,12 @@ export type SessionResponse = {
expires_at: string;
};
+export type SignupRequest = {
+ email: string;
+ password: string;
+ name?: string | null;
+};
+
export type SuperuserWorkflowRunResponse = {
id: number;
name: string;
@@ -1129,6 +1147,13 @@ export type UserConfigurationRequestResponseSchema = {
} | null;
};
+export type UserResponse = {
+ id: number;
+ email: string | null;
+ name?: string | null;
+ organization_id?: number | null;
+};
+
export type ValidateWorkflowResponse = {
is_valid: boolean;
errors: Array;
@@ -5018,6 +5043,97 @@ export type SearchChunksApiV1KnowledgeBaseSearchPostResponses = {
export type SearchChunksApiV1KnowledgeBaseSearchPostResponse = SearchChunksApiV1KnowledgeBaseSearchPostResponses[keyof SearchChunksApiV1KnowledgeBaseSearchPostResponses];
+export type SignupApiV1AuthSignupPostData = {
+ body: SignupRequest;
+ path?: never;
+ query?: never;
+ url: '/api/v1/auth/signup';
+};
+
+export type SignupApiV1AuthSignupPostErrors = {
+ /**
+ * Not found
+ */
+ 404: unknown;
+ /**
+ * Validation Error
+ */
+ 422: HttpValidationError;
+};
+
+export type SignupApiV1AuthSignupPostError = SignupApiV1AuthSignupPostErrors[keyof SignupApiV1AuthSignupPostErrors];
+
+export type SignupApiV1AuthSignupPostResponses = {
+ /**
+ * Successful Response
+ */
+ 200: AuthResponse;
+};
+
+export type SignupApiV1AuthSignupPostResponse = SignupApiV1AuthSignupPostResponses[keyof SignupApiV1AuthSignupPostResponses];
+
+export type LoginApiV1AuthLoginPostData = {
+ body: LoginRequest;
+ path?: never;
+ query?: never;
+ url: '/api/v1/auth/login';
+};
+
+export type LoginApiV1AuthLoginPostErrors = {
+ /**
+ * Not found
+ */
+ 404: unknown;
+ /**
+ * Validation Error
+ */
+ 422: HttpValidationError;
+};
+
+export type LoginApiV1AuthLoginPostError = LoginApiV1AuthLoginPostErrors[keyof LoginApiV1AuthLoginPostErrors];
+
+export type LoginApiV1AuthLoginPostResponses = {
+ /**
+ * Successful Response
+ */
+ 200: AuthResponse;
+};
+
+export type LoginApiV1AuthLoginPostResponse = LoginApiV1AuthLoginPostResponses[keyof LoginApiV1AuthLoginPostResponses];
+
+export type GetCurrentUserApiV1AuthMeGetData = {
+ body?: never;
+ headers?: {
+ authorization?: string | null;
+ 'X-API-Key'?: string | null;
+ };
+ path?: never;
+ query?: never;
+ url: '/api/v1/auth/me';
+};
+
+export type GetCurrentUserApiV1AuthMeGetErrors = {
+ /**
+ * Not found
+ */
+ 404: unknown;
+ /**
+ * Validation Error
+ */
+ 422: HttpValidationError;
+};
+
+export type GetCurrentUserApiV1AuthMeGetError = GetCurrentUserApiV1AuthMeGetErrors[keyof GetCurrentUserApiV1AuthMeGetErrors];
+
+export type GetCurrentUserApiV1AuthMeGetResponses = {
+ /**
+ * Successful Response
+ */
+ 200: UserResponse;
+};
+
+export type GetCurrentUserApiV1AuthMeGetResponse = GetCurrentUserApiV1AuthMeGetResponses[keyof GetCurrentUserApiV1AuthMeGetResponses];
+
export type HealthApiV1HealthGetData = {
body?: never;
path?: never;
diff --git a/ui/src/components/SignInClient.tsx b/ui/src/components/SignInClient.tsx
index 0ecf521..72d15cb 100644
--- a/ui/src/components/SignInClient.tsx
+++ b/ui/src/components/SignInClient.tsx
@@ -2,6 +2,10 @@
import { Loader2 } from 'lucide-react';
import dynamic from 'next/dynamic';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+
+import { useAuth } from '@/lib/auth';
import Footer from './Footer';
@@ -12,16 +16,19 @@ const SignIn = dynamic(
);
export default function SignInClient() {
- const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
+ const { provider } = useAuth();
+ const router = useRouter();
- if (authProvider !== 'stack') {
+ useEffect(() => {
+ if (provider === 'local') {
+ router.replace('/auth/login');
+ }
+ }, [provider, router]);
+
+ if (provider !== 'stack') {
return (
-
-
Local Authentication
-
Local authentication is enabled. No sign-in required.
-
-
+
);
}
diff --git a/ui/src/components/flow/nodes/StartCall.tsx b/ui/src/components/flow/nodes/StartCall.tsx
index abec38d..3370b37 100644
--- a/ui/src/components/flow/nodes/StartCall.tsx
+++ b/ui/src/components/flow/nodes/StartCall.tsx
@@ -14,7 +14,6 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
-import { isOSSMode } from "@/lib/utils";
import { NodeContent } from "./common/NodeContent";
import { NodeEditDialog } from "./common/NodeEditDialog";
@@ -350,21 +349,19 @@ const StartCallEditForm = ({
Add Global Prompt
- {!isOSSMode() && (
-