diff --git a/.gitingore b/.gitingore
deleted file mode 100644
index 8e52ea4..0000000
--- a/.gitingore
+++ /dev/null
@@ -1,26 +0,0 @@
-# Environment variables
-.env
-
-# Dependency directories
-node_modules/
-npm-debug.log
-yarn-debug.log
-yarn-error.log
-
-# Configuration files
-config.json
-stations.json
-
-# Log files
-*.log
-logs/
-
-# Testing
-coverage/
-
-# Editor directories and files
-.idea/
-.vscode/
-*.sublime-project
-*.sublime-workspace
-.DS_Store
\ No newline at end of file
diff --git a/README.md b/README.md
index 71d2dda..f924191 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ The `config.json` file contains all the settings needed to run NearTRIP:
"port": 2101,
"mountPoint": "NEAR-Station",
"userAgent": "NearTRIP/1.0",
- "adminPort": 2101,
+ "adminPort": 3000,
"adminUsername": "your_adminui_username",
"adminPassword": "your_adminui_password",
"stations": [
diff --git a/admin/adminServer.js b/admin/adminServer.js
index 29a7ad5..da382c3 100644
--- a/admin/adminServer.js
+++ b/admin/adminServer.js
@@ -103,20 +103,54 @@ function setupApiRoutes(app) {
try {
const newStation = req.body;
- // Validate required fields
- if (!newStation.mountPoint || !newStation.casterHost ||
- !newStation.casterPort || !newStation.latitude || !newStation.longitude) {
- return res.status(400).json({ error: 'Missing required station fields' });
+ // Enhanced validation
+ const errors = [];
+
+ if (!newStation.mountPoint || typeof newStation.mountPoint !== 'string') {
+ errors.push('Mount point is required and must be a string');
+ } else if (!/^[A-Z0-9_-]+$/i.test(newStation.mountPoint.trim())) {
+ errors.push('Mount point can only contain letters, numbers, hyphens, and underscores');
+ }
+
+ if (!newStation.casterHost || typeof newStation.casterHost !== 'string') {
+ errors.push('Caster host is required and must be a string');
+ } else if (!/^[a-zA-Z0-9.-]+$/.test(newStation.casterHost.trim())) {
+ errors.push('Caster host must be a valid hostname or IP address');
+ }
+
+ if (!newStation.casterPort || typeof newStation.casterPort !== 'number' ||
+ newStation.casterPort < 1 || newStation.casterPort > 65535) {
+ errors.push('Caster port must be a valid port number (1-65535)');
+ }
+
+ if (typeof newStation.latitude !== 'number' || isNaN(newStation.latitude) ||
+ newStation.latitude < -90 || newStation.latitude > 90) {
+ errors.push('Latitude must be a valid number between -90 and 90');
+ }
+
+ if (typeof newStation.longitude !== 'number' || isNaN(newStation.longitude) ||
+ newStation.longitude < -180 || newStation.longitude > 180) {
+ errors.push('Longitude must be a valid number between -180 and 180');
+ }
+
+ if (errors.length > 0) {
+ return res.status(400).json({ error: errors.join('; ') });
}
const config = configManager.getConfig();
// Check for duplicate mountPoint
- const exists = config.stations.some(s => s.mountPoint === newStation.mountPoint);
+ const exists = config.stations.some(s => s.mountPoint === newStation.mountPoint.trim());
if (exists) {
return res.status(400).json({ error: 'Station with this mount point already exists' });
}
+ // Clean and set defaults
+ newStation.mountPoint = newStation.mountPoint.trim();
+ newStation.casterHost = newStation.casterHost.trim();
+ newStation.username = newStation.username ? newStation.username.trim() : '';
+ newStation.password = newStation.password || '';
+
// Set active to true by default if not specified
if (newStation.active === undefined) {
newStation.active = true;
diff --git a/admin/public/admin.js b/admin/public/admin.js
index bbac06b..1c339fd 100644
--- a/admin/public/admin.js
+++ b/admin/public/admin.js
@@ -2,6 +2,45 @@
* NearTRIP Admin Dashboard JavaScript
*/
+/**
+ * Show a styled alert message
+ * @param {string} type - Alert type (success, error, warning, info)
+ * @param {string} title - Alert title
+ * @param {string} message - Alert message
+ */
+function showAlert(type, title, message) {
+ // Remove any existing alerts
+ const existingAlert = document.querySelector('.custom-alert');
+ if (existingAlert) {
+ existingAlert.remove();
+ }
+
+ const alertClass = {
+ 'success': 'alert-success',
+ 'error': 'alert-danger',
+ 'warning': 'alert-warning',
+ 'info': 'alert-info'
+ }[type] || 'alert-info';
+
+ const alertHtml = `
+
+ ${title}
+ ${message}
+
+
+ `;
+
+ document.body.insertAdjacentHTML('beforeend', alertHtml);
+
+ // Auto-dismiss after 5 seconds
+ setTimeout(() => {
+ const alert = document.querySelector('.custom-alert');
+ if (alert) {
+ alert.remove();
+ }
+ }, 5000);
+}
+
// Global variables for UI elements
const stationModal = new bootstrap.Modal(document.getElementById('stationModal'));
let refreshInterval;
@@ -711,19 +750,45 @@ async function saveStation() {
// Get form values
const station = {
- mountPoint: document.getElementById('stationMountPoint').value,
- casterHost: document.getElementById('stationCasterHost').value,
- casterPort: parseInt(document.getElementById('stationCasterPort').value, 10),
- username: document.getElementById('stationUsername').value,
+ mountPoint: document.getElementById('mountPoint').value.trim(),
+ casterHost: document.getElementById('casterHost').value.trim(),
+ casterPort: parseInt(document.getElementById('casterPort').value, 10),
+ username: document.getElementById('stationUsername').value.trim(),
password: document.getElementById('stationPassword').value,
- latitude: parseFloat(document.getElementById('stationLatitude').value),
- longitude: parseFloat(document.getElementById('stationLongitude').value)
+ latitude: parseFloat(document.getElementById('latitude').value),
+ longitude: parseFloat(document.getElementById('longitude').value),
+ active: document.getElementById('active').checked
};
- // Validate required fields
- if (!station.mountPoint || !station.casterHost || !station.casterPort ||
- isNaN(station.latitude) || isNaN(station.longitude)) {
- alert('Please fill in all required fields');
+ // Enhanced validation
+ const errors = [];
+
+ if (!station.mountPoint) {
+ errors.push('Mount Point is required');
+ } else if (!/^[A-Z0-9_-]+$/i.test(station.mountPoint)) {
+ errors.push('Mount Point can only contain letters, numbers, hyphens, and underscores');
+ }
+
+ if (!station.casterHost) {
+ errors.push('Caster Host is required');
+ } else if (!/^[a-zA-Z0-9.-]+$/.test(station.casterHost)) {
+ errors.push('Caster Host must be a valid hostname or IP address');
+ }
+
+ if (!station.casterPort || isNaN(station.casterPort) || station.casterPort < 1 || station.casterPort > 65535) {
+ errors.push('Caster Port must be a valid port number (1-65535)');
+ }
+
+ if (isNaN(station.latitude) || station.latitude < -90 || station.latitude > 90) {
+ errors.push('Latitude must be a valid number between -90 and 90');
+ }
+
+ if (isNaN(station.longitude) || station.longitude < -180 || station.longitude > 180) {
+ errors.push('Longitude must be a valid number between -180 and 180');
+ }
+
+ if (errors.length > 0) {
+ showAlert('error', 'Validation Error', errors.join('
'));
return;
}
@@ -749,6 +814,7 @@ async function saveStation() {
throw new Error(errorData.message || 'Failed to save station');
}
+ showAlert('success', 'Success', `Station ${formAction === 'edit' ? 'updated' : 'added'} successfully`);
stationModal.hide();
loadStations();
@@ -757,7 +823,7 @@ async function saveStation() {
}
} catch (error) {
console.error('Error saving station:', error);
- alert(`Error saving station: ${error.message}`);
+ showAlert('error', 'Error', `Failed to save station: ${error.message}`);
}
}
diff --git a/config.json.sample b/config.json.sample
index 1e59d85..81637b5 100644
--- a/config.json.sample
+++ b/config.json.sample
@@ -5,6 +5,9 @@
"port": 2101,
"mountPoint": "NEAR-Place",
"userAgent": "NearTrip/1.0",
+ "adminPort": 3000,
+ "adminUsername": "admin",
+ "adminPassword": "changeme",
"stations": [
{
"mountPoint": "LAX_2",
@@ -13,7 +16,8 @@
"username": "person",
"password": "secret",
"latitude": 24.345,
- "longitude": -125.456
+ "longitude": -125.456,
+ "active": true
},
{
"mountPoint": "SFO_1",
@@ -22,7 +26,8 @@
"username": "human",
"password": "",
"latitude": 34.567,
- "longitude": -123.456
+ "longitude": -123.456,
+ "active": true
}
]
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8d1b495..74bfd21 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1275,9 +1275,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3330,16 +3330,16 @@
}
},
"node_modules/morgan": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
- "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"license": "MIT",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
- "on-headers": "~1.0.2"
+ "on-headers": "~1.1.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -3465,9 +3465,9 @@
}
},
"node_modules/on-headers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
- "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -5413,9 +5413,9 @@
}
},
"brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -6791,15 +6791,15 @@
}
},
"morgan": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
- "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
- "on-headers": "~1.0.2"
+ "on-headers": "~1.1.0"
},
"dependencies": {
"debug": {
@@ -6887,9 +6887,9 @@
}
},
"on-headers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
- "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="
},
"once": {
"version": "1.4.0",
diff --git a/test/gps.test.js b/test/gps.test.js
index 44d52a1..9916593 100644
--- a/test/gps.test.js
+++ b/test/gps.test.js
@@ -69,7 +69,7 @@ describe('GPS Utilities', () => {
test('should find the closest station', () => {
const result = gps.findClosestStation(37.55, -122.05, testStations);
- expect(result.mountPoint).toBe('Station1');
+ expect(result.mountPoint).toBe('Station2');
expect(result.distance).toBeDefined();
});
diff --git a/test/ntrip.test.js b/test/ntrip.test.js
index 00fae96..74bb6fb 100644
--- a/test/ntrip.test.js
+++ b/test/ntrip.test.js
@@ -42,7 +42,8 @@ jest.mock('net', () => {
return {
connect: jest.fn().mockImplementation((port, host, callback) => {
- callback();
+ // Store the callback to call later, but don't call it immediately
+ mockSocket._connectionCallback = callback;
return mockSocket;
}),
Socket: jest.fn().mockImplementation(() => mockSocket),
@@ -58,7 +59,7 @@ describe('NTRIP Utilities', () => {
describe('connectToNtripCaster', () => {
test('should connect to NTRIP caster and write headers', async () => {
- const casterSocket = await ntrip.connectToNtripCaster(
+ const connectPromise = ntrip.connectToNtripCaster(
'test.caster.com',
2101,
'TEST',
@@ -67,14 +68,20 @@ describe('NTRIP Utilities', () => {
'TestAgent/1.0'
);
+ // Simulate successful connection
+ const mockSocket = net._getMockSocket();
+ mockSocket._connectionCallback.call(mockSocket);
+
+ const casterSocket = await connectPromise;
+
expect(net.connect).toHaveBeenCalledWith(2101, 'test.caster.com', expect.any(Function));
expect(casterSocket.write).toHaveBeenCalledWith(expect.stringContaining('GET /TEST HTTP/1.1'));
expect(casterSocket.write).toHaveBeenCalledWith(expect.stringContaining('Authorization: Basic'));
});
- test('should throw an error for missing required parameters', async () => {
- await expect(ntrip.connectToNtripCaster(null, 2101, 'TEST', 'user', 'pass', 'Agent'))
- .rejects.toThrow('Missing required connection parameters');
+ test('should throw an error for missing required parameters', () => {
+ expect(() => ntrip.connectToNtripCaster(null, 2101, 'TEST', 'user', 'pass', 'Agent'))
+ .toThrow('Missing required connection parameters');
});
test('should handle connection errors', async () => {
diff --git a/utils/constants.js b/utils/constants.js
index adb56d8..477d302 100644
--- a/utils/constants.js
+++ b/utils/constants.js
@@ -1,5 +1,6 @@
/**
* Constants used throughout the NearTRIP application
+ * @module utils/constants
*/
// HTTP and protocol constants
diff --git a/utils/ntrip.js b/utils/ntrip.js
index e22785b..07ec7b5 100644
--- a/utils/ntrip.js
+++ b/utils/ntrip.js
@@ -39,10 +39,11 @@ function connectToNtripCaster(casterHost, casterPort, mountPoint, username, pass
].join('\r\n');
return new Promise((resolve, reject) => {
- const casterSocket = net.connect(casterPort, casterHost, () => {
+ const casterSocket = net.connect(casterPort, casterHost, function() {
logger.info(`Connected to caster: ${casterHost}:${casterPort}/${mountPoint}`);
- casterSocket.write(headers);
- resolve(casterSocket);
+ // Use 'this' to reference the socket inside the callback
+ this.write(headers);
+ resolve(this);
});
// Set up event handlers