Files
SimpleClient-releases/rest-socket-adapter.js
Pierre Marx 2258013394 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.
2025-11-24 16:05:30 -05:00

286 lines
9.6 KiB
JavaScript

/**
* 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;