UNPKG

@supernick135/face-scanner-client

Version:

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

307 lines (259 loc) 8.93 kB
#!/usr/bin/env node /** * Face Scanner Service Client * * Production-ready service client for WebSocket server with secure authentication, * auto-reconnection, and face scan event processing capabilities. * * Key Features: * - Secure runtime token management (no file storage) * - Multi-step authentication flow (Admin Login → JWT → API Key) * - Auto-reconnection with exponential backoff * - Ping/pong health monitoring * - Face scan event processing * - Room management * * @example * const ServiceClient = require('@supernick135/face-scanner-client/lib/client/ServiceClient'); * * const client = new ServiceClient({ * WS_SERVER_URL: 'ws://localhost:8008/ws', * API_BASE_URL: 'http://localhost:8008', * ADMIN_USERNAME: 'admin', * ADMIN_PASSWORD: 'password', * SERVICE_NAME: 'OrderingService', * SERVICE_SCHOOL_ID: 'SCHOOL_001', * DEBUG_MODE: true * }); * * await client.start(); */ const { EventEmitter } = require('events'); class ServiceClient extends EventEmitter { constructor(config) { super(); // Validate required config based on client type const clientType = config.SERVICE_CLIENT_TYPE || 'service'; const baseRequired = ['WS_SERVER_URL', 'API_BASE_URL', 'ADMIN_USERNAME', 'ADMIN_PASSWORD', 'SERVICE_NAME']; // Service type doesn't need school ID (can access all schools) // Other types need school ID const required = clientType === 'service' ? baseRequired : [...baseRequired, 'SERVICE_SCHOOL_ID']; for (const field of required) { if (!config[field]) { throw new Error(`Missing required config: ${field}`); } } this.config = { ...config, SERVICE_CLIENT_TYPE: clientType, SERVICE_SCHOOL_ID: config.SERVICE_SCHOOL_ID || 'GLOBAL', // Default for service type DEBUG_MODE: config.DEBUG_MODE !== false, AUTO_RECONNECT: config.AUTO_RECONNECT !== false, ENABLE_PING_PONG: config.ENABLE_PING_PONG !== false, PING_INTERVAL: config.PING_INTERVAL || 30000, MAX_RECONNECT_ATTEMPTS: config.MAX_RECONNECT_ATTEMPTS || 5, RECONNECT_DELAY: config.RECONNECT_DELAY || 5000, API_KEY_REFRESH_THRESHOLD: config.API_KEY_REFRESH_THRESHOLD || 86400000 }; this.authManager = null; this.wsClient = null; this.isRunning = false; } /** * Start the service client */ async start() { if (this.isRunning) { console.log('⚠️ Service client is already running'); return; } try { console.log('🚀 Starting Face Scanner Service Client...'); // Load dependencies const AuthManager = require('./auth-manager'); const WebSocketClient = require('./websocket-client'); // Initialize managers this.authManager = new AuthManager(this.config); this.wsClient = new WebSocketClient(this.config, this.authManager); // Setup event handlers this.setupEventHandlers(); // Authenticate console.log('🔐 Authenticating...'); await this.authManager.authenticate(); // Connect to WebSocket console.log('🔌 Connecting to WebSocket server...'); await this.wsClient.connect(); this.isRunning = true; console.log('✅ Service client started successfully'); // Wait for authentication to complete before joining rooms this.wsClient.once('auth_success', () => { console.log('🔐 Authentication confirmed, joining default rooms...'); this.joinDefaultRooms(); }); // Setup graceful shutdown this.setupShutdown(); this.emit('started'); } catch (error) { console.error('❌ Failed to start service client:', error.message); this.emit('error', error); throw error; } } /** * Stop the service client */ async stop() { if (!this.isRunning) { console.log('⚠️ Service client is not running'); return; } console.log('🛑 Stopping service client...'); if (this.wsClient) { this.wsClient.disconnect(); } if (this.authManager) { this.authManager.destroy(); } this.isRunning = false; console.log('✅ Service client stopped'); this.emit('stopped'); } /** * Send face scan event */ sendFaceScan(faceData) { if (!this.wsClient) { throw new Error('Service client not started'); } return this.wsClient.sendFaceScan(faceData); } /** * Join a room */ joinRoom(roomName) { if (!this.wsClient) { throw new Error('Service client not started'); } return this.wsClient.joinRoom(roomName); } /** * Leave a room */ leaveRoom(roomName) { if (!this.wsClient) { throw new Error('Service client not started'); } return this.wsClient.leaveRoom(roomName); } /** * Setup event handlers */ setupEventHandlers() { // WebSocket events this.wsClient.on('connected', () => { console.log('🟢 WebSocket connected'); this.emit('connected'); }); this.wsClient.on('disconnected', () => { console.log('🔴 WebSocket disconnected'); this.emit('disconnected'); }); this.wsClient.on('auth_success', (data) => { console.log('🔐 Authentication successful'); this.emit('authenticated', data); }); this.wsClient.on('face_detected', (data) => { this.handleFaceDetected(data); }); this.wsClient.on('room_update', (data) => { console.log('🏠 Room update:', data); this.emit('room_update', data); }); this.wsClient.on('error', (error) => { console.error('❌ WebSocket error:', error.message); this.emit('error', error); }); } /** * Handle face detection events */ handleFaceDetected(data) { console.log('👤 Face detected:', { studentId: data.studentId, confidence: data.confidence, schoolId: data.schoolId }); // Emit event for external handlers this.emit('face_detected', data); // Process with ordering logic (override this method in subclasses) this.processOrderingLogic(data); } /** * Process ordering logic (override in subclasses) */ processOrderingLogic(faceData) { // Default implementation - override this in your service console.log('📦 Processing order for student:', faceData.studentId); } /** * Join default rooms */ joinDefaultRooms() { // Join global room (for services) this.joinRoom('global'); // For services, we don't need specific school rooms since they have GLOBAL access console.log('🚪 Joined default rooms'); } /** * Setup graceful shutdown */ setupShutdown() { const shutdown = async () => { console.log('\n📡 Received shutdown signal'); await this.stop(); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); } /** * Get connection status */ getStatus() { return { isRunning: this.isRunning, isConnected: this.wsClient?.isConnected || false, authStatus: this.authManager?.getStatus() || null }; } } // Export for use as library module.exports = ServiceClient; // Run as standalone if executed directly if (require.main === module) { // Load environment variables require('dotenv').config(); const config = { WS_SERVER_URL: process.env.WS_SERVER_URL || 'ws://localhost:8008/ws', API_BASE_URL: process.env.API_BASE_URL || 'http://localhost:8008', ADMIN_USERNAME: process.env.ADMIN_USERNAME, ADMIN_PASSWORD: process.env.ADMIN_PASSWORD, SERVICE_NAME: process.env.SERVICE_NAME || 'ServiceClient', SERVICE_SCHOOL_ID: process.env.SERVICE_SCHOOL_ID, SERVICE_CLIENT_TYPE: process.env.SERVICE_CLIENT_TYPE || 'service', DEBUG_MODE: process.env.DEBUG_MODE === 'true', AUTO_RECONNECT: process.env.AUTO_RECONNECT !== 'false', ENABLE_PING_PONG: process.env.ENABLE_PING_PONG !== 'false', PING_INTERVAL: parseInt(process.env.PING_INTERVAL) || 30000, MAX_RECONNECT_ATTEMPTS: parseInt(process.env.MAX_RECONNECT_ATTEMPTS) || 5, RECONNECT_DELAY: parseInt(process.env.RECONNECT_DELAY) || 5000, API_KEY_REFRESH_THRESHOLD: parseInt(process.env.API_KEY_REFRESH_THRESHOLD) || 86400000 }; const client = new ServiceClient(config); client.start().catch(error => { console.error('💥 Failed to start:', error); process.exit(1); }); }