Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions .gitingore

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
44 changes: 39 additions & 5 deletions admin/adminServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
88 changes: 77 additions & 11 deletions admin/public/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div class="alert ${alertClass} alert-dismissible fade show custom-alert" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px;">
<strong>${title}</strong><br>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;

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;
Expand Down Expand Up @@ -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('<br>'));
return;
}

Expand All @@ -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();

Expand All @@ -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}`);
}
}

Expand Down
9 changes: 7 additions & 2 deletions config.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"port": 2101,
"mountPoint": "NEAR-Place",
"userAgent": "NearTrip/1.0",
"adminPort": 3000,
"adminUsername": "admin",
"adminPassword": "changeme",
"stations": [
{
"mountPoint": "LAX_2",
Expand All @@ -13,7 +16,8 @@
"username": "person",
"password": "secret",
"latitude": 24.345,
"longitude": -125.456
"longitude": -125.456,
"active": true
},
{
"mountPoint": "SFO_1",
Expand All @@ -22,7 +26,8 @@
"username": "human",
"password": "",
"latitude": 34.567,
"longitude": -123.456
"longitude": -123.456,
"active": true
}
]
}
40 changes: 20 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/gps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
17 changes: 12 additions & 5 deletions test/ntrip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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',
Expand All @@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions utils/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Constants used throughout the NearTRIP application
* @module utils/constants
*/

// HTTP and protocol constants
Expand Down
Loading