UNPKG

@supernick135/face-scanner-client

Version:

Node.js client library for ZKTeco face scanning devices integration with comprehensive API support

489 lines (412 loc) 13.6 kB
/** * WebSocket Client for Face Scanner API * Real-time connection to Face Scanner WebSocket server */ const WebSocket = require('ws'); const EventEmitter = require('events'); class WebSocketClient extends EventEmitter { constructor(options = {}) { super(); this.config = { url: options.wsUrl || options.serverUrl || 'ws://localhost:3002', reconnect: options.reconnect !== false, reconnectDelay: options.reconnectDelay || 5000, maxReconnectAttempts: options.maxReconnectAttempts || 10, heartbeatInterval: options.heartbeatInterval || 30000, // Authentication configuration authMethod: options.authMethod || 'auto', // 'jwt', 'apikey', 'auto' token: options.token || options.jwt, // JWT token (legacy) apiKey: options.apiKey, // API key (new) clientType: options.clientType, // student, device, service, monitor schoolId: options.schoolId, deviceId: options.deviceId, studentId: options.studentId, ...options }; this.ws = null; this.isConnected = false; this.reconnectAttempts = 0; this.heartbeatTimer = null; this.subscriptions = new Set(); } async connect() { return new Promise((resolve, reject) => { try { // Build WebSocket URL with authentication const wsUrl = this.buildWebSocketUrl(); this.ws = new WebSocket(wsUrl); this.ws.on('open', async () => { this.isConnected = true; this.reconnectAttempts = 0; // Try to authenticate if credentials are provided if (this.config.apiKey || this.config.token) { try { await this.authenticateAfterConnection(); this.emit('authenticated'); } catch (authError) { this.emit('authError', authError); reject(authError); return; } } // Only start heartbeat if interval is set and > 0 if (this.config.heartbeatInterval && this.config.heartbeatInterval > 0) { this.startHeartbeat(); } this.emit('connected'); resolve(); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleMessage(message); } catch (error) { this.emit('error', error); } }); // Handle native WebSocket pong this.ws.on('pong', () => { this.emit('pong'); }); this.ws.on('close', (code, reason) => { this.isConnected = false; this.stopHeartbeat(); this.emit('disconnected', { code, reason: reason.toString() }); if (this.config.reconnect && !this.isShuttingDown) { this.handleReconnect(); } }); this.ws.on('error', (error) => { this.emit('error', error); if (!this.isConnected) { reject(error); } }); // Connection timeout setTimeout(() => { if (!this.isConnected) { reject(new Error('WebSocket connection timeout')); } }, 10000); } catch (error) { reject(error); } }); } disconnect() { this.isShuttingDown = true; this.stopHeartbeat(); if (this.ws) { this.ws.close(1000, 'Client disconnect'); } } handleMessage(message) { this.emit('message', message); // Handle specific message types switch (message.type) { case 'auth_success': this.emit('auth_success', message.payload); break; case 'auth_error': this.emit('auth_error', message.payload); break; case 'face_detected': this.emit('faceDetected', message.payload || message.data); break; case 'scan_complete': this.emit('scanComplete', message.payload || message.data); break; case 'device_status': this.emit('deviceStatus', message.payload || message.data); break; case 'recovery_progress': this.emit('recoveryProgress', message.payload || message.data); break; case 'queue_stats': this.emit('queueStats', message.payload || message.data); break; case 'system_alert': this.emit('systemAlert', message.payload || message.data); break; case 'room_update': this.emit('roomUpdate', message.payload); break; case 'client_connected': this.emit('clientConnected', message.payload); break; case 'client_disconnected': this.emit('clientDisconnected', message.payload); break; case 'error': this.emit('serverError', message.payload); break; case 'pong': this.emit('pong', message.payload || message.data); break; default: this.emit('unknownMessage', message); } } send(message) { if (!this.isConnected || !this.ws) { throw new Error('WebSocket not connected'); } const data = typeof message === 'string' ? message : JSON.stringify(message); this.ws.send(data); } // Subscribe to specific events subscribe(eventType, deviceId = null) { const subscription = { type: eventType, deviceId }; this.subscriptions.add(JSON.stringify(subscription)); this.send({ type: 'subscribe', payload: { eventType, deviceId }, timestamp: Date.now() }); } // Unsubscribe from events unsubscribe(eventType, deviceId = null) { const subscription = { type: eventType, deviceId }; this.subscriptions.delete(JSON.stringify(subscription)); this.send({ type: 'unsubscribe', payload: { eventType, deviceId }, timestamp: Date.now() }); } // Send WebSocket native ping ping() { if (this.ws && this.isConnected) { this.ws.ping(); } } // Send application-level ping message sendPingMessage() { this.send({ type: 'ping', payload: { status: 'active' }, timestamp: Date.now() }); } // Start heartbeat startHeartbeat() { this.heartbeatTimer = setInterval(() => { if (this.isConnected) { // Use WebSocket native ping for connection health check this.ping(); } }, this.config.heartbeatInterval); } // Stop heartbeat stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } // Build WebSocket URL with authentication parameters buildWebSocketUrl() { let baseUrl = this.config.url; // Ensure URL starts with ws:// or wss:// if (!baseUrl.startsWith('ws://') && !baseUrl.startsWith('wss://')) { baseUrl = 'ws://' + baseUrl; } const url = new URL(baseUrl); // Add authentication based on method if (this.config.authMethod === 'apikey' || (this.config.authMethod === 'auto' && this.config.apiKey)) { if (this.config.apiKey) { url.searchParams.set('apikey', this.config.apiKey); } // Add client identification parameters if (this.config.clientType) { url.searchParams.set('clientType', this.config.clientType); } if (this.config.schoolId) { url.searchParams.set('schoolId', this.config.schoolId); } if (this.config.deviceId) { url.searchParams.set('deviceId', this.config.deviceId); } if (this.config.studentId) { url.searchParams.set('studentId', this.config.studentId); } } else if (this.config.authMethod === 'jwt' || (this.config.authMethod === 'auto' && this.config.token)) { if (this.config.token) { url.searchParams.set('token', this.config.token); } } return url.toString(); } // Authenticate after connection using message-based authentication async authenticateAfterConnection() { return new Promise((resolve, reject) => { // Prepare authentication payload const authPayload = { clientType: this.config.clientType }; // Add authentication credentials if (this.config.apiKey) { authPayload.apiKey = this.config.apiKey; } else if (this.config.token) { authPayload.token = this.config.token; } // Add client identification if (this.config.schoolId) authPayload.schoolId = this.config.schoolId; if (this.config.deviceId) authPayload.deviceId = this.config.deviceId; if (this.config.studentId) authPayload.studentId = this.config.studentId; // Set up one-time listeners for auth response const authSuccessHandler = (data) => { this.removeListener('auth_error', authErrorHandler); this.removeListener('error', errorHandler); resolve(data); }; const authErrorHandler = (data) => { this.removeListener('auth_success', authSuccessHandler); this.removeListener('error', errorHandler); reject(new Error(data.message || 'Authentication failed')); }; const errorHandler = (error) => { this.removeListener('auth_success', authSuccessHandler); this.removeListener('auth_error', authErrorHandler); reject(error); }; this.once('auth_success', authSuccessHandler); this.once('auth_error', authErrorHandler); this.once('error', errorHandler); // Send authentication message this.send({ type: 'auth', payload: authPayload, timestamp: Date.now() }); }); } // Handle reconnection async handleReconnect() { if (this.reconnectAttempts >= this.config.maxReconnectAttempts) { this.emit('reconnectFailed'); return; } this.reconnectAttempts++; this.emit('reconnecting', this.reconnectAttempts); setTimeout(async () => { try { await this.connect(); // Re-subscribe to previous subscriptions for (const subscriptionStr of this.subscriptions) { const subscription = JSON.parse(subscriptionStr); this.subscribe(subscription.type, subscription.deviceId); } } catch (error) { this.emit('error', error); } }, this.config.reconnectDelay * this.reconnectAttempts); } // Get connection status getConnectionStatus() { return { connected: this.isConnected, reconnectAttempts: this.reconnectAttempts, subscriptions: Array.from(this.subscriptions).map(s => JSON.parse(s)), authMethod: this.config.authMethod, clientType: this.config.clientType, schoolId: this.config.schoolId, deviceId: this.config.deviceId }; } // Convenience methods for different client types // Join room (for non-student clients) joinRoom(roomName) { this.send({ type: 'join_room', payload: { room: roomName }, timestamp: Date.now() }); } // Leave room leaveRoom(roomName) { this.send({ type: 'leave_room', payload: { room: roomName }, timestamp: Date.now() }); } // Send face scan event (for device clients) sendFaceScanEvent(eventType, eventData) { const payload = { schoolId: this.config.schoolId, deviceId: this.config.deviceId, timestamp: Date.now(), ...eventData }; this.send({ type: eventType, payload, timestamp: Date.now() }); } // Request server stats (admin only) requestStats() { this.send({ type: 'get_stats', payload: {}, timestamp: Date.now() }); } // Kick client (admin only) kickClient(targetClientId, reason) { this.send({ type: 'kick_client', payload: { clientId: targetClientId, reason: reason }, timestamp: Date.now() }); } // Helper method to create client instances for different types static createDeviceClient(options) { return new WebSocketClient({ clientType: 'device', authMethod: 'apikey', ...options }); } static createStudentClient(options) { return new WebSocketClient({ clientType: 'student', authMethod: 'apikey', ...options }); } static createMonitorClient(options) { return new WebSocketClient({ clientType: 'monitor', authMethod: 'apikey', ...options }); } static createServiceClient(options) { return new WebSocketClient({ clientType: 'service', authMethod: 'apikey', ...options }); } static createAdminClient(options) { return new WebSocketClient({ clientType: 'admin', authMethod: 'jwt', ...options }); } } module.exports = WebSocketClient;