Files
DentalManagement09/apps/Backend/src/routes/twilio.ts
T
GiteadandClaude Sonnet 5 79ac6aaa31 feat: migrate SMS/voice to Telnyx (TeXML), keep browser dial pad on Twilio
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>
2026-09-03 15:39:43 -04:00

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;