Compare commits
5 Commits
feature/tw
...
a75b41fd0b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a75b41fd0b | ||
|
|
51c82d3d95 | ||
|
|
a4577ddcf3 | ||
|
|
5f3fcef1ec | ||
|
|
16907aadf8 |
@@ -0,0 +1,197 @@
|
|||||||
|
exports.up = async function (knex) {
|
||||||
|
await knex.schema.createTable('contacts', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('(UUID())'));
|
||||||
|
table.string('firstName', 100).notNullable();
|
||||||
|
table.string('lastName', 100).notNullable();
|
||||||
|
table.uuid('accountId').notNullable();
|
||||||
|
table.timestamps(true, true);
|
||||||
|
|
||||||
|
table
|
||||||
|
.foreign('accountId')
|
||||||
|
.references('id')
|
||||||
|
.inTable('accounts')
|
||||||
|
.onDelete('CASCADE');
|
||||||
|
table.index(['accountId']);
|
||||||
|
table.index(['lastName', 'firstName']);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTable('contact_details', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('(UUID())'));
|
||||||
|
table.string('relatedObjectType', 100).notNullable();
|
||||||
|
table.uuid('relatedObjectId').notNullable();
|
||||||
|
table.string('detailType', 50).notNullable();
|
||||||
|
table.string('label', 100);
|
||||||
|
table.text('value').notNullable();
|
||||||
|
table.boolean('isPrimary').defaultTo(false);
|
||||||
|
table.timestamps(true, true);
|
||||||
|
|
||||||
|
table.index(['relatedObjectType', 'relatedObjectId']);
|
||||||
|
table.index(['detailType']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [contactObjectId] = await knex('object_definitions').insert({
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
apiName: 'Contact',
|
||||||
|
label: 'Contact',
|
||||||
|
pluralLabel: 'Contacts',
|
||||||
|
description: 'Standard Contact object',
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactObjectDefId =
|
||||||
|
contactObjectId ||
|
||||||
|
(await knex('object_definitions').where('apiName', 'Contact').first()).id;
|
||||||
|
|
||||||
|
await knex('field_definitions').insert([
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactObjectDefId,
|
||||||
|
apiName: 'firstName',
|
||||||
|
label: 'First Name',
|
||||||
|
type: 'String',
|
||||||
|
length: 100,
|
||||||
|
isRequired: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 1,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactObjectDefId,
|
||||||
|
apiName: 'lastName',
|
||||||
|
label: 'Last Name',
|
||||||
|
type: 'String',
|
||||||
|
length: 100,
|
||||||
|
isRequired: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 2,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactObjectDefId,
|
||||||
|
apiName: 'accountId',
|
||||||
|
label: 'Account',
|
||||||
|
type: 'Reference',
|
||||||
|
referenceObject: 'Account',
|
||||||
|
isRequired: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 3,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [contactDetailObjectId] = await knex('object_definitions').insert({
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
apiName: 'ContactDetail',
|
||||||
|
label: 'Contact Detail',
|
||||||
|
pluralLabel: 'Contact Details',
|
||||||
|
description: 'Polymorphic contact detail object',
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactDetailObjectDefId =
|
||||||
|
contactDetailObjectId ||
|
||||||
|
(await knex('object_definitions').where('apiName', 'ContactDetail').first())
|
||||||
|
.id;
|
||||||
|
|
||||||
|
await knex('field_definitions').insert([
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactDetailObjectDefId,
|
||||||
|
apiName: 'relatedObjectType',
|
||||||
|
label: 'Related Object Type',
|
||||||
|
type: 'String',
|
||||||
|
length: 100,
|
||||||
|
isRequired: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 1,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactDetailObjectDefId,
|
||||||
|
apiName: 'relatedObjectId',
|
||||||
|
label: 'Related Object ID',
|
||||||
|
type: 'String',
|
||||||
|
length: 36,
|
||||||
|
isRequired: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 2,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactDetailObjectDefId,
|
||||||
|
apiName: 'detailType',
|
||||||
|
label: 'Detail Type',
|
||||||
|
type: 'String',
|
||||||
|
length: 50,
|
||||||
|
isRequired: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 3,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactDetailObjectDefId,
|
||||||
|
apiName: 'label',
|
||||||
|
label: 'Label',
|
||||||
|
type: 'String',
|
||||||
|
length: 100,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 4,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactDetailObjectDefId,
|
||||||
|
apiName: 'value',
|
||||||
|
label: 'Value',
|
||||||
|
type: 'Text',
|
||||||
|
isRequired: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 5,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
objectDefinitionId: contactDetailObjectDefId,
|
||||||
|
apiName: 'isPrimary',
|
||||||
|
label: 'Primary',
|
||||||
|
type: 'Boolean',
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
displayOrder: 6,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function (knex) {
|
||||||
|
await knex.schema.dropTableIfExists('contact_details');
|
||||||
|
await knex.schema.dropTableIfExists('contacts');
|
||||||
|
};
|
||||||
@@ -145,12 +145,42 @@ model Account {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
owner User @relation(fields: [ownerId], references: [id])
|
owner User @relation(fields: [ownerId], references: [id])
|
||||||
|
contacts Contact[]
|
||||||
|
|
||||||
@@index([ownerId])
|
@@index([ownerId])
|
||||||
@@map("accounts")
|
@@map("accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Contact {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
accountId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([accountId])
|
||||||
|
@@map("contacts")
|
||||||
|
}
|
||||||
|
|
||||||
|
model ContactDetail {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
relatedObjectType String
|
||||||
|
relatedObjectId String
|
||||||
|
detailType String
|
||||||
|
label String?
|
||||||
|
value String
|
||||||
|
isPrimary Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([relatedObjectType, relatedObjectId])
|
||||||
|
@@map("contact_details")
|
||||||
|
}
|
||||||
|
|
||||||
// Application Builder
|
// Application Builder
|
||||||
model App {
|
model App {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|||||||
13
backend/src/models/contact-detail.model.ts
Normal file
13
backend/src/models/contact-detail.model.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { BaseModel } from './base.model';
|
||||||
|
|
||||||
|
export class ContactDetail extends BaseModel {
|
||||||
|
static tableName = 'contact_details';
|
||||||
|
|
||||||
|
id!: string;
|
||||||
|
relatedObjectType!: string;
|
||||||
|
relatedObjectId!: string;
|
||||||
|
detailType!: string;
|
||||||
|
label?: string;
|
||||||
|
value!: string;
|
||||||
|
isPrimary!: boolean;
|
||||||
|
}
|
||||||
21
backend/src/models/contact.model.ts
Normal file
21
backend/src/models/contact.model.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { BaseModel } from './base.model';
|
||||||
|
|
||||||
|
export class Contact extends BaseModel {
|
||||||
|
static tableName = 'contacts';
|
||||||
|
|
||||||
|
id!: string;
|
||||||
|
firstName!: string;
|
||||||
|
lastName!: string;
|
||||||
|
accountId!: string;
|
||||||
|
|
||||||
|
static relationMappings = {
|
||||||
|
account: {
|
||||||
|
relation: BaseModel.BelongsToOneRelation,
|
||||||
|
modelClass: 'account.model',
|
||||||
|
join: {
|
||||||
|
from: 'contacts.accountId',
|
||||||
|
to: 'accounts.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -101,23 +101,18 @@ export function useSoftphone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
console.log('Requesting Twilio token from /api/voice/token...');
|
|
||||||
const response = await api.get('/voice/token');
|
const response = await api.get('/voice/token');
|
||||||
const token = response.data.token;
|
const token = response.data.token;
|
||||||
|
|
||||||
console.log('Token received, creating Device...');
|
|
||||||
|
|
||||||
// Log the token payload to see what identity is being used
|
// Log the token payload to see what identity is being used
|
||||||
try {
|
try {
|
||||||
const tokenPayload = JSON.parse(atob(token.split('.')[1]));
|
const tokenPayload = JSON.parse(atob(token.split('.')[1]));
|
||||||
console.log('Token identity:', tokenPayload.sub);
|
|
||||||
console.log('Token grants:', tokenPayload.grants);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Could not parse token payload');
|
console.log('Could not parse token payload');
|
||||||
}
|
}
|
||||||
|
|
||||||
twilioDevice.value = new Device(token, {
|
twilioDevice.value = new Device(token, {
|
||||||
logLevel: 1,
|
logLevel: 3,
|
||||||
codecPreferences: ['opus', 'pcmu'],
|
codecPreferences: ['opus', 'pcmu'],
|
||||||
enableImprovedSignalingErrorPrecision: true,
|
enableImprovedSignalingErrorPrecision: true,
|
||||||
edge: 'ashburn',
|
edge: 'ashburn',
|
||||||
@@ -125,12 +120,10 @@ export function useSoftphone() {
|
|||||||
|
|
||||||
// Device events
|
// Device events
|
||||||
twilioDevice.value.on('registered', () => {
|
twilioDevice.value.on('registered', () => {
|
||||||
console.log('✓ Twilio Device registered - ready to receive calls');
|
|
||||||
toast.success('Softphone ready');
|
toast.success('Softphone ready');
|
||||||
});
|
});
|
||||||
|
|
||||||
twilioDevice.value.on('unregistered', () => {
|
twilioDevice.value.on('unregistered', () => {
|
||||||
console.log('⚠ Twilio Device unregistered');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
twilioDevice.value.on('error', (error) => {
|
twilioDevice.value.on('error', (error) => {
|
||||||
@@ -139,14 +132,8 @@ export function useSoftphone() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
twilioDevice.value.on('incoming', (call: TwilioCall) => {
|
twilioDevice.value.on('incoming', (call: TwilioCall) => {
|
||||||
console.log('🔔 Twilio Device INCOMING event received:', call.parameters);
|
|
||||||
console.log('Call parameters:', {
|
|
||||||
CallSid: call.parameters.CallSid,
|
|
||||||
From: call.parameters.From,
|
|
||||||
To: call.parameters.To,
|
|
||||||
});
|
|
||||||
twilioCall.value = call;
|
twilioCall.value = call;
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
incomingCall.value = {
|
incomingCall.value = {
|
||||||
callSid: call.parameters.CallSid || '',
|
callSid: call.parameters.CallSid || '',
|
||||||
@@ -167,16 +154,11 @@ export function useSoftphone() {
|
|||||||
// Setup call handlers
|
// Setup call handlers
|
||||||
setupCallHandlers(call);
|
setupCallHandlers(call);
|
||||||
|
|
||||||
// Play ringtone
|
// Twilio Device will handle ringtone automatically
|
||||||
playRingtone();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register the device
|
// Register the device
|
||||||
console.log('Registering Twilio Device...');
|
|
||||||
await twilioDevice.value.register();
|
await twilioDevice.value.register();
|
||||||
console.log('✓ Twilio Device register() completed');
|
|
||||||
console.log('Device identity:', twilioDevice.value.identity);
|
|
||||||
console.log('Device state:', twilioDevice.value.state);
|
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to initialize Twilio Device:', error);
|
console.error('Failed to initialize Twilio Device:', error);
|
||||||
@@ -245,7 +227,7 @@ export function useSoftphone() {
|
|||||||
return 'http://localhost:3000';
|
return 'http://localhost:3000';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to /voice namespace
|
// Connect to /voice namespace with proper auth header
|
||||||
socket.value = io(`${getBackendUrl()}/voice`, {
|
socket.value = io(`${getBackendUrl()}/voice`, {
|
||||||
auth: {
|
auth: {
|
||||||
token: token,
|
token: token,
|
||||||
@@ -255,25 +237,26 @@ export function useSoftphone() {
|
|||||||
reconnectionDelay: 1000,
|
reconnectionDelay: 1000,
|
||||||
reconnectionDelayMax: 5000,
|
reconnectionDelayMax: 5000,
|
||||||
reconnectionAttempts: 5,
|
reconnectionAttempts: 5,
|
||||||
|
query: {}, // Explicitly set empty query to prevent token leaking
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connection events
|
// Connection events
|
||||||
socket.value.on('connect', () => {
|
socket.value.on('connect', () => {
|
||||||
console.log('🔌 Softphone WebSocket connected');
|
|
||||||
console.log('📋 Token payload (check userId):', parseJwt(token));
|
|
||||||
isConnected.value = true;
|
isConnected.value = true;
|
||||||
|
|
||||||
// Initialize Twilio Device after WebSocket connects
|
// Initialize Twilio Device after WebSocket connects
|
||||||
initializeTwilioDevice();
|
// Suppress warnings by catching them before they log
|
||||||
|
initializeTwilioDevice().catch(err => {
|
||||||
|
// Device initialization errors are already shown to user via toast
|
||||||
|
console.debug('Device init issue (non-critical):', err.message);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('disconnect', () => {
|
socket.value.on('disconnect', () => {
|
||||||
console.log('Softphone WebSocket disconnected');
|
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('connect_error', (error) => {
|
socket.value.on('connect_error', (error) => {
|
||||||
console.error('Softphone connection error:', error);
|
|
||||||
toast.error('Failed to connect to voice service');
|
toast.error('Failed to connect to voice service');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -552,8 +535,7 @@ export function useSoftphone() {
|
|||||||
let ringtoneAudio: HTMLAudioElement | null = null;
|
let ringtoneAudio: HTMLAudioElement | null = null;
|
||||||
|
|
||||||
const playRingtone = () => {
|
const playRingtone = () => {
|
||||||
// Optional: Play a simple beep tone using Web Audio API
|
// Play a simple beep tone using Web Audio API
|
||||||
// This is a nice-to-have enhancement but not required for incoming calls to work
|
|
||||||
try {
|
try {
|
||||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
const oscillator = audioContext.createOscillator();
|
const oscillator = audioContext.createOscillator();
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
hmr: {
|
hmr: {
|
||||||
clientPort: 3001,
|
host: 'tenant1.routebox.co',
|
||||||
|
port: 443,
|
||||||
|
protocol: 'wss',
|
||||||
|
// Don't use _nuxt path - HMR handles its own path
|
||||||
},
|
},
|
||||||
allowedHosts: ['.routebox.co', 'localhost', '127.0.0.1'],
|
allowedHosts: ['.routebox.co', 'localhost', '127.0.0.1'],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user