feat: add REST + Socket.IO fallback for Python server
Add RestSocketAdapter that uses: - REST API for actions (login, logout, terminals) - Socket.IO for real-time events (IpbxEvent) ConnectionManager now tries SignalR first (.NET server), then falls back to REST+SocketIO (Python server). This enables the client to work with both servers during migration.
This commit is contained in:
285
rest-socket-adapter.js
Normal file
285
rest-socket-adapter.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Adaptateur REST + Socket.IO pour le serveur Python
|
||||
*
|
||||
* Utilise REST pour les actions (login, logout, terminaux)
|
||||
* et Socket.IO pour les événements temps réel (IpbxEvent)
|
||||
*
|
||||
* Émule l'API SignalR pour compatibilité avec le code existant
|
||||
*/
|
||||
|
||||
const io = require('socket.io-client');
|
||||
|
||||
class RestSocketAdapter {
|
||||
constructor(serverUrl, options = {}) {
|
||||
// Nettoyer l'URL
|
||||
this.serverUrl = serverUrl;
|
||||
if (this.serverUrl.includes('/signalR')) {
|
||||
this.serverUrl = this.serverUrl.replace('/signalR', '');
|
||||
}
|
||||
if (!this.serverUrl.startsWith('http://') && !this.serverUrl.startsWith('https://')) {
|
||||
this.serverUrl = `http://${this.serverUrl}`;
|
||||
}
|
||||
|
||||
this.socket = null;
|
||||
this.connected = false;
|
||||
this.handlers = new Map();
|
||||
this.options = options;
|
||||
|
||||
// Token JWT pour les requêtes authentifiées
|
||||
this.authToken = null;
|
||||
this.currentAgent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Émule connection.start() de SignalR
|
||||
* Établit la connexion Socket.IO pour les événements
|
||||
*/
|
||||
async start() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`[RestSocketAdapter] Connexion à ${this.serverUrl}...`);
|
||||
|
||||
// Vérifier d'abord que le serveur Python répond
|
||||
fetch(`${this.serverUrl}/health`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Health check failed: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(health => {
|
||||
console.log(`[RestSocketAdapter] Serveur Python détecté:`, health);
|
||||
|
||||
// Connecter Socket.IO pour les événements temps réel
|
||||
this.socket = io(this.serverUrl, {
|
||||
path: '/socket.io/',
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 2000,
|
||||
...this.options
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('[RestSocketAdapter] Socket.IO connecté');
|
||||
this.connected = true;
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.socket.on('connect_error', (error) => {
|
||||
console.error('[RestSocketAdapter] Erreur Socket.IO:', error.message);
|
||||
// On reste connecté même si Socket.IO échoue (REST fonctionne)
|
||||
if (!this.connected) {
|
||||
this.connected = true;
|
||||
console.log('[RestSocketAdapter] Mode REST seul (Socket.IO indisponible)');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
console.log('[RestSocketAdapter] Socket.IO déconnecté:', reason);
|
||||
});
|
||||
|
||||
// Configurer les handlers d'événements existants
|
||||
this.handlers.forEach((handler, eventName) => {
|
||||
this.socket.on(eventName, (...args) => {
|
||||
console.log(`[RestSocketAdapter] Événement reçu: ${eventName}`, args);
|
||||
handler(eventName, args);
|
||||
});
|
||||
});
|
||||
|
||||
// Timeout de connexion
|
||||
setTimeout(() => {
|
||||
if (!this.connected) {
|
||||
reject(new Error('Timeout de connexion au serveur Python'));
|
||||
}
|
||||
}, 10000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[RestSocketAdapter] Serveur Python non disponible:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Émule connection.stop() de SignalR
|
||||
*/
|
||||
async stop() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
this.connected = false;
|
||||
this.authToken = null;
|
||||
this.currentAgent = null;
|
||||
console.log('[RestSocketAdapter] Déconnecté');
|
||||
}
|
||||
|
||||
/**
|
||||
* Émule connection.invoke() de SignalR
|
||||
* Redirige vers les endpoints REST appropriés
|
||||
*/
|
||||
async invoke(methodName, ...args) {
|
||||
if (!this.connected) {
|
||||
throw new Error('Non connecté au serveur');
|
||||
}
|
||||
|
||||
console.log(`[RestSocketAdapter] invoke: ${methodName}`, args);
|
||||
|
||||
switch (methodName) {
|
||||
case 'GetTerminalListByServiceProvider':
|
||||
return this._getTerminalList(args[0]);
|
||||
|
||||
case 'AgentLogin':
|
||||
return this._agentLogin(args[0], args[1], args[2]);
|
||||
|
||||
case 'AgentLogoff':
|
||||
return this._agentLogoff(args[0]);
|
||||
|
||||
default:
|
||||
console.warn(`[RestSocketAdapter] Méthode non implémentée: ${methodName}`);
|
||||
throw new Error(`Méthode non supportée: ${methodName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/terminals/{serviceProvider}
|
||||
*/
|
||||
async _getTerminalList(serviceProvider) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/api/v1/terminals/${encodeURIComponent(serviceProvider)}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || 'Erreur récupération terminaux');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Retourner juste la liste des numéros de terminaux (format SignalR)
|
||||
return data.terminals.map(t => t.number || t);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/login
|
||||
*/
|
||||
async _agentLogin(email, password, terminal) {
|
||||
const response = await fetch(`${this.serverUrl}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
access_code: email,
|
||||
password: password,
|
||||
terminal: terminal,
|
||||
force_disconnect: false
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || 'Erreur de connexion');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Stocker le token si présent
|
||||
if (data.token) {
|
||||
this.authToken = data.token;
|
||||
}
|
||||
this.currentAgent = data;
|
||||
|
||||
// Enregistrer le terminal sur Socket.IO pour recevoir les événements
|
||||
if (this.socket && this.socket.connected) {
|
||||
this.socket.emit('register', {
|
||||
terminal: terminal,
|
||||
agentId: data.accessCode || data.access_code
|
||||
});
|
||||
}
|
||||
|
||||
// Retourner au format attendu par le client (compatible SignalR)
|
||||
return {
|
||||
accessCode: data.accessCode || data.access_code,
|
||||
firstName: data.firstName || data.first_name,
|
||||
lastName: data.lastName || data.last_name,
|
||||
connList: data.connList || data.conn_list || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/logout
|
||||
*/
|
||||
async _agentLogoff(accessCode) {
|
||||
// Désenregistrer du Socket.IO
|
||||
if (this.socket && this.socket.connected) {
|
||||
this.socket.emit('unregister', { terminal: this.currentAgent?.terminal });
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Ajouter le token si disponible
|
||||
if (this.authToken) {
|
||||
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.serverUrl}/api/v1/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ access_code: accessCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || 'Erreur de déconnexion');
|
||||
}
|
||||
|
||||
this.authToken = null;
|
||||
this.currentAgent = null;
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Émule connection.on() de SignalR
|
||||
*/
|
||||
on(eventName, handler) {
|
||||
console.log(`[RestSocketAdapter] Enregistrement handler: ${eventName}`);
|
||||
this.handlers.set(eventName, handler);
|
||||
|
||||
// Si Socket.IO est déjà connecté, ajouter le handler
|
||||
if (this.socket) {
|
||||
this.socket.on(eventName, (...args) => {
|
||||
console.log(`[RestSocketAdapter] Événement reçu: ${eventName}`, args);
|
||||
handler(eventName, args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Émule connection.off() de SignalR
|
||||
*/
|
||||
off(eventName) {
|
||||
this.handlers.delete(eventName);
|
||||
if (this.socket) {
|
||||
this.socket.off(eventName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* État de la connexion (émule SignalR HubConnectionState)
|
||||
*/
|
||||
get state() {
|
||||
return this.connected ? 'Connected' : 'Disconnected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifie ce type d'adaptateur
|
||||
*/
|
||||
get isRestSocketAdapter() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RestSocketAdapter;
|
||||
Reference in New Issue
Block a user