feat: add telnyx webhook api key in telephony config (#270)

This commit is contained in:
Sabiha Khan 2026-05-09 18:03:42 +05:30 committed by GitHub
parent 45a81c88e0
commit 01c201bf09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 249 additions and 13 deletions

View file

@ -75,6 +75,34 @@ class TelephonyConfigurationClient(BaseDBClient):
)
return list(result.scalars().all())
async def count_telnyx_configs_missing_webhook_public_key(
self, organization_id: int
) -> int:
"""Count Telnyx configs in this org with no webhook_public_key in credentials.
Used by the org-warnings endpoint to surface a UI nudge until customers
paste their portal-issued public key.
"""
async with self.async_session() as session:
result = await session.execute(
select(func.count(TelephonyConfigurationModel.id)).where(
TelephonyConfigurationModel.organization_id == organization_id,
TelephonyConfigurationModel.provider == "telnyx",
(
TelephonyConfigurationModel.credentials.op("->>")(
"webhook_public_key"
).is_(None)
)
| (
TelephonyConfigurationModel.credentials.op("->>")(
"webhook_public_key"
)
== ""
),
)
)
return int(result.scalar() or 0)
async def list_all_telephony_configurations_by_provider(
self, provider: str
) -> List[TelephonyConfigurationModel]:

View file

@ -87,6 +87,17 @@ class TelephonyProvidersMetadataResponse(BaseModel):
providers: List[TelephonyProviderMetadata]
class TelephonyConfigWarningsResponse(BaseModel):
"""Aggregated telephony-configuration warning counts for the user's org.
Drives the page banner and nav badge that nudge customers to finish
optional-but-recommended configuration steps. Shape is a flat dict so
new warning types can be added without breaking the client.
"""
telnyx_missing_webhook_public_key_count: int
@router.get(
"/telephony-providers/metadata",
response_model=TelephonyProvidersMetadataResponse,
@ -127,6 +138,27 @@ async def get_telephony_providers_metadata(user: UserModel = Depends(get_user)):
return TelephonyProvidersMetadataResponse(providers=providers)
@router.get(
"/telephony-config-warnings",
response_model=TelephonyConfigWarningsResponse,
)
async def get_telephony_config_warnings(user: UserModel = Depends(get_user)):
"""Return aggregated warning counts for the current org's telephony configs.
Today this surfaces only Telnyx configs missing ``webhook_public_key``;
additional warning types should be added as new fields on the response.
"""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
telnyx_missing = await db_client.count_telnyx_configs_missing_webhook_public_key(
user.selected_organization_id
)
return TelephonyConfigWarningsResponse(
telnyx_missing_webhook_public_key_count=telnyx_missing,
)
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):

View file

@ -27,6 +27,7 @@ def _config_loader(value: Dict[str, Any]) -> Dict[str, Any]:
"provider": "telnyx",
"api_key": value.get("api_key"),
"connection_id": value.get("connection_id"),
"webhook_public_key": value.get("webhook_public_key"),
"from_numbers": value.get("from_numbers", []),
}
@ -124,6 +125,18 @@ _UI_METADATA = ProviderUIMetadata(
"blank and we will auto-create one for you on save."
),
),
ProviderUIField(
name="webhook_public_key",
label="Webhook Public Key",
type="textarea",
required=False,
sensitive=False,
description=(
"Public key from Mission Control Portal → Keys & Credentials "
"→ Public Key. Used to verify Telnyx webhook signatures. "
"Without it, webhooks from Telnyx will be rejected."
),
),
ProviderUIField(
name="from_numbers",
label="Phone Numbers",

View file

@ -18,6 +18,14 @@ class TelnyxConfigurationRequest(BaseModel):
"stored on the configuration."
),
)
webhook_public_key: Optional[str] = Field(
default=None,
description=(
"Webhook public key from Mission Control Portal → Keys & "
"Credentials → Public Key. Used to verify Telnyx webhook "
"signatures."
),
)
# Phone numbers are managed via the dedicated phone-numbers endpoints; the
# legacy /telephony-config POST shim still accepts them inline.
from_numbers: List[str] = Field(
@ -31,4 +39,5 @@ class TelnyxConfigurationResponse(BaseModel):
provider: Literal["telnyx"] = Field(default="telnyx")
api_key: str # Masked
connection_id: Optional[str] = None
webhook_public_key: Optional[str] = None
from_numbers: List[str]

View file

@ -48,6 +48,7 @@ class TelnyxProvider(TelephonyProvider):
def __init__(self, config: Dict[str, Any]):
self.api_key = config.get("api_key")
self.connection_id = config.get("connection_id")
self.webhook_public_key = config.get("webhook_public_key")
self.from_numbers = config.get("from_numbers", [])
if isinstance(self.from_numbers, str):