feat: add warning with missing signature secret

This commit is contained in:
Sabiha Khan 2026-06-29 15:06:00 +05:30
parent f644933e28
commit e1e9782c48
10 changed files with 122 additions and 20 deletions

View file

@ -103,6 +103,30 @@ class TelephonyConfigurationClient(BaseDBClient):
)
return int(result.scalar() or 0)
async def count_vonage_configs_missing_signature_secret(
self, organization_id: int
) -> int:
"""Count Vonage configs in this org with no signature_secret."""
async with self.async_session() as session:
result = await session.execute(
select(func.count(TelephonyConfigurationModel.id)).where(
TelephonyConfigurationModel.organization_id == organization_id,
TelephonyConfigurationModel.provider == "vonage",
(
TelephonyConfigurationModel.credentials.op("->>")(
"signature_secret"
).is_(None)
)
| (
TelephonyConfigurationModel.credentials.op("->>")(
"signature_secret"
)
== ""
),
)
)
return int(result.scalar() or 0)
async def list_all_telephony_configurations_by_provider(
self, provider: str
) -> List[TelephonyConfigurationModel]:

View file

@ -145,6 +145,7 @@ class TelephonyConfigWarningsResponse(BaseModel):
"""
telnyx_missing_webhook_public_key_count: int
vonage_missing_signature_secret_count: int
@router.get("/context", response_model=OrganizationContextResponse)
@ -200,8 +201,7 @@ async def get_telephony_providers_metadata(user: UserModel = Depends(get_user)):
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.
Surfaces provider configs missing webhook-verification credentials.
"""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
@ -209,8 +209,12 @@ async def get_telephony_config_warnings(user: UserModel = Depends(get_user)):
telnyx_missing = await db_client.count_telnyx_configs_missing_webhook_public_key(
user.selected_organization_id
)
vonage_missing = await db_client.count_vonage_configs_missing_signature_secret(
user.selected_organization_id
)
return TelephonyConfigWarningsResponse(
telnyx_missing_webhook_public_key_count=telnyx_missing,
vonage_missing_signature_secret_count=vonage_missing,
)

View file

@ -13,8 +13,8 @@ class VonageConfigurationRequest(BaseModel):
api_secret: str = Field(..., description="Vonage API Secret")
application_id: str = Field(..., description="Vonage Application ID")
private_key: str = Field(..., description="Private key for JWT generation")
signature_secret: str = Field(
...,
signature_secret: Optional[str] = Field(
None,
description="Vonage signature secret used to verify signed webhooks",
)
from_numbers: List[str] = Field(

View file

@ -442,7 +442,7 @@ class VonageProvider(TelephonyProvider):
account_id=claims.get("api_key") or webhook_data.get("account_id"),
from_country=None,
to_country=None,
raw_data={**webhook_data, "signed_claims": claims},
raw_data=webhook_data,
)
@staticmethod
@ -630,13 +630,14 @@ class VonageProvider(TelephonyProvider):
capabilities = app_data.get("capabilities") or {}
voice = capabilities.get("voice") or {}
webhooks = voice.get("webhooks") or {}
backend_endpoint, _ = await get_backend_endpoints()
webhooks["answer_url"] = {
"address": webhook_url,
"http_method": "POST",
}
webhooks["event_url"] = {
"address": f"{webhook_url.rsplit('/inbound/run', 1)[0]}/vonage/events",
"address": f"{backend_endpoint}/api/v1/telephony/vonage/events",
"http_method": "POST",
}
voice["webhooks"] = webhooks

View file

@ -53,6 +53,7 @@ export default function TelephonyConfigurationsPage() {
const { user, getAccessToken, loading: authLoading } = useAuth();
const {
telnyxMissingWebhookPublicKeyCount,
vonageMissingSignatureSecretCount,
refresh: refreshWarnings,
} = useTelephonyConfigWarnings();
const [items, setItems] = useState<TelephonyConfigurationListItem[]>([]);
@ -82,9 +83,9 @@ export default function TelephonyConfigurationsPage() {
}
}, [authLoading, user, getAccessToken]);
// After a save (create/update), the backing config may have flipped between
// missing/present webhook_public_key — refresh the cached warning state so
// the page banner and nav badge update without a manual reload.
// After a save (create/update), webhook-verification warning state may have
// changed — refresh the cached warning state so the page banner and nav badge
// update without a manual reload.
const onSaved = useCallback(async () => {
await fetchItems();
await refreshWarnings();
@ -194,6 +195,26 @@ export default function TelephonyConfigurationsPage() {
</div>
)}
{vonageMissingSignatureSecretCount > 0 && (
<div className="mb-6 rounded-md border border-amber-300 bg-amber-50 p-4 text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 shrink-0 mt-0.5" />
<div className="space-y-1 text-sm">
<p className="font-medium">Signature secret not configured</p>
<p>
{vonageMissingSignatureSecretCount === 1
? "1 Vonage configuration is"
: `${vonageMissingSignatureSecretCount} Vonage configurations are`}{" "}
missing a signature secret. Without it, Vonage signed webhooks
are rejected, so inbound calls and call status updates will not
work. Copy the signature secret from your Vonage account and
paste it into the affected Vonage configuration below.
</p>
</div>
</div>
</div>
)}
{loading ? (
<div className="grid gap-3">
<Skeleton className="h-24 w-full" />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5323,6 +5323,10 @@ export type TelephonyConfigWarningsResponse = {
* Telnyx Missing Webhook Public Key Count
*/
telnyx_missing_webhook_public_key_count: number;
/**
* Vonage Missing Signature Secret Count
*/
vonage_missing_signature_secret_count: number;
};
/**
@ -6427,6 +6431,12 @@ export type VonageConfigurationRequest = {
* Private key for JWT generation
*/
private_key: string;
/**
* Signature Secret
*
* Vonage signature secret used to verify signed webhooks
*/
signature_secret?: string | null;
/**
* From Numbers
*
@ -6461,6 +6471,10 @@ export type VonageConfigurationResponse = {
* Private Key
*/
private_key: string;
/**
* Signature Secret
*/
signature_secret?: string | null;
/**
* From Numbers
*/
@ -7553,6 +7567,27 @@ export type HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostRespons
200: unknown;
};
export type HandleVonageEventsWithoutRunApiV1TelephonyVonageEventsPostData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/telephony/vonage/events';
};
export type HandleVonageEventsWithoutRunApiV1TelephonyVonageEventsPostErrors = {
/**
* Not found
*/
404: unknown;
};
export type HandleVonageEventsWithoutRunApiV1TelephonyVonageEventsPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type ImpersonateApiV1SuperuserImpersonatePostData = {
body: ImpersonateRequest;
headers?: {

View file

@ -167,8 +167,13 @@ export function AppSidebar() {
const { provider, getSelectedTeam, logout, user } = useAuth();
const { config } = useAppConfig();
const { openHireExpert } = useLeadForms();
const { telnyxMissingWebhookPublicKeyCount } = useTelephonyConfigWarnings();
const hasTelephonyWarning = telnyxMissingWebhookPublicKeyCount > 0;
const {
telnyxMissingWebhookPublicKeyCount,
vonageMissingSignatureSecretCount,
} = useTelephonyConfigWarnings();
const hasTelephonyWarning =
telnyxMissingWebhookPublicKeyCount > 0 ||
vonageMissingSignatureSecretCount > 0;
const isCollapsed = !isMobile && state === "collapsed";
// Get selected team for Stack auth (cast to Team type from Stack)

View file

@ -7,12 +7,14 @@ import { useAuth } from '@/lib/auth';
interface TelephonyConfigWarningsContextType {
telnyxMissingWebhookPublicKeyCount: number;
vonageMissingSignatureSecretCount: number;
refresh: () => Promise<void>;
loading: boolean;
}
const TelephonyConfigWarningsContext = createContext<TelephonyConfigWarningsContextType>({
telnyxMissingWebhookPublicKeyCount: 0,
vonageMissingSignatureSecretCount: 0,
refresh: async () => { },
loading: false,
});
@ -23,7 +25,8 @@ const TelephonyConfigWarningsContext = createContext<TelephonyConfigWarningsCont
// change. Page-level callers invalidate via refresh() after a save.
export function TelephonyConfigWarningsProvider({ children }: { children: ReactNode }) {
const auth = useAuth();
const [count, setCount] = useState(0);
const [telnyxCount, setTelnyxCount] = useState(0);
const [vonageCount, setVonageCount] = useState(0);
const [loading, setLoading] = useState(false);
const hasFetched = useRef(false);
@ -31,9 +34,11 @@ export function TelephonyConfigWarningsProvider({ children }: { children: ReactN
setLoading(true);
try {
const res = await getTelephonyConfigWarningsApiV1OrganizationsTelephonyConfigWarningsGet();
setCount(res.data?.telnyx_missing_webhook_public_key_count ?? 0);
setTelnyxCount(res.data?.telnyx_missing_webhook_public_key_count ?? 0);
setVonageCount(res.data?.vonage_missing_signature_secret_count ?? 0);
} catch {
setCount(0);
setTelnyxCount(0);
setVonageCount(0);
} finally {
setLoading(false);
}
@ -53,7 +58,8 @@ export function TelephonyConfigWarningsProvider({ children }: { children: ReactN
return (
<TelephonyConfigWarningsContext.Provider
value={{
telnyxMissingWebhookPublicKeyCount: count,
telnyxMissingWebhookPublicKeyCount: telnyxCount,
vonageMissingSignatureSecretCount: vonageCount,
refresh,
loading,
}}