UNPKG

navflow-proxy-server

Version:

Dynamic WebSocket proxy server for NavFlow

287 lines (250 loc) 9.29 kB
/** * Device Management for NavFlow Proxy Server * Handles device registration and validation with Firestore */ const admin = require('firebase-admin'); const axios = require('axios'); class DeviceManager { constructor() { this.db = null; this.initialized = false; this.initializeFirebase(); } initializeFirebase() { try { // Initialize Firebase Admin SDK using Application Default Credentials // The Cloud Run service uses runtime@buildship-pfw15o.iam.gserviceaccount.com if (!admin.apps.length) { admin.initializeApp({ projectId: 'buildship-pfw15o' }); } this.db = admin.firestore(); this.initialized = true; console.log('✅ Firebase Admin initialized for device management'); // Test Firestore connection this.testFirestoreConnection(); } catch (error) { console.error('❌ Failed to initialize Firebase Admin:', error); this.initialized = false; } } /** * Test Firestore connection and permissions */ async testFirestoreConnection() { try { // Try to read from a test collection to verify permissions const testDoc = await this.db.collection('_test').doc('connection').get(); console.log('✅ Firestore connection test successful'); } catch (error) { console.warn('⚠️ Firestore connection test failed:', error.message); if (error.code === 'permission-denied') { console.error('❌ Service account lacks Firestore permissions'); } } } /** * Validate API key by connecting to browser-server * @param {string} apiKey - The API key to validate * @param {string} ipAddress - IP address of the browser-server (default: localhost) * @param {number} port - Port of the browser-server (default: 3002) * @returns {Promise<Object>} Device information from browser-server */ async validateApiKey(apiKey, ipAddress = 'localhost', port = 3002) { try { const baseUrl = `http://${ipAddress}:${port}`; // First check if server is reachable const healthResponse = await axios.get(`${baseUrl}/health`, { timeout: 5000, headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!healthResponse.data || healthResponse.data.status !== 'healthy') { throw new Error('Browser server is not healthy'); } // Get device information const deviceResponse = await axios.get(`${baseUrl}/device`, { timeout: 5000, headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!deviceResponse.data) { throw new Error('Failed to retrieve device information'); } return { valid: true, deviceInfo: { name: deviceResponse.data.name, macAddress: deviceResponse.data.macAddress, port: deviceResponse.data.port, createdAt: deviceResponse.data.createdAt, ipAddress: ipAddress, apiKey: apiKey }, serverStatus: healthResponse.data }; } catch (error) { console.error('Device validation failed:', error.message); if (error.code === 'ECONNREFUSED') { throw new Error('Cannot connect to browser server. Make sure it is running on the specified address and port.'); } else if (error.response?.status === 401 || error.response?.status === 403) { throw new Error('Invalid API key. Please check your API key and try again.'); } else if (error.code === 'ENOTFOUND') { throw new Error('Cannot resolve the browser server address. Please check the IP address.'); } else { throw new Error(`Device validation failed: ${error.message}`); } } } /** * Register device in Firestore * @param {string} apiKey - The API key to validate and register * @param {string} ipAddress - IP address of the browser-server * @param {number} port - Port of the browser-server * @returns {Promise<Object>} Created or updated device document */ async registerDevice(apiKey, ipAddress = 'localhost', port = 3002, deviceInfo = null) { if (!this.initialized) { throw new Error('Firebase not initialized'); } try { let deviceDetails; // If device info is provided (from browser-server), use it directly if (deviceInfo) { deviceDetails = { name: deviceInfo.name, macAddress: deviceInfo.macAddress, apiKey: apiKey, ipAddress: ipAddress, port: port }; console.log('📋 Using provided device info for registration'); } else { // Otherwise, try to validate with the device (for manual registrations) console.log('🔍 Validating API key with device...'); const validation = await this.validateApiKey(apiKey, ipAddress, port); if (!validation.valid) { throw new Error('API key validation failed'); } deviceDetails = validation.deviceInfo; } // Check if device already exists by MAC address const existingDeviceQuery = await this.db .collection('devices') .where('macAddress', '==', deviceDetails.macAddress) .limit(1) .get(); if (!existingDeviceQuery.empty) { // Device already exists, update it const existingDevice = existingDeviceQuery.docs[0]; const updateData = { name: deviceDetails.name, apiKey: deviceDetails.apiKey, ipAddress: deviceDetails.ipAddress, port: deviceDetails.port, status: 'online', lastSeen: admin.firestore.FieldValue.serverTimestamp(), updatedAt: admin.firestore.FieldValue.serverTimestamp() }; await existingDevice.ref.update(updateData); return { id: existingDevice.id, ...existingDevice.data(), ...updateData }; } // Create new device document const deviceData = { name: deviceDetails.name, macAddress: deviceDetails.macAddress, apiKey: deviceDetails.apiKey, ipAddress: deviceDetails.ipAddress, port: deviceDetails.port, status: 'online', lastSeen: admin.firestore.FieldValue.serverTimestamp(), browserSettings: { browserType: 'chromium', headless: false, stealth: true, viewport: { width: 1920, height: 1080 }, timeout: 30000 }, createdAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: admin.firestore.FieldValue.serverTimestamp() }; const docRef = await this.db.collection('devices').add(deviceData); console.log(`✅ Device registered in Firestore: ${deviceDetails.name} (${deviceDetails.macAddress})`); console.log(`📄 Firestore document ID: ${docRef.id}`); return { id: docRef.id, ...deviceData }; } catch (error) { console.error('❌ Device registration failed:', error); // Enhanced error handling for Firestore operations if (error.code === 'permission-denied') { console.error('❌ Firestore permission denied - check service account permissions'); throw new Error('Database permission denied. Please check server configuration.'); } else if (error.code === 'unavailable') { console.error('❌ Firestore service unavailable'); throw new Error('Database service unavailable. Please try again later.'); } else if (error.code === 'unauthenticated') { console.error('❌ Firestore authentication failed'); throw new Error('Database authentication failed. Please check server configuration.'); } throw error; } } /** * Update device status * @param {string} deviceId - The device document ID * @param {string} status - New status ('online' | 'offline') */ async updateDeviceStatus(deviceId, status) { if (!this.initialized) { throw new Error('Firebase not initialized'); } try { await this.db.collection('devices').doc(deviceId).update({ status: status, lastSeen: admin.firestore.FieldValue.serverTimestamp(), updatedAt: admin.firestore.FieldValue.serverTimestamp() }); } catch (error) { console.error(`Failed to update device status for ${deviceId}:`, error); throw error; } } /** * Get device by API key * @param {string} apiKey - The API key to search for * @returns {Promise<Object|null>} Device document or null if not found */ async getDeviceByApiKey(apiKey) { if (!this.initialized) { throw new Error('Firebase not initialized'); } try { const deviceQuery = await this.db .collection('devices') .where('apiKey', '==', apiKey) .limit(1) .get(); if (deviceQuery.empty) { return null; } const deviceDoc = deviceQuery.docs[0]; return { id: deviceDoc.id, ...deviceDoc.data() }; } catch (error) { console.error('Failed to get device by API key:', error); throw error; } } } module.exports = DeviceManager;