mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
feat: make vici configurable from UI
This commit is contained in:
parent
cf80b20be1
commit
8628e576ca
46 changed files with 2277 additions and 618 deletions
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List, Optional
|
||||
from copy import deepcopy
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from loguru import logger
|
||||
|
|
@ -74,6 +75,7 @@ from api.services.organization_context import (
|
|||
get_organization_context,
|
||||
)
|
||||
from api.services.organization_preferences import (
|
||||
external_pbx_integrations_enabled,
|
||||
get_organization_preferences,
|
||||
upsert_organization_preferences,
|
||||
)
|
||||
|
|
@ -101,14 +103,24 @@ def _sensitive_fields(provider_name: str) -> List[str]:
|
|||
|
||||
def _mask_sensitive(provider_name: str, value: dict) -> dict:
|
||||
"""Return a copy of ``value`` with sensitive fields masked for display."""
|
||||
out = dict(value)
|
||||
out = deepcopy(value)
|
||||
for field_name in _sensitive_fields(provider_name):
|
||||
v = out.get(field_name)
|
||||
v = _get_nested_field(out, field_name)
|
||||
if v:
|
||||
out[field_name] = mask_key(v)
|
||||
_set_nested_field(out, field_name, mask_key(str(v)))
|
||||
return out
|
||||
|
||||
|
||||
class TelephonyProviderUIOption(BaseModel):
|
||||
value: str
|
||||
label: str
|
||||
|
||||
|
||||
class TelephonyProviderUICondition(BaseModel):
|
||||
field: str
|
||||
equals: Any
|
||||
|
||||
|
||||
class TelephonyProviderUIField(BaseModel):
|
||||
"""One form field on a telephony provider's configuration UI."""
|
||||
|
||||
|
|
@ -119,6 +131,9 @@ class TelephonyProviderUIField(BaseModel):
|
|||
sensitive: bool
|
||||
description: Optional[str] = None
|
||||
placeholder: Optional[str] = None
|
||||
options: Optional[List[TelephonyProviderUIOption]] = None
|
||||
visible_when: Optional[TelephonyProviderUICondition] = None
|
||||
section: Optional[str] = None
|
||||
|
||||
|
||||
class TelephonyProviderMetadata(BaseModel):
|
||||
|
|
@ -184,6 +199,9 @@ async def get_telephony_providers_metadata(user: UserModel = Depends(get_user)):
|
|||
if not user.selected_organization_id:
|
||||
raise HTTPException(status_code=400, detail="No organization selected")
|
||||
|
||||
external_pbx_enabled = await external_pbx_integrations_enabled(
|
||||
user.selected_organization_id
|
||||
)
|
||||
providers = []
|
||||
for spec in telephony_registry.all_specs():
|
||||
if spec.ui_metadata is None:
|
||||
|
|
@ -201,8 +219,30 @@ async def get_telephony_providers_metadata(user: UserModel = Depends(get_user)):
|
|||
sensitive=f.sensitive,
|
||||
description=f.description,
|
||||
placeholder=f.placeholder,
|
||||
options=(
|
||||
[
|
||||
{"value": option.value, "label": option.label}
|
||||
for option in f.options
|
||||
]
|
||||
if f.options
|
||||
else None
|
||||
),
|
||||
visible_when=(
|
||||
{
|
||||
"field": f.visible_when.field,
|
||||
"equals": f.visible_when.equals,
|
||||
}
|
||||
if f.visible_when
|
||||
else None
|
||||
),
|
||||
section=f.section,
|
||||
)
|
||||
for f in spec.ui_metadata.fields
|
||||
if not f.feature_gate
|
||||
or (
|
||||
f.feature_gate == "external_pbx_integrations"
|
||||
and external_pbx_enabled
|
||||
)
|
||||
],
|
||||
docs_url=spec.ui_metadata.docs_url,
|
||||
)
|
||||
|
|
@ -511,9 +551,59 @@ async def save_model_configuration_preferences_legacy(
|
|||
def preserve_masked_fields(provider: str, request_dict: dict, existing: dict):
|
||||
"""If the client re-submitted a masked sensitive field, restore the original."""
|
||||
for field_name in _sensitive_fields(provider):
|
||||
v = request_dict.get(field_name)
|
||||
if v and is_mask_of(v, existing.get(field_name, "")):
|
||||
request_dict[field_name] = existing[field_name]
|
||||
v = _get_nested_field(request_dict, field_name)
|
||||
existing_value = _get_nested_field(existing, field_name)
|
||||
if v and is_mask_of(v, existing_value or ""):
|
||||
_set_nested_field(request_dict, field_name, existing_value)
|
||||
|
||||
|
||||
def _get_nested_field(value: dict, dotted_path: str):
|
||||
current = value
|
||||
for part in dotted_path.split("."):
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(part)
|
||||
return current
|
||||
|
||||
|
||||
def _set_nested_field(value: dict, dotted_path: str, field_value) -> None:
|
||||
current = value
|
||||
parts = dotted_path.split(".")
|
||||
for part in parts[:-1]:
|
||||
child = current.get(part)
|
||||
if not isinstance(child, dict):
|
||||
child = {}
|
||||
current[part] = child
|
||||
current = child
|
||||
current[parts[-1]] = field_value
|
||||
|
||||
|
||||
async def _enforce_external_pbx_feature(
|
||||
organization_id: int,
|
||||
provider: str,
|
||||
credentials: dict,
|
||||
*,
|
||||
existing_credentials: Optional[dict] = None,
|
||||
) -> None:
|
||||
if provider != "ari":
|
||||
return
|
||||
if await external_pbx_integrations_enabled(organization_id):
|
||||
return
|
||||
requested_external_pbx = credentials.get("external_pbx")
|
||||
existing_external_pbx = (existing_credentials or {}).get("external_pbx")
|
||||
if not requested_external_pbx and not existing_external_pbx:
|
||||
return
|
||||
# Disabling the feature hides and disables the integration, but does not
|
||||
# destroy saved credentials. Allow only an unchanged masked round-trip.
|
||||
if requested_external_pbx == existing_external_pbx:
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=(
|
||||
"External PBX integrations are disabled for this organization. "
|
||||
"Enable them in Platform Settings before changing this configuration."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _credentials_from_payload(config: TelephonyConfigRequest) -> dict:
|
||||
|
|
@ -612,6 +702,9 @@ async def create_telephony_configuration(
|
|||
raise HTTPException(status_code=400, detail="No organization selected")
|
||||
|
||||
credentials = _credentials_from_payload(request.config)
|
||||
await _enforce_external_pbx_feature(
|
||||
user.selected_organization_id, request.config.provider, credentials
|
||||
)
|
||||
credentials = await _run_preprocess_hook(request.config.provider, credentials)
|
||||
|
||||
try:
|
||||
|
|
@ -694,6 +787,12 @@ async def update_telephony_configuration(
|
|||
preserve_masked_fields(
|
||||
existing.provider, credentials, existing.credentials or {}
|
||||
)
|
||||
await _enforce_external_pbx_feature(
|
||||
user.selected_organization_id,
|
||||
existing.provider,
|
||||
credentials,
|
||||
existing_credentials=existing.credentials or {},
|
||||
)
|
||||
credentials = await _run_preprocess_hook(existing.provider, credentials)
|
||||
|
||||
row = await db_client.update_telephony_configuration(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from api.services.tool_management import (
|
|||
build_tool_response,
|
||||
create_tool_for_user,
|
||||
refresh_mcp_tool_for_user,
|
||||
validate_external_pbx_tool_definition,
|
||||
validate_tool_credential_references,
|
||||
)
|
||||
from api.services.tool_management import (
|
||||
|
|
@ -223,6 +224,18 @@ async def update_tool(
|
|||
if request.definition:
|
||||
definition = request.definition.model_dump()
|
||||
try:
|
||||
existing_tool = await db_client.get_tool_by_uuid(
|
||||
tool_uuid,
|
||||
user.selected_organization_id,
|
||||
include_archived=True,
|
||||
)
|
||||
await validate_external_pbx_tool_definition(
|
||||
definition,
|
||||
organization_id=user.selected_organization_id,
|
||||
existing_definition=(
|
||||
existing_tool.definition if existing_tool else None
|
||||
),
|
||||
)
|
||||
await validate_tool_credential_references(
|
||||
definition,
|
||||
organization_id=user.selected_organization_id,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from api.services.configuration.resolve import (
|
|||
resolve_effective_config,
|
||||
)
|
||||
from api.services.mps_service_key_client import mps_service_key_client
|
||||
from api.services.organization_preferences import external_pbx_integrations_enabled
|
||||
from api.services.posthog_client import capture_event
|
||||
from api.services.reports import generate_workflow_report_csv
|
||||
from api.services.storage import storage_fs
|
||||
|
|
@ -1050,6 +1051,41 @@ async def update_workflow(
|
|||
if request.workflow_configurations is not None
|
||||
else None
|
||||
)
|
||||
if workflow_configurations is not None and not (
|
||||
await external_pbx_integrations_enabled(user.selected_organization_id)
|
||||
):
|
||||
existing_workflow = await db_client.get_workflow(
|
||||
workflow_id, organization_id=user.selected_organization_id
|
||||
)
|
||||
if existing_workflow is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Workflow with id {workflow_id} not found"
|
||||
)
|
||||
existing_draft = await db_client.get_draft_version(workflow_id)
|
||||
existing_configs = (
|
||||
existing_draft.workflow_configurations
|
||||
if existing_draft
|
||||
else existing_workflow.released_definition.workflow_configurations
|
||||
)
|
||||
existing_mappings = (existing_configs or {}).get(
|
||||
"external_pbx_field_mappings", []
|
||||
)
|
||||
incoming_mappings = workflow_configurations.get(
|
||||
"external_pbx_field_mappings", existing_mappings
|
||||
)
|
||||
if incoming_mappings != existing_mappings:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=(
|
||||
"External PBX integrations are disabled for this organization. "
|
||||
"Enable them in Platform Settings before changing field "
|
||||
"mappings."
|
||||
),
|
||||
)
|
||||
if existing_mappings:
|
||||
workflow_configurations["external_pbx_field_mappings"] = (
|
||||
existing_mappings
|
||||
)
|
||||
if workflow_configurations and workflow_configurations.get(
|
||||
WORKFLOW_MODEL_CONFIGURATION_V2_OVERRIDE_KEY
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue