mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-22 08:38:13 +02:00
feat: add vonage telephony (#35)
* refactor: telephony integration * feat: add vonage telephony
This commit is contained in:
parent
6503d806c5
commit
4cfdc3d420
39 changed files with 3382 additions and 335 deletions
|
|
@ -25,10 +25,18 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
// TODO: Make UI provider-agnostic
|
||||
interface TelephonyConfigForm {
|
||||
provider: string;
|
||||
account_sid: string;
|
||||
auth_token: string;
|
||||
// Twilio fields
|
||||
account_sid?: string;
|
||||
auth_token?: string;
|
||||
// Vonage fields
|
||||
application_id?: string;
|
||||
private_key?: string;
|
||||
api_key?: string;
|
||||
api_secret?: string;
|
||||
// Common field
|
||||
from_number: string;
|
||||
}
|
||||
|
||||
|
|
@ -70,13 +78,26 @@ export default function ConfigureTelephonyPage() {
|
|||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
if (!response.error && response.data?.twilio) {
|
||||
setHasExistingConfig(true);
|
||||
// Masked values like "****************def0" from backend
|
||||
setValue("account_sid", response.data.twilio.account_sid);
|
||||
setValue("auth_token", response.data.twilio.auth_token);
|
||||
if (response.data.twilio.from_numbers?.length > 0) {
|
||||
setValue("from_number", response.data.twilio.from_numbers[0]);
|
||||
if (!response.error) {
|
||||
// Simple single provider config
|
||||
if (response.data?.twilio) {
|
||||
setHasExistingConfig(true);
|
||||
setValue("provider", "twilio");
|
||||
setValue("account_sid", response.data.twilio.account_sid);
|
||||
setValue("auth_token", response.data.twilio.auth_token);
|
||||
if (response.data.twilio.from_numbers?.length > 0) {
|
||||
setValue("from_number", response.data.twilio.from_numbers[0]);
|
||||
}
|
||||
} else if (response.data?.vonage) {
|
||||
setHasExistingConfig(true);
|
||||
setValue("provider", "vonage");
|
||||
setValue("application_id", response.data.vonage.application_id);
|
||||
setValue("private_key", response.data.vonage.private_key);
|
||||
setValue("api_key", response.data.vonage.api_key || "");
|
||||
setValue("api_secret", response.data.vonage.api_secret || "");
|
||||
if (response.data.vonage.from_numbers?.length > 0) {
|
||||
setValue("from_number", response.data.vonage.from_numbers[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -92,14 +113,26 @@ export default function ConfigureTelephonyPage() {
|
|||
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
|
||||
// Build the request body based on provider
|
||||
let requestBody: any = {
|
||||
provider: data.provider,
|
||||
from_numbers: [data.from_number],
|
||||
};
|
||||
|
||||
if (data.provider === "twilio") {
|
||||
requestBody.account_sid = data.account_sid;
|
||||
requestBody.auth_token = data.auth_token;
|
||||
} else if (data.provider === "vonage") {
|
||||
requestBody.application_id = data.application_id;
|
||||
requestBody.private_key = data.private_key;
|
||||
requestBody.api_key = data.api_key;
|
||||
requestBody.api_secret = data.api_secret;
|
||||
}
|
||||
|
||||
const response = await saveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPost({
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body: {
|
||||
provider: data.provider,
|
||||
account_sid: data.account_sid,
|
||||
auth_token: data.auth_token,
|
||||
from_numbers: [data.from_number],
|
||||
},
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
|
|
@ -177,8 +210,14 @@ export default function ConfigureTelephonyPage() {
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="twilio">Twilio</SelectItem>
|
||||
<SelectItem value="vonage">Vonage</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{hasExistingConfig && (
|
||||
<p className="text-sm text-amber-600">
|
||||
⚠️ Switching providers will require entering new credentials
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Twilio-specific fields */}
|
||||
|
|
@ -249,6 +288,87 @@ export default function ConfigureTelephonyPage() {
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* Vonage-specific fields */}
|
||||
{selectedProvider === "vonage" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="application_id">Application ID</Label>
|
||||
<Input
|
||||
id="application_id"
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
{...register("application_id", {
|
||||
required: selectedProvider === "vonage" ? "Application ID is required" : false,
|
||||
})}
|
||||
/>
|
||||
{errors.application_id && (
|
||||
<p className="text-sm text-red-500">
|
||||
{errors.application_id.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="private_key">Private Key</Label>
|
||||
<textarea
|
||||
id="private_key"
|
||||
className="w-full min-h-[100px] px-3 py-2 text-sm border rounded-md"
|
||||
placeholder="-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"
|
||||
{...register("private_key", {
|
||||
required: selectedProvider === "vonage" && !hasExistingConfig
|
||||
? "Private key is required"
|
||||
: false,
|
||||
})}
|
||||
/>
|
||||
{errors.private_key && (
|
||||
<p className="text-sm text-red-500">
|
||||
{errors.private_key.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_key">API Key (Optional)</Label>
|
||||
<Input
|
||||
id="api_key"
|
||||
placeholder="Optional - for some operations"
|
||||
{...register("api_key")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_secret">API Secret (Optional)</Label>
|
||||
<Input
|
||||
id="api_secret"
|
||||
type="password"
|
||||
placeholder="Optional - for webhook verification"
|
||||
{...register("api_secret")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="from_number">From Phone Number</Label>
|
||||
<Input
|
||||
id="from_number"
|
||||
autoComplete="tel"
|
||||
placeholder="14155551234 (no + prefix for Vonage)"
|
||||
{...register("from_number", {
|
||||
required: "Phone number is required",
|
||||
pattern: {
|
||||
value: /^[1-9]\d{1,14}$/,
|
||||
message:
|
||||
"Enter a valid phone number without + prefix (e.g., 14155551234)",
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.from_number && (
|
||||
<p className="text-sm text-red-500">
|
||||
{errors.from_number.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { PhoneInput } from 'react-international-phone';
|
||||
|
||||
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, initiateCallApiV1TwilioInitiateCallPost } from '@/client/sdk.gen';
|
||||
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, initiateCallApiV1TelephonyInitiateCallPost } from '@/client/sdk.gen';
|
||||
import { WorkflowError } from '@/client/types.gen';
|
||||
import { FlowEdge, FlowNode } from "@/components/flow/types";
|
||||
import { OnboardingTooltip } from '@/components/onboarding/OnboardingTooltip';
|
||||
|
|
@ -117,7 +117,8 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
|
|||
});
|
||||
|
||||
// If no configuration exists, show configure dialog
|
||||
if (configResponse.error || !configResponse.data?.twilio) {
|
||||
// Check if any telephony provider is configured (Twilio or Vonage)
|
||||
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage)) {
|
||||
setConfigureDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
|
@ -151,8 +152,11 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
|
|||
}
|
||||
|
||||
// Configuration exists, proceed with call initiation
|
||||
const response = await initiateCallApiV1TwilioInitiateCallPost({
|
||||
body: { workflow_id: workflowId },
|
||||
const response = await initiateCallApiV1TelephonyInitiateCallPost({
|
||||
body: {
|
||||
workflow_id: workflowId,
|
||||
phone_number: phoneNumber
|
||||
},
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { type ClientOptions as DefaultClientOptions, type Config, createClient, createConfig } from '@hey-api/client-fetch';
|
||||
|
||||
import { createClientConfig } from '../lib/apiClient';
|
||||
import type { ClientOptions } from './types.gen';
|
||||
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
|
||||
import { createClientConfig } from '../lib/apiClient';
|
||||
|
||||
/**
|
||||
* The `createClientConfig()` function will be called on client initialization
|
||||
|
|
@ -17,4 +16,4 @@ export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
|
|||
|
||||
export const client = createClient(createClientConfig(createConfig<ClientOptions>({
|
||||
baseUrl: 'http://127.0.0.1:8000'
|
||||
})));
|
||||
})));
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
export * from './sdk.gen';
|
||||
export * from './types.gen';
|
||||
export * from './sdk.gen';
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -261,11 +261,6 @@ export type ImpersonateResponse = {
|
|||
access_token: string;
|
||||
};
|
||||
|
||||
export type InitiateCallRequest = {
|
||||
workflow_id: number;
|
||||
workflow_run_id?: number | null;
|
||||
};
|
||||
|
||||
export type IntegrationResponse = {
|
||||
id: number;
|
||||
integration_id: string;
|
||||
|
|
@ -390,6 +385,7 @@ export type SuperuserWorkflowRunsListResponse = {
|
|||
*/
|
||||
export type TelephonyConfigurationResponse = {
|
||||
twilio?: TwilioConfigurationResponse | null;
|
||||
vonage?: VonageConfigurationResponse | null;
|
||||
};
|
||||
|
||||
export type TestSessionResponse = {
|
||||
|
|
@ -502,6 +498,45 @@ export type ValidationError = {
|
|||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request schema for Vonage configuration.
|
||||
*/
|
||||
export type VonageConfigurationRequest = {
|
||||
provider?: string;
|
||||
/**
|
||||
* Vonage API Key
|
||||
*/
|
||||
api_key?: string | null;
|
||||
/**
|
||||
* Vonage API Secret
|
||||
*/
|
||||
api_secret?: string | null;
|
||||
/**
|
||||
* Vonage Application ID
|
||||
*/
|
||||
application_id: string;
|
||||
/**
|
||||
* Private key for JWT generation
|
||||
*/
|
||||
private_key: string;
|
||||
/**
|
||||
* List of Vonage phone numbers (without + prefix)
|
||||
*/
|
||||
from_numbers: Array<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Response schema for Vonage configuration with masked sensitive fields.
|
||||
*/
|
||||
export type VonageConfigurationResponse = {
|
||||
provider: string;
|
||||
application_id: string;
|
||||
api_key: string | null;
|
||||
api_secret: string | null;
|
||||
private_key: string;
|
||||
from_numbers: Array<string>;
|
||||
};
|
||||
|
||||
export type WorkflowError = {
|
||||
kind: ItemKind;
|
||||
id: string | null;
|
||||
|
|
@ -621,8 +656,110 @@ export type WorkflowTemplateResponse = {
|
|||
created_at: string;
|
||||
};
|
||||
|
||||
export type ApiRoutesTelephonyInitiateCallRequest = {
|
||||
workflow_id: number;
|
||||
workflow_run_id?: number | null;
|
||||
phone_number?: string | null;
|
||||
};
|
||||
|
||||
export type ApiRoutesTwilioInitiateCallRequest = {
|
||||
workflow_id: number;
|
||||
workflow_run_id?: number | null;
|
||||
};
|
||||
|
||||
export type InitiateCallApiV1TelephonyInitiateCallPostData = {
|
||||
body: ApiRoutesTelephonyInitiateCallRequest;
|
||||
headers?: {
|
||||
authorization?: string | null;
|
||||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/telephony/initiate-call';
|
||||
};
|
||||
|
||||
export type InitiateCallApiV1TelephonyInitiateCallPostErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type InitiateCallApiV1TelephonyInitiateCallPostError = InitiateCallApiV1TelephonyInitiateCallPostErrors[keyof InitiateCallApiV1TelephonyInitiateCallPostErrors];
|
||||
|
||||
export type InitiateCallApiV1TelephonyInitiateCallPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
'x-twilio-signature'?: string | null;
|
||||
};
|
||||
path: {
|
||||
workflow_run_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/telephony/status-callback/{workflow_run_id}';
|
||||
};
|
||||
|
||||
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostError = HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors[keyof HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors];
|
||||
|
||||
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
workflow_run_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/telephony/events/{workflow_run_id}';
|
||||
};
|
||||
|
||||
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostError = HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostErrors[keyof HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostErrors];
|
||||
|
||||
export type HandleVonageEventsApiV1TelephonyEventsWorkflowRunIdPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type InitiateCallApiV1TwilioInitiateCallPostData = {
|
||||
body: InitiateCallRequest;
|
||||
body: ApiRoutesTwilioInitiateCallRequest;
|
||||
headers?: {
|
||||
authorization?: string | null;
|
||||
};
|
||||
|
|
@ -1957,7 +2094,9 @@ export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData =
|
|||
authorization?: string | null;
|
||||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
query?: {
|
||||
provider?: string | null;
|
||||
};
|
||||
url: '/api/v1/organizations/telephony-config';
|
||||
};
|
||||
|
||||
|
|
@ -1984,7 +2123,7 @@ export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetRespons
|
|||
export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse = GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses[keyof GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses];
|
||||
|
||||
export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData = {
|
||||
body: TwilioConfigurationRequest;
|
||||
body: TwilioConfigurationRequest | VonageConfigurationRequest;
|
||||
headers?: {
|
||||
authorization?: string | null;
|
||||
};
|
||||
|
|
@ -2809,4 +2948,4 @@ export type HealthApiV1HealthGetResponses = {
|
|||
|
||||
export type ClientOptions = {
|
||||
baseUrl: 'http://127.0.0.1:8000' | (string & {});
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue