dograh/api/routes/user.py

422 lines
14 KiB
Python
Raw Normal View History

2025-09-09 14:37:32 +05:30
from datetime import datetime, timedelta
from typing import List, Literal, Optional, TypedDict, Union
2025-09-09 14:37:32 +05:30
from fastapi import APIRouter, Depends, HTTPException, Query
from loguru import logger
2026-03-11 17:57:04 +05:30
from pydantic import BaseModel, ValidationError
2025-09-09 14:37:32 +05:30
from api.db import db_client
from api.db.models import (
UserModel,
)
feat: UI refresh and user onboarding (#430) * docs: design spec for lead-gen surfaces (Credits & Billing, Hire-an-Expert, Top-up, Enterprise) Add brainstorming spec for: sidebar OBSERVE→MANAGE rename + Credits & Billing link + Hire-an-Expert footer button; new /billing page with extracted Dograh Model Credits card + CTAs; Top-up / Hire-an-Expert / Enterprise intake modals with inline math captcha; and a workflow-builder Hire-an-Expert nudge. Frontend only; submissions fire PostHog events via a submitLead() seam for a future MongoDB endpoint. Also gitignore .superpowers/ brainstorm mockups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for user-onboarding lead-gen surfaces 14 bite-sized tasks: PostHog events, shared helpers (field options, work-email blocklist, submitLead seam, math captcha), three intake modals (enterprise/hire/top-up), LeadFormsProvider context, AppLayout mount, sidebar MANAGE rename + Credits & Billing link + footer Hire button, extracted DograhCreditsCard, /billing page, credits removal from Agent Runs, builder nudge, and a full verification/dogfood pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): register PostHog events for lead-gen surfaces Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): shared field options, work-email validation, and submit seam Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): inline math captcha field Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): enterprise intake modal Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): hire-an-expert modal with enterprise link Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): top-up modal with >20k volume-pricing gate Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): shared lead-forms context provider Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): mount LeadFormsProvider in app layout Wrap the sidebar branch of AppLayout with LeadFormsProvider so the shared lead modals are available to the sidebar, billing card, and builder nudge. Includes eslint import-order auto-fixes in TopUpModal and LeadFormsContext. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): rename OBSERVE to MANAGE, add Credits & Billing link and Hire-an-Expert footer button Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): extract DograhCreditsCard with top-up + hire CTAs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): add Credits & Billing page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(lead-gen): move Dograh Model Credits card out of Agent Runs to /billing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): delayed Hire-an-Expert nudge on the workflow builder Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(ui): add lint:lead-flow guard script Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ui): restructure lead forms, self-serve Buy Credits, dialog blur Revised lead-capture surfaces and credits bar: - Dialog overlay gains backdrop blur (bg-black/60 backdrop-blur-sm). - Shared primitives: LeadModalShell (icon/eyebrow header, scrollable body, sticky footer, trust-line slot), PhoneField (react-international-phone, dark, E.164 out), FormTrustLine ("Average response: under 10 minutes..."). - HireExpertModal: Name, Company, Job title, agent goal, Phone (required), monthly volume. EnterpriseModal: + work email (required logged-out), conditional deployment (yes/no/maybe, source-gated), agent goal. OnboardingModal: drop useCase. Phone mandatory except onboarding. - Volume buckets match the backend qualifier (0-5k/5k-100k/100k+/not-sure). - Delete TopUpModal; DograhCreditsCard now self-serve Buy Credits (amount chips $5/$10/$25/$50/$100 + custom min $5 → startTopUp seam) + Hire an Expert + dashed custom-pricing link opening Enterprise (billing_custom_pricing). - PostHog events: drop topup_*, add buy_credits_clicked, buy_credits_amount_selected, custom_pricing_clicked. LeadFormsContext drops topup; LeadKind/LeadSource updated. - Introduce a single --cta warm accent token (CTAs + focus rings only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ui): split-screen auth + enterprise CTA + dark theme default - AuthShell: dark two-column auth layout (brand/value panel with CSS-only waveform motif + proof points + Bland-style enterprise CTA block on the left, zinc-900 form card on the right; single-column on mobile). - AuthEnterpriseCTA: "Talk to our team" → dograh.com/contact?intent=enterprise. - stack-theme: dark StackTheme token overrides synced to globals.css. - page.tsx: wrap StackHandler (non-fullPage) in AuthShell + StackTheme; local-auth fallback preserved inside the shell. BackButton slimmed for the card. - Dark locked as default: <html className="dark">, next-themes ThemeProvider (defaultTheme="dark", enableSystem=false); inline no-FOUC script defaults dark. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ui rezig, onboarding, billing, hire us & on prem cues * ui changes * chore: update comment * chore: untrack docs/superpowers and gitignore it * feat: refactor user configuration table * feat(ui): 'check your email' confirmation on lead forms Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * added email and country in form submissions * chore: update leads api * fix: wrap dograh model config in card --------- Co-authored-by: Pritesh <pritesh@dograh.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:33 +05:30
from api.schemas.onboarding_state import OnboardingState, OnboardingStateUpdate
2025-09-09 14:37:32 +05:30
from api.services.auth.depends import get_user
from api.services.configuration.ai_model_configuration import (
get_resolved_ai_model_configuration,
)
2025-09-09 14:37:32 +05:30
from api.services.configuration.check_validity import (
APIKeyStatusResponse,
UserConfigurationValidator,
)
from api.services.configuration.defaults import DEFAULT_SERVICE_PROVIDERS
2026-03-11 17:57:04 +05:30
from api.services.configuration.masking import check_for_masked_keys, mask_user_config
2025-09-09 14:37:32 +05:30
from api.services.configuration.merge import merge_user_configurations
from api.services.configuration.registry import REGISTRY, ServiceType
from api.services.mps_service_key_client import mps_service_key_client
from api.services.organization_preferences import (
get_organization_preferences,
upsert_organization_preferences,
)
feat: UI refresh and user onboarding (#430) * docs: design spec for lead-gen surfaces (Credits & Billing, Hire-an-Expert, Top-up, Enterprise) Add brainstorming spec for: sidebar OBSERVE→MANAGE rename + Credits & Billing link + Hire-an-Expert footer button; new /billing page with extracted Dograh Model Credits card + CTAs; Top-up / Hire-an-Expert / Enterprise intake modals with inline math captcha; and a workflow-builder Hire-an-Expert nudge. Frontend only; submissions fire PostHog events via a submitLead() seam for a future MongoDB endpoint. Also gitignore .superpowers/ brainstorm mockups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for user-onboarding lead-gen surfaces 14 bite-sized tasks: PostHog events, shared helpers (field options, work-email blocklist, submitLead seam, math captcha), three intake modals (enterprise/hire/top-up), LeadFormsProvider context, AppLayout mount, sidebar MANAGE rename + Credits & Billing link + footer Hire button, extracted DograhCreditsCard, /billing page, credits removal from Agent Runs, builder nudge, and a full verification/dogfood pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): register PostHog events for lead-gen surfaces Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): shared field options, work-email validation, and submit seam Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): inline math captcha field Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): enterprise intake modal Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): hire-an-expert modal with enterprise link Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): top-up modal with >20k volume-pricing gate Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): shared lead-forms context provider Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): mount LeadFormsProvider in app layout Wrap the sidebar branch of AppLayout with LeadFormsProvider so the shared lead modals are available to the sidebar, billing card, and builder nudge. Includes eslint import-order auto-fixes in TopUpModal and LeadFormsContext. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): rename OBSERVE to MANAGE, add Credits & Billing link and Hire-an-Expert footer button Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): extract DograhCreditsCard with top-up + hire CTAs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): add Credits & Billing page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(lead-gen): move Dograh Model Credits card out of Agent Runs to /billing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): delayed Hire-an-Expert nudge on the workflow builder Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(ui): add lint:lead-flow guard script Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ui): restructure lead forms, self-serve Buy Credits, dialog blur Revised lead-capture surfaces and credits bar: - Dialog overlay gains backdrop blur (bg-black/60 backdrop-blur-sm). - Shared primitives: LeadModalShell (icon/eyebrow header, scrollable body, sticky footer, trust-line slot), PhoneField (react-international-phone, dark, E.164 out), FormTrustLine ("Average response: under 10 minutes..."). - HireExpertModal: Name, Company, Job title, agent goal, Phone (required), monthly volume. EnterpriseModal: + work email (required logged-out), conditional deployment (yes/no/maybe, source-gated), agent goal. OnboardingModal: drop useCase. Phone mandatory except onboarding. - Volume buckets match the backend qualifier (0-5k/5k-100k/100k+/not-sure). - Delete TopUpModal; DograhCreditsCard now self-serve Buy Credits (amount chips $5/$10/$25/$50/$100 + custom min $5 → startTopUp seam) + Hire an Expert + dashed custom-pricing link opening Enterprise (billing_custom_pricing). - PostHog events: drop topup_*, add buy_credits_clicked, buy_credits_amount_selected, custom_pricing_clicked. LeadFormsContext drops topup; LeadKind/LeadSource updated. - Introduce a single --cta warm accent token (CTAs + focus rings only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ui): split-screen auth + enterprise CTA + dark theme default - AuthShell: dark two-column auth layout (brand/value panel with CSS-only waveform motif + proof points + Bland-style enterprise CTA block on the left, zinc-900 form card on the right; single-column on mobile). - AuthEnterpriseCTA: "Talk to our team" → dograh.com/contact?intent=enterprise. - stack-theme: dark StackTheme token overrides synced to globals.css. - page.tsx: wrap StackHandler (non-fullPage) in AuthShell + StackTheme; local-auth fallback preserved inside the shell. BackButton slimmed for the card. - Dark locked as default: <html className="dark">, next-themes ThemeProvider (defaultTheme="dark", enableSystem=false); inline no-FOUC script defaults dark. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ui rezig, onboarding, billing, hire us & on prem cues * ui changes * chore: update comment * chore: untrack docs/superpowers and gitignore it * feat: refactor user configuration table * feat(ui): 'check your email' confirmation on lead forms Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * added email and country in form submissions * chore: update leads api * fix: wrap dograh model config in card --------- Co-authored-by: Pritesh <pritesh@dograh.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:33 +05:30
from api.services.user_onboarding import (
get_onboarding_state,
update_onboarding_state,
)
2025-09-09 14:37:32 +05:30
router = APIRouter(prefix="/user")
class AuthUserResponse(TypedDict):
id: int
is_superuser: bool
class DefaultConfigurationsResponse(TypedDict):
llm: dict[str, dict]
tts: dict[str, dict]
stt: dict[str, dict]
embeddings: dict[str, dict]
realtime: dict[str, dict]
2025-09-09 14:37:32 +05:30
default_providers: dict[str, str]
@router.get("/configurations/defaults")
async def get_default_configurations() -> DefaultConfigurationsResponse:
configurations = {
"llm": {
provider: model_cls.model_json_schema()
for provider, model_cls in REGISTRY[ServiceType.LLM].items()
},
"tts": {
provider: model_cls.model_json_schema()
for provider, model_cls in REGISTRY[ServiceType.TTS].items()
},
"stt": {
provider: model_cls.model_json_schema()
for provider, model_cls in REGISTRY[ServiceType.STT].items()
},
"embeddings": {
provider: model_cls.model_json_schema()
for provider, model_cls in REGISTRY[ServiceType.EMBEDDINGS].items()
},
"realtime": {
provider: model_cls.model_json_schema()
for provider, model_cls in REGISTRY[ServiceType.REALTIME].items()
},
2025-09-09 14:37:32 +05:30
"default_providers": DEFAULT_SERVICE_PROVIDERS,
}
return configurations
@router.get("/auth/user")
async def get_auth_user(
user: UserModel = Depends(get_user),
) -> AuthUserResponse:
return {
"id": user.id,
"is_superuser": user.is_superuser,
}
class UserConfigurationRequestResponseSchema(BaseModel):
2026-03-19 15:06:59 +05:30
llm: dict[str, Union[str, float, list[str], None]] | None = None
tts: dict[str, Union[str, float, list[str], None]] | None = None
stt: dict[str, Union[str, float, list[str], None]] | None = None
embeddings: dict[str, Union[str, float, list[str], None]] | None = None
realtime: dict[str, Union[str, float, list[str], None]] | None = None
is_realtime: bool | None = None
2025-09-09 14:37:32 +05:30
test_phone_number: str | None = None
timezone: str | None = None
organization_pricing: dict[str, Union[float, str, bool]] | None = None
@router.get("/configurations/user")
async def get_user_configurations(
user: UserModel = Depends(get_user),
) -> UserConfigurationRequestResponseSchema:
resolved_config = await get_resolved_ai_model_configuration(
user_id=user.id,
organization_id=user.selected_organization_id,
)
masked_config = mask_user_config(resolved_config.effective)
if user.selected_organization_id:
preferences = await get_organization_preferences(user.selected_organization_id)
if preferences.test_phone_number is not None:
masked_config["test_phone_number"] = preferences.test_phone_number
if preferences.timezone is not None:
masked_config["timezone"] = preferences.timezone
2025-09-09 14:37:32 +05:30
# Add organization pricing info if available
if user.selected_organization_id:
org = await db_client.get_organization_by_id(user.selected_organization_id)
if org and org.price_per_second_usd is not None:
masked_config["organization_pricing"] = {
"price_per_second_usd": org.price_per_second_usd,
"currency": "USD",
"billing_enabled": True,
}
return masked_config
@router.put("/configurations/user")
async def update_user_configurations(
request: UserConfigurationRequestResponseSchema,
user: UserModel = Depends(get_user),
) -> UserConfigurationRequestResponseSchema:
existing_config = await db_client.get_user_configurations(user.id)
incoming_dict = request.model_dump(exclude_none=True)
# Remove organization_pricing from incoming dict as it's read-only
incoming_dict.pop("organization_pricing", None)
preferences_update = {
key: incoming_dict.pop(key)
for key in ("test_phone_number", "timezone")
if key in incoming_dict
}
2025-09-09 14:37:32 +05:30
if incoming_dict:
# Merge via helper
try:
user_configurations = merge_user_configurations(
existing_config, incoming_dict
)
except ValidationError as e:
raise HTTPException(status_code=422, detail=str(e))
2026-03-11 17:57:04 +05:30
try:
check_for_masked_keys(user_configurations)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
2025-09-09 14:37:32 +05:30
try:
validator = UserConfigurationValidator()
await validator.validate(
user_configurations,
organization_id=user.selected_organization_id,
created_by=user.provider_id,
)
except ValueError as e:
raise HTTPException(status_code=422, detail=e.args[0])
2025-09-09 14:37:32 +05:30
user_configurations = await db_client.update_user_configuration(
user.id, user_configurations
)
else:
user_configurations = existing_config
if user.selected_organization_id and preferences_update:
preferences = await get_organization_preferences(user.selected_organization_id)
if "test_phone_number" in preferences_update:
preferences.test_phone_number = preferences_update["test_phone_number"]
if "timezone" in preferences_update:
preferences.timezone = preferences_update["timezone"]
await upsert_organization_preferences(
user.selected_organization_id,
preferences,
)
2025-09-09 14:37:32 +05:30
# Return masked version of updated config
masked_config = mask_user_config(user_configurations)
if user.selected_organization_id:
preferences = await get_organization_preferences(user.selected_organization_id)
if preferences.test_phone_number is not None:
masked_config["test_phone_number"] = preferences.test_phone_number
if preferences.timezone is not None:
masked_config["timezone"] = preferences.timezone
2025-09-09 14:37:32 +05:30
# Add organization pricing info if available
if user.selected_organization_id:
org = await db_client.get_organization_by_id(user.selected_organization_id)
if org and org.price_per_second_usd is not None:
masked_config["organization_pricing"] = {
"price_per_second_usd": org.price_per_second_usd,
"currency": "USD",
"billing_enabled": True,
}
return masked_config
feat: UI refresh and user onboarding (#430) * docs: design spec for lead-gen surfaces (Credits & Billing, Hire-an-Expert, Top-up, Enterprise) Add brainstorming spec for: sidebar OBSERVE→MANAGE rename + Credits & Billing link + Hire-an-Expert footer button; new /billing page with extracted Dograh Model Credits card + CTAs; Top-up / Hire-an-Expert / Enterprise intake modals with inline math captcha; and a workflow-builder Hire-an-Expert nudge. Frontend only; submissions fire PostHog events via a submitLead() seam for a future MongoDB endpoint. Also gitignore .superpowers/ brainstorm mockups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for user-onboarding lead-gen surfaces 14 bite-sized tasks: PostHog events, shared helpers (field options, work-email blocklist, submitLead seam, math captcha), three intake modals (enterprise/hire/top-up), LeadFormsProvider context, AppLayout mount, sidebar MANAGE rename + Credits & Billing link + footer Hire button, extracted DograhCreditsCard, /billing page, credits removal from Agent Runs, builder nudge, and a full verification/dogfood pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): register PostHog events for lead-gen surfaces Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): shared field options, work-email validation, and submit seam Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): inline math captcha field Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): enterprise intake modal Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): hire-an-expert modal with enterprise link Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): top-up modal with >20k volume-pricing gate Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): shared lead-forms context provider Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): mount LeadFormsProvider in app layout Wrap the sidebar branch of AppLayout with LeadFormsProvider so the shared lead modals are available to the sidebar, billing card, and builder nudge. Includes eslint import-order auto-fixes in TopUpModal and LeadFormsContext. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): rename OBSERVE to MANAGE, add Credits & Billing link and Hire-an-Expert footer button Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): extract DograhCreditsCard with top-up + hire CTAs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): add Credits & Billing page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(lead-gen): move Dograh Model Credits card out of Agent Runs to /billing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(lead-gen): delayed Hire-an-Expert nudge on the workflow builder Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(ui): add lint:lead-flow guard script Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ui): restructure lead forms, self-serve Buy Credits, dialog blur Revised lead-capture surfaces and credits bar: - Dialog overlay gains backdrop blur (bg-black/60 backdrop-blur-sm). - Shared primitives: LeadModalShell (icon/eyebrow header, scrollable body, sticky footer, trust-line slot), PhoneField (react-international-phone, dark, E.164 out), FormTrustLine ("Average response: under 10 minutes..."). - HireExpertModal: Name, Company, Job title, agent goal, Phone (required), monthly volume. EnterpriseModal: + work email (required logged-out), conditional deployment (yes/no/maybe, source-gated), agent goal. OnboardingModal: drop useCase. Phone mandatory except onboarding. - Volume buckets match the backend qualifier (0-5k/5k-100k/100k+/not-sure). - Delete TopUpModal; DograhCreditsCard now self-serve Buy Credits (amount chips $5/$10/$25/$50/$100 + custom min $5 → startTopUp seam) + Hire an Expert + dashed custom-pricing link opening Enterprise (billing_custom_pricing). - PostHog events: drop topup_*, add buy_credits_clicked, buy_credits_amount_selected, custom_pricing_clicked. LeadFormsContext drops topup; LeadKind/LeadSource updated. - Introduce a single --cta warm accent token (CTAs + focus rings only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ui): split-screen auth + enterprise CTA + dark theme default - AuthShell: dark two-column auth layout (brand/value panel with CSS-only waveform motif + proof points + Bland-style enterprise CTA block on the left, zinc-900 form card on the right; single-column on mobile). - AuthEnterpriseCTA: "Talk to our team" → dograh.com/contact?intent=enterprise. - stack-theme: dark StackTheme token overrides synced to globals.css. - page.tsx: wrap StackHandler (non-fullPage) in AuthShell + StackTheme; local-auth fallback preserved inside the shell. BackButton slimmed for the card. - Dark locked as default: <html className="dark">, next-themes ThemeProvider (defaultTheme="dark", enableSystem=false); inline no-FOUC script defaults dark. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ui rezig, onboarding, billing, hire us & on prem cues * ui changes * chore: update comment * chore: untrack docs/superpowers and gitignore it * feat: refactor user configuration table * feat(ui): 'check your email' confirmation on lead forms Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * added email and country in form submissions * chore: update leads api * fix: wrap dograh model config in card --------- Co-authored-by: Pritesh <pritesh@dograh.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:33 +05:30
@router.get("/onboarding-state")
async def get_user_onboarding_state(
user: UserModel = Depends(get_user),
) -> OnboardingState:
return await get_onboarding_state(user.id)
@router.put("/onboarding-state")
async def update_user_onboarding_state(
request: OnboardingStateUpdate,
user: UserModel = Depends(get_user),
) -> OnboardingState:
return await update_onboarding_state(user.id, request)
2025-09-09 14:37:32 +05:30
@router.get("/configurations/user/validate")
async def validate_user_configurations(
validity_ttl_seconds: int = Query(default=60, ge=0, le=86400),
user: UserModel = Depends(get_user),
) -> APIKeyStatusResponse:
resolved_config = await get_resolved_ai_model_configuration(
user_id=user.id,
organization_id=user.selected_organization_id,
)
configurations = resolved_config.effective
2025-09-09 14:37:32 +05:30
if (
configurations.last_validated_at
and configurations.last_validated_at
< datetime.now() - timedelta(seconds=validity_ttl_seconds)
):
validator = UserConfigurationValidator()
try:
status = await validator.validate(
configurations,
organization_id=user.selected_organization_id,
created_by=user.provider_id,
)
2025-09-09 14:37:32 +05:30
await db_client.update_user_configuration_last_validated_at(user.id)
return status
except ValueError as e:
raise HTTPException(status_code=422, detail=e.args[0])
else:
return {"status": []}
# API Key Management Endpoints
class APIKeyResponse(BaseModel):
id: int
name: str
key_prefix: str
is_active: bool
created_at: datetime
last_used_at: Optional[datetime] = None
archived_at: Optional[datetime] = None
class CreateAPIKeyRequest(BaseModel):
name: str
class CreateAPIKeyResponse(BaseModel):
id: int
name: str
key_prefix: str
api_key: str # Only returned when creating a new key
created_at: datetime
@router.get("/api-keys")
async def get_api_keys(
include_archived: bool = Query(default=False),
user: UserModel = Depends(get_user),
) -> List[APIKeyResponse]:
"""Get all API keys for the user's selected organization."""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
api_keys = await db_client.get_api_keys_by_organization(
user.selected_organization_id, include_archived=include_archived
)
return [
APIKeyResponse(
id=key.id,
name=key.name,
key_prefix=key.key_prefix,
is_active=key.is_active,
created_at=key.created_at,
last_used_at=key.last_used_at,
archived_at=key.archived_at,
)
for key in api_keys
]
@router.post("/api-keys")
async def create_api_key(
request: CreateAPIKeyRequest,
user: UserModel = Depends(get_user),
) -> CreateAPIKeyResponse:
"""Create a new API key for the user's selected organization."""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
api_key, raw_key = await db_client.create_api_key(
organization_id=user.selected_organization_id,
name=request.name,
created_by=user.id,
)
return CreateAPIKeyResponse(
id=api_key.id,
name=api_key.name,
key_prefix=api_key.key_prefix,
api_key=raw_key,
created_at=api_key.created_at,
)
@router.delete("/api-keys/{api_key_id}")
async def archive_api_key(
api_key_id: int,
user: UserModel = Depends(get_user),
) -> dict:
"""Archive an API key (soft delete)."""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
# Verify the API key belongs to the user's organization
api_keys = await db_client.get_api_keys_by_organization(
user.selected_organization_id, include_archived=True
)
if not any(key.id == api_key_id for key in api_keys):
raise HTTPException(status_code=404, detail="API key not found")
success = await db_client.archive_api_key(api_key_id)
if not success:
raise HTTPException(status_code=500, detail="Failed to archive API key")
return {"success": True, "message": "API key archived successfully"}
@router.put("/api-keys/{api_key_id}/reactivate")
async def reactivate_api_key(
api_key_id: int,
user: UserModel = Depends(get_user),
) -> dict:
"""Reactivate an archived API key."""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
# Verify the API key belongs to the user's organization
api_keys = await db_client.get_api_keys_by_organization(
user.selected_organization_id, include_archived=True
)
if not any(key.id == api_key_id for key in api_keys):
raise HTTPException(status_code=404, detail="API key not found")
success = await db_client.reactivate_api_key(api_key_id)
if not success:
raise HTTPException(status_code=500, detail="Failed to reactivate API key")
return {"success": True, "message": "API key reactivated successfully"}
# Voice Configuration Endpoints
2026-04-07 14:05:47 +05:30
TTSProvider = Literal["elevenlabs", "deepgram", "sarvam", "cartesia", "dograh", "rime"]
class VoiceInfo(BaseModel):
voice_id: str
name: str
description: Optional[str] = None
accent: Optional[str] = None
gender: Optional[str] = None
language: Optional[str] = None
preview_url: Optional[str] = None
class VoicesResponse(BaseModel):
provider: str
voices: List[VoiceInfo]
@router.get("/configurations/voices/{provider}")
async def get_voices(
provider: TTSProvider,
2026-04-07 14:05:47 +05:30
model: Optional[str] = None,
language: Optional[str] = None,
user: UserModel = Depends(get_user),
) -> VoicesResponse:
"""Get available voices for a TTS provider."""
try:
result = await mps_service_key_client.get_voices(
provider=provider,
2026-04-07 14:05:47 +05:30
model=model,
language=language,
organization_id=user.selected_organization_id,
created_by=user.provider_id,
)
return VoicesResponse(
provider=result.get("provider", provider),
voices=[VoiceInfo(**voice) for voice in result.get("voices", [])],
)
except Exception as e:
logger.error(f"Failed to fetch voices for {provider}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to fetch voices for {provider}",
)