navflow-proxy-server
Version:
Dynamic WebSocket proxy server for NavFlow
287 lines (250 loc) • 9.29 kB
JavaScript
/**
* 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;