WIP - fix twilio functionality now that we use BFF
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ref, computed, onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { Device, Call as TwilioCall } from '@twilio/voice-sdk';
|
||||
import { useAuth } from './useAuth';
|
||||
@@ -44,17 +44,6 @@ const volume = ref(100);
|
||||
export function useSoftphone() {
|
||||
const auth = useAuth();
|
||||
|
||||
// Get token and tenantId from localStorage
|
||||
const getToken = () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('token');
|
||||
};
|
||||
|
||||
const getTenantId = () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('tenantId');
|
||||
};
|
||||
|
||||
// Computed properties
|
||||
const isInCall = computed(() => currentCall.value !== null);
|
||||
const hasIncomingCall = computed(() => incomingCall.value !== null);
|
||||
@@ -93,6 +82,12 @@ export function useSoftphone() {
|
||||
* Initialize Twilio Device
|
||||
*/
|
||||
const initializeTwilioDevice = async () => {
|
||||
// Prevent re-initialization if device already exists and is registered
|
||||
if (twilioDevice.value) {
|
||||
console.log('Twilio Device already exists, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// First, explicitly request microphone permission
|
||||
const hasPermission = await requestMicrophonePermission();
|
||||
@@ -107,12 +102,14 @@ export function useSoftphone() {
|
||||
// Log the token payload to see what identity is being used
|
||||
try {
|
||||
const tokenPayload = JSON.parse(atob(token.split('.')[1]));
|
||||
console.log('📱 Twilio Device identity:', tokenPayload.grants?.identity || tokenPayload.sub);
|
||||
} catch (e) {
|
||||
console.log('Could not parse token payload');
|
||||
}
|
||||
|
||||
console.log('📱 Creating new Twilio Device...');
|
||||
twilioDevice.value = new Device(token, {
|
||||
logLevel: 3,
|
||||
logLevel: 1, // Reduce log level (1 = errors only, 3 = debug)
|
||||
codecPreferences: ['opus', 'pcmu'],
|
||||
enableImprovedSignalingErrorPrecision: true,
|
||||
edge: 'ashburn',
|
||||
@@ -120,10 +117,12 @@ export function useSoftphone() {
|
||||
|
||||
// Device events
|
||||
twilioDevice.value.on('registered', () => {
|
||||
console.log('✅ Twilio Device registered successfully');
|
||||
toast.success('Softphone ready');
|
||||
});
|
||||
|
||||
twilioDevice.value.on('unregistered', () => {
|
||||
console.log('📱 Twilio Device unregistered');
|
||||
});
|
||||
|
||||
twilioDevice.value.on('error', (error) => {
|
||||
@@ -132,6 +131,11 @@ export function useSoftphone() {
|
||||
});
|
||||
|
||||
twilioDevice.value.on('incoming', (call: TwilioCall) => {
|
||||
console.log('📞 Twilio Device incoming call event!', {
|
||||
callSid: call.parameters.CallSid,
|
||||
from: call.parameters.From,
|
||||
to: call.parameters.To,
|
||||
});
|
||||
twilioCall.value = call;
|
||||
|
||||
// Update state
|
||||
@@ -210,35 +214,54 @@ export function useSoftphone() {
|
||||
/**
|
||||
* Initialize WebSocket connection
|
||||
*/
|
||||
const connect = () => {
|
||||
const token = getToken();
|
||||
|
||||
if (socket.value?.connected || !token) {
|
||||
const connect = async () => {
|
||||
// Guard against multiple connection attempts
|
||||
if (socket.value) {
|
||||
console.log('Softphone: Socket already exists, skipping connection');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use same pattern as useApi to preserve subdomain for multi-tenant
|
||||
const getBackendUrl = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const currentHost = window.location.hostname;
|
||||
const protocol = window.location.protocol;
|
||||
return `${protocol}//${currentHost}`;
|
||||
// Check if user is authenticated
|
||||
if (!auth.isAuthenticated.value) {
|
||||
// Try to verify authentication first
|
||||
const isValid = await auth.checkAuth();
|
||||
if (!isValid) {
|
||||
console.log('Softphone: User not authenticated, skipping connection');
|
||||
return;
|
||||
}
|
||||
return 'http://localhost:3000';
|
||||
};
|
||||
}
|
||||
|
||||
// Connect to /voice namespace with proper auth header
|
||||
socket.value = io(`${getBackendUrl()}/voice`, {
|
||||
auth: {
|
||||
token: token,
|
||||
},
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
reconnectionAttempts: 5,
|
||||
query: {}, // Explicitly set empty query to prevent token leaking
|
||||
});
|
||||
try {
|
||||
// Get WebSocket token from BFF (this retrieves the token from HTTP-only cookie server-side)
|
||||
const wsAuth = await $fetch('/api/auth/ws-token');
|
||||
|
||||
if (!wsAuth.token) {
|
||||
console.log('Softphone: No WebSocket token available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use same pattern as useApi to preserve subdomain for multi-tenant
|
||||
const getBackendUrl = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const currentHost = window.location.hostname;
|
||||
const protocol = window.location.protocol;
|
||||
return `${protocol}//${currentHost}`;
|
||||
}
|
||||
return 'http://localhost:3000';
|
||||
};
|
||||
|
||||
// Connect to /voice namespace with proper auth header
|
||||
socket.value = io(`${getBackendUrl()}/voice`, {
|
||||
auth: {
|
||||
token: wsAuth.token,
|
||||
},
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
reconnectionAttempts: 5,
|
||||
query: {}, // Explicitly set empty query to prevent token leaking
|
||||
});
|
||||
|
||||
// Connection events
|
||||
socket.value.on('connect', () => {
|
||||
@@ -279,6 +302,10 @@ export function useSoftphone() {
|
||||
socket.value.on('ai:action', handleAiAction);
|
||||
|
||||
isInitialized.value = true;
|
||||
} catch (error: any) {
|
||||
console.error('Softphone: Failed to connect:', error);
|
||||
toast.error('Failed to initialize voice service');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -569,14 +596,25 @@ export function useSoftphone() {
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-connect on mount if token is available
|
||||
onMounted(() => {
|
||||
if (getToken() && !isInitialized.value) {
|
||||
// Only set up auto-connect and watchers once (not for every component that uses this composable)
|
||||
// Use a module-level flag to track if watchers are already set up
|
||||
if (process.client && !isInitialized.value && !socket.value) {
|
||||
// Auto-connect if authenticated
|
||||
if (auth.isAuthenticated.value) {
|
||||
connect();
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
// Watch for authentication changes to connect/disconnect
|
||||
watch(() => auth.isAuthenticated.value, async (isAuth) => {
|
||||
if (isAuth && !isInitialized.value && !socket.value) {
|
||||
await connect();
|
||||
} else if (!isAuth && isInitialized.value) {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup on unmount - only stop ringtone, don't disconnect shared socket
|
||||
onUnmounted(() => {
|
||||
stopRingtone();
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@ export default defineEventHandler(async (event) => {
|
||||
const queryString = new URLSearchParams(query as Record<string, string>).toString()
|
||||
const fullUrl = `${backendUrl}/api/${path}${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
console.log(`[BFF Proxy] ${method} ${fullUrl} (subdomain: ${subdomain}, hasToken: ${!!token})`)
|
||||
|
||||
// Build headers to forward
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': getHeader(event, 'content-type') || 'application/json',
|
||||
@@ -74,8 +76,14 @@ export default defineEventHandler(async (event) => {
|
||||
errorMessage = errorData.message || errorMessage
|
||||
} catch {
|
||||
// Response wasn't JSON
|
||||
try {
|
||||
const text = await response.text()
|
||||
console.error(`[BFF Proxy] Backend error (non-JSON): ${text}`)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
console.error(`[BFF Proxy] Backend returned ${response.status}: ${errorMessage}`, errorData)
|
||||
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: errorMessage,
|
||||
|
||||
26
frontend/server/api/auth/ws-token.get.ts
Normal file
26
frontend/server/api/auth/ws-token.get.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineEventHandler, createError } from 'h3'
|
||||
import { getSubdomainFromRequest } from '~/server/utils/tenant'
|
||||
import { getSessionToken } from '~/server/utils/session'
|
||||
|
||||
/**
|
||||
* Get a short-lived token for WebSocket authentication
|
||||
* This is needed because socket.io cannot use HTTP-only cookies directly
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const subdomain = getSubdomainFromRequest(event)
|
||||
const token = getSessionToken(event)
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Not authenticated',
|
||||
})
|
||||
}
|
||||
|
||||
// Return the token for WebSocket use
|
||||
// The token is already validated by being in the HTTP-only cookie
|
||||
return {
|
||||
token,
|
||||
subdomain,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user