WIP - placing calls
This commit is contained in:
@@ -20,21 +20,22 @@ export class TenantController {
|
||||
* Get integrations configuration for the current tenant
|
||||
*/
|
||||
@Get('integrations')
|
||||
async getIntegrationsConfig(@TenantId() tenantId: string) {
|
||||
async getIntegrationsConfig(@TenantId() domain: string) {
|
||||
const centralPrisma = getCentralPrisma();
|
||||
|
||||
const tenant = await centralPrisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { integrationsConfig: true },
|
||||
// Look up tenant by domain
|
||||
const domainRecord = await centralPrisma.domain.findUnique({
|
||||
where: { domain },
|
||||
include: { tenant: { select: { id: true, integrationsConfig: true } } },
|
||||
});
|
||||
|
||||
if (!tenant || !tenant.integrationsConfig) {
|
||||
if (!domainRecord?.tenant || !domainRecord.tenant.integrationsConfig) {
|
||||
return { data: null };
|
||||
}
|
||||
|
||||
// Decrypt the config
|
||||
const config = this.tenantDbService.decryptIntegrationsConfig(
|
||||
tenant.integrationsConfig as any,
|
||||
domainRecord.tenant.integrationsConfig as any,
|
||||
);
|
||||
|
||||
// Return config with sensitive fields masked
|
||||
@@ -48,20 +49,34 @@ export class TenantController {
|
||||
*/
|
||||
@Put('integrations')
|
||||
async updateIntegrationsConfig(
|
||||
@TenantId() tenantId: string,
|
||||
@TenantId() domain: string,
|
||||
@Body() body: { integrationsConfig: any },
|
||||
) {
|
||||
const { integrationsConfig } = body;
|
||||
|
||||
if (!domain) {
|
||||
throw new Error('Domain is missing from request');
|
||||
}
|
||||
|
||||
// Look up tenant by domain
|
||||
const centralPrisma = getCentralPrisma();
|
||||
const domainRecord = await centralPrisma.domain.findUnique({
|
||||
where: { domain },
|
||||
include: { tenant: { select: { id: true } } },
|
||||
});
|
||||
|
||||
if (!domainRecord?.tenant) {
|
||||
throw new Error(`Tenant with domain ${domain} not found`);
|
||||
}
|
||||
|
||||
// Encrypt the config
|
||||
const encryptedConfig = this.tenantDbService.encryptIntegrationsConfig(
|
||||
integrationsConfig,
|
||||
);
|
||||
|
||||
// Update in database
|
||||
const centralPrisma = getCentralPrisma();
|
||||
await centralPrisma.tenant.update({
|
||||
where: { id: tenantId },
|
||||
where: { id: domainRecord.tenant.id },
|
||||
data: {
|
||||
integrationsConfig: encryptedConfig as any,
|
||||
},
|
||||
|
||||
@@ -56,13 +56,33 @@ export class VoiceGateway
|
||||
|
||||
// Verify JWT token
|
||||
const payload = await this.jwtService.verifyAsync(token);
|
||||
client.tenantId = payload.tenantId;
|
||||
|
||||
// Extract domain from origin header (e.g., http://tenant1.routebox.co:3001)
|
||||
// The domains table stores just the subdomain part (e.g., "tenant1")
|
||||
const origin = client.handshake.headers.origin || client.handshake.headers.referer;
|
||||
let domain = 'localhost';
|
||||
|
||||
if (origin) {
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
const hostname = url.hostname; // e.g., tenant1.routebox.co or localhost
|
||||
|
||||
// Extract first part of subdomain as domain
|
||||
// tenant1.routebox.co -> tenant1
|
||||
// localhost -> localhost
|
||||
domain = hostname.split('.')[0];
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to parse origin: ${origin}`);
|
||||
}
|
||||
}
|
||||
|
||||
client.tenantId = domain; // Store the subdomain as tenantId
|
||||
client.userId = payload.sub;
|
||||
client.tenantSlug = payload.tenantSlug;
|
||||
client.tenantSlug = domain; // Same as subdomain
|
||||
|
||||
this.connectedUsers.set(client.userId, client);
|
||||
this.logger.log(
|
||||
`Client connected: ${client.id} (User: ${client.userId}, Tenant: ${client.tenantSlug})`,
|
||||
`Client connected: ${client.id} (User: ${client.userId}, Domain: ${domain})`,
|
||||
);
|
||||
|
||||
// Send current call state if any active call
|
||||
|
||||
@@ -20,40 +20,57 @@ export class VoiceService {
|
||||
/**
|
||||
* Get Twilio client for a tenant
|
||||
*/
|
||||
private async getTwilioClient(tenantId: string): Promise<{ client: Twilio.Twilio; config: TwilioConfig }> {
|
||||
private async getTwilioClient(tenantIdOrDomain: string): Promise<{ client: Twilio.Twilio; config: TwilioConfig; tenantId: string }> {
|
||||
// Check cache first
|
||||
if (this.twilioClients.has(tenantId)) {
|
||||
if (this.twilioClients.has(tenantIdOrDomain)) {
|
||||
const centralPrisma = getCentralPrisma();
|
||||
const tenant = await centralPrisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { integrationsConfig: true },
|
||||
|
||||
// Look up tenant by domain
|
||||
const domainRecord = await centralPrisma.domain.findUnique({
|
||||
where: { domain: tenantIdOrDomain },
|
||||
include: { tenant: { select: { id: true, integrationsConfig: true } } },
|
||||
});
|
||||
|
||||
const config = this.getIntegrationConfig(tenant?.integrationsConfig as any);
|
||||
return { client: this.twilioClients.get(tenantId), config: config.twilio };
|
||||
const config = this.getIntegrationConfig(domainRecord?.tenant?.integrationsConfig as any);
|
||||
return {
|
||||
client: this.twilioClients.get(tenantIdOrDomain),
|
||||
config: config.twilio,
|
||||
tenantId: domainRecord.tenant.id
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch tenant integrations config
|
||||
const centralPrisma = getCentralPrisma();
|
||||
const tenant = await centralPrisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { integrationsConfig: true },
|
||||
|
||||
this.logger.log(`Looking up domain: ${tenantIdOrDomain}`);
|
||||
|
||||
const domainRecord = await centralPrisma.domain.findUnique({
|
||||
where: { domain: tenantIdOrDomain },
|
||||
include: { tenant: { select: { id: true, integrationsConfig: true } } },
|
||||
});
|
||||
|
||||
if (!tenant?.integrationsConfig) {
|
||||
throw new Error('Tenant integrations config not found');
|
||||
this.logger.log(`Domain record found: ${!!domainRecord}, Tenant: ${!!domainRecord?.tenant}, Config: ${!!domainRecord?.tenant?.integrationsConfig}`);
|
||||
|
||||
if (!domainRecord?.tenant) {
|
||||
throw new Error(`Domain ${tenantIdOrDomain} not found`);
|
||||
}
|
||||
|
||||
const config = this.getIntegrationConfig(tenant.integrationsConfig as any);
|
||||
if (!domainRecord.tenant.integrationsConfig) {
|
||||
throw new Error('Tenant integrations config not found. Please configure Twilio credentials in Settings > Integrations');
|
||||
}
|
||||
|
||||
const config = this.getIntegrationConfig(domainRecord.tenant.integrationsConfig as any);
|
||||
|
||||
this.logger.log(`Config decrypted: ${!!config.twilio}, AccountSid: ${config.twilio?.accountSid?.substring(0, 10)}..., AuthToken: ${config.twilio?.authToken?.substring(0, 10)}..., Phone: ${config.twilio?.phoneNumber}`);
|
||||
|
||||
if (!config.twilio?.accountSid || !config.twilio?.authToken) {
|
||||
throw new Error('Twilio credentials not configured for tenant');
|
||||
}
|
||||
|
||||
const client = Twilio.default(config.twilio.accountSid, config.twilio.authToken);
|
||||
this.twilioClients.set(tenantId, client);
|
||||
this.twilioClients.set(tenantIdOrDomain, client);
|
||||
|
||||
return { client, config: config.twilio };
|
||||
return { client, config: config.twilio, tenantId: domainRecord.tenant.id };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,28 +102,32 @@ export class VoiceService {
|
||||
userId: string;
|
||||
toNumber: string;
|
||||
}) {
|
||||
const { tenantId, userId, toNumber } = params;
|
||||
const { tenantId: tenantDomain, userId, toNumber } = params;
|
||||
|
||||
try {
|
||||
const { client, config } = await this.getTwilioClient(tenantId);
|
||||
const { client, config, tenantId } = await this.getTwilioClient(tenantDomain);
|
||||
|
||||
// Create call record in database
|
||||
const tenantKnex = await this.tenantDbService.getTenantKnexById(tenantId);
|
||||
const callId = uuidv4();
|
||||
|
||||
// Generate TwiML URL for call flow
|
||||
const twimlUrl = `${process.env.BACKEND_URL || 'http://localhost:3000'}/api/voice/twiml/outbound`;
|
||||
// Construct tenant-specific webhook URLs
|
||||
// The tenantDomain is the subdomain (e.g., "tenant1")
|
||||
const backendPort = process.env.PORT || '3000';
|
||||
const backendUrl = `http://${tenantDomain}.routebox.co:${backendPort}`;
|
||||
|
||||
const twimlUrl = `${backendUrl}/api/voice/twiml/outbound`;
|
||||
|
||||
// Initiate call via Twilio
|
||||
const call = await client.calls.create({
|
||||
to: toNumber,
|
||||
from: config.phoneNumber,
|
||||
url: twimlUrl,
|
||||
statusCallback: `${process.env.BACKEND_URL || 'http://localhost:3000'}/api/voice/webhook/status`,
|
||||
statusCallback: `${backendUrl}/api/voice/webhook/status`,
|
||||
statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'],
|
||||
statusCallbackMethod: 'POST',
|
||||
record: true,
|
||||
recordingStatusCallback: `${process.env.BACKEND_URL || 'http://localhost:3000'}/api/voice/webhook/recording`,
|
||||
recordingStatusCallback: `${backendUrl}/api/voice/webhook/recording`,
|
||||
});
|
||||
|
||||
// Store call in database
|
||||
|
||||
Reference in New Issue
Block a user