@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
JavaScript
/**
* 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);
});
}