Twilio now handles only the in-browser dial pad (Access Token + browser-Device outbound TwiML); Telnyx takes over SMS, inbound voicemail, the AI voice conversation loop, and staff-triggered outbound calls via TeXML. Adds TelnyxSettings model, Communication.provider discriminator, new comms/routes modules, and a real Telnyx Settings page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
// NOTE: Twilio is now used only for the in-browser dial pad (Access Token
|
|
// minting + browser-Device outbound-call TwiML). Everything else — SMS,
|
|
// inbound/AI voice, staff-triggered outbound calls — moved to Telnyx, see
|
|
// routes/telnyx.ts and routes/telnyx-webhooks.ts.
|
|
import express, { Request, Response } from "express";
|
|
import twilio from "twilio";
|
|
import { storage } from "../storage";
|
|
|
|
const router = express.Router();
|
|
|
|
// GET /api/twilio/settings
|
|
router.get("/settings", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
|
|
const settings = await storage.getTwilioSettings(userId);
|
|
if (!settings) return res.status(200).json(null);
|
|
|
|
const templates = (settings.templates as Record<string, string>) || {};
|
|
return res.status(200).json({
|
|
id: settings.id,
|
|
accountSid: settings.accountSid,
|
|
authToken: settings.authToken,
|
|
phoneNumber: settings.phoneNumber,
|
|
twimlAppSid: templates["_twiml_app_sid"] || null,
|
|
});
|
|
} catch (err) {
|
|
return res.status(500).json({ error: "Failed to fetch Twilio settings", details: String(err) });
|
|
}
|
|
});
|
|
|
|
// PUT /api/twilio/settings
|
|
router.put("/settings", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
|
|
const { accountSid, authToken, phoneNumber, twimlAppSid } = req.body;
|
|
if (!accountSid?.trim() || !authToken?.trim() || !phoneNumber?.trim()) {
|
|
return res.status(400).json({ message: "accountSid, authToken, and phoneNumber are required" });
|
|
}
|
|
|
|
const settings = await storage.upsertTwilioSettings(userId, {
|
|
accountSid: accountSid.trim(),
|
|
authToken: authToken.trim(),
|
|
phoneNumber: phoneNumber.trim(),
|
|
twimlAppSid: twimlAppSid?.trim() || null,
|
|
});
|
|
|
|
const templates = (settings.templates as Record<string, string>) || {};
|
|
return res.status(200).json({
|
|
id: settings.id,
|
|
accountSid: settings.accountSid,
|
|
authToken: settings.authToken,
|
|
phoneNumber: settings.phoneNumber,
|
|
twimlAppSid: templates["_twiml_app_sid"] || null,
|
|
});
|
|
} catch (err) {
|
|
return res.status(500).json({ error: "Failed to save Twilio settings", details: String(err) });
|
|
}
|
|
});
|
|
|
|
// POST /api/twilio/voice-token
|
|
// Returns a short-lived Twilio Access Token so the browser Voice SDK can place calls.
|
|
router.post("/voice-token", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
|
|
|
const settings = await storage.getTwilioSettings(userId);
|
|
if (!settings) {
|
|
return res.status(400).json({ message: "Twilio is not configured. Please add your Twilio credentials in Settings." });
|
|
}
|
|
|
|
const templates = (settings.templates as Record<string, string>) || {};
|
|
const twimlAppSid = templates["_twiml_app_sid"]?.trim();
|
|
if (!twimlAppSid) {
|
|
return res.status(400).json({ message: "TwiML App SID is not configured. Please add it in Twilio Settings." });
|
|
}
|
|
|
|
const AccessToken = twilio.jwt.AccessToken;
|
|
const VoiceGrant = AccessToken.VoiceGrant;
|
|
|
|
const token = new AccessToken(
|
|
settings.accountSid,
|
|
settings.accountSid,
|
|
settings.authToken,
|
|
{ identity: `user-${userId}`, ttl: 3600 }
|
|
);
|
|
const voiceGrant = new VoiceGrant({ outgoingApplicationSid: twimlAppSid, incomingAllow: false });
|
|
token.addGrant(voiceGrant);
|
|
|
|
return res.status(200).json({ token: token.toJwt(), phoneNumber: settings.phoneNumber });
|
|
} catch (err: any) {
|
|
return res.status(500).json({ error: err.message || "Failed to generate voice token" });
|
|
}
|
|
});
|
|
|
|
export default router;
|