First commit
This commit is contained in:
116
server/config.js
Normal file
116
server/config.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Server Configuration
|
||||
* Central configuration for all backend constants and settings
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const config = {
|
||||
// Server Settings
|
||||
server: {
|
||||
port: process.env.PORT || 3000,
|
||||
host: process.env.HOST || '0.0.0.0', // Bind to all interfaces
|
||||
env: process.env.NODE_ENV || 'development'
|
||||
},
|
||||
|
||||
// Database Settings
|
||||
database: {
|
||||
path: path.join(__dirname, '../database/datacenter.db'),
|
||||
// Enable WAL mode for better concurrency
|
||||
walMode: true,
|
||||
// Enable foreign keys
|
||||
foreignKeys: true,
|
||||
// Busy timeout in ms
|
||||
busyTimeout: 5000
|
||||
},
|
||||
|
||||
// Rack Configuration
|
||||
rack: {
|
||||
// Default dimensions in pixels
|
||||
defaultWidth: 520,
|
||||
defaultHeight: 1485,
|
||||
// Number of U slots
|
||||
slots: 42,
|
||||
// Grid spacing
|
||||
gridHorizontal: 600,
|
||||
gridVertical: 1585
|
||||
},
|
||||
|
||||
// Device Configuration
|
||||
device: {
|
||||
// Default device dimensions
|
||||
defaultHeight: 32,
|
||||
defaultSpacing: 2,
|
||||
// Margins within rack
|
||||
margin: {
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
left: 10
|
||||
},
|
||||
// Physical view width
|
||||
physicalWidth: 500,
|
||||
// Logical view width
|
||||
logicalWidth: 120
|
||||
},
|
||||
|
||||
// Validation Rules
|
||||
validation: {
|
||||
project: {
|
||||
nameMinLength: 1,
|
||||
nameMaxLength: 100,
|
||||
descriptionMaxLength: 500
|
||||
},
|
||||
rack: {
|
||||
nameMinLength: 1,
|
||||
nameMaxLength: 50,
|
||||
maxPerProject: 1000
|
||||
},
|
||||
device: {
|
||||
nameMinLength: 1,
|
||||
nameMaxLength: 50,
|
||||
minRackUnits: 1,
|
||||
maxRackUnits: 42
|
||||
},
|
||||
connection: {
|
||||
maxWaypoints: 20
|
||||
}
|
||||
},
|
||||
|
||||
// API Settings
|
||||
api: {
|
||||
// Request size limits
|
||||
jsonLimit: '10mb',
|
||||
// Enable CORS (set to true if frontend is on different domain)
|
||||
cors: false,
|
||||
// Rate limiting (requests per minute per IP)
|
||||
rateLimit: {
|
||||
enabled: false,
|
||||
windowMs: 60000, // 1 minute
|
||||
max: 100 // 100 requests per minute
|
||||
}
|
||||
},
|
||||
|
||||
// Logging
|
||||
logging: {
|
||||
enabled: true,
|
||||
level: process.env.LOG_LEVEL || 'info', // debug, info, warn, error
|
||||
format: 'combined' // Morgan format
|
||||
},
|
||||
|
||||
// Default Device Types (seed data)
|
||||
deviceTypes: [
|
||||
{ name: 'Switch 24-Port', portsCount: 24, color: '#4A90E2', rackUnits: 1 },
|
||||
{ name: 'Switch 48-Port', portsCount: 48, color: '#5CA6E8', rackUnits: 1 },
|
||||
{ name: 'Router', portsCount: 8, color: '#E27D60', rackUnits: 1 },
|
||||
{ name: 'Firewall', portsCount: 6, color: '#E8A87C', rackUnits: 1 },
|
||||
{ name: 'Server 1U', portsCount: 4, color: '#41B3A3', rackUnits: 1 },
|
||||
{ name: 'Server 2U', portsCount: 4, color: '#41B3A3', rackUnits: 2 },
|
||||
{ name: 'Server 4U', portsCount: 8, color: '#38A169', rackUnits: 4 },
|
||||
{ name: 'Storage', portsCount: 8, color: '#38A169', rackUnits: 2 },
|
||||
{ name: 'Patch Panel 24', portsCount: 24, color: '#9B59B6', rackUnits: 1 },
|
||||
{ name: 'Patch Panel 48', portsCount: 48, color: '#A569BD', rackUnits: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
408
server/db.js
Normal file
408
server/db.js
Normal file
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* Database Layer using better-sqlite3
|
||||
* Synchronous, simpler, and faster than callback-based sqlite3
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const config = require('./config');
|
||||
|
||||
class DatacenterDB {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database connection and schema
|
||||
*/
|
||||
init() {
|
||||
// Open database connection
|
||||
this.db = new Database(config.database.path);
|
||||
|
||||
// Configure database
|
||||
if (config.database.walMode) {
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
}
|
||||
if (config.database.foreignKeys) {
|
||||
this.db.pragma('foreign_keys = ON');
|
||||
}
|
||||
if (config.database.busyTimeout) {
|
||||
this.db.pragma(`busy_timeout = ${config.database.busyTimeout}`);
|
||||
}
|
||||
|
||||
console.log('Connected to SQLite database with better-sqlite3');
|
||||
|
||||
// Create schema
|
||||
this.createTables();
|
||||
this.seedDeviceTypes();
|
||||
this.ensureDefaultProject();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all database tables
|
||||
*/
|
||||
createTables() {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS racks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
x REAL NOT NULL,
|
||||
y REAL NOT NULL,
|
||||
width REAL DEFAULT ${config.rack.defaultWidth},
|
||||
height REAL DEFAULT ${config.rack.defaultHeight},
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
UNIQUE(project_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS device_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
ports_count INTEGER NOT NULL DEFAULT 24,
|
||||
color TEXT DEFAULT '#4A90E2',
|
||||
rack_units INTEGER DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_type_id INTEGER NOT NULL,
|
||||
rack_id INTEGER NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
rack_units INTEGER DEFAULT 1,
|
||||
logical_x REAL,
|
||||
logical_y REAL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (device_type_id) REFERENCES device_types(id),
|
||||
FOREIGN KEY (rack_id) REFERENCES racks(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
UNIQUE(project_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS connections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_device_id INTEGER NOT NULL,
|
||||
source_port INTEGER NOT NULL,
|
||||
target_device_id INTEGER NOT NULL,
|
||||
target_port INTEGER NOT NULL,
|
||||
waypoints_physical TEXT,
|
||||
waypoints_logical TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (source_device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (target_device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
UNIQUE(source_device_id, source_port),
|
||||
UNIQUE(target_device_id, target_port)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_racks_project ON racks(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_rack ON devices(rack_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_project ON devices(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_type ON devices(device_type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_source ON connections(source_device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_target ON connections(target_device_id);
|
||||
`);
|
||||
|
||||
console.log('Database schema created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed device types with defaults
|
||||
*/
|
||||
seedDeviceTypes() {
|
||||
const insert = this.db.prepare(`
|
||||
INSERT OR IGNORE INTO device_types (name, ports_count, color, rack_units)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertMany = this.db.transaction((types) => {
|
||||
for (const type of types) {
|
||||
insert.run(type.name, type.portsCount, type.color, type.rackUnits);
|
||||
}
|
||||
});
|
||||
|
||||
insertMany(config.deviceTypes);
|
||||
console.log('Device types seeded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure default project exists
|
||||
*/
|
||||
ensureDefaultProject() {
|
||||
const insert = this.db.prepare(`
|
||||
INSERT OR IGNORE INTO projects (id, name, description)
|
||||
VALUES (1, ?, ?)
|
||||
`);
|
||||
|
||||
insert.run('Default Project', 'Default datacenter project');
|
||||
console.log('Default project ensured');
|
||||
}
|
||||
|
||||
// ==================== PROJECT OPERATIONS ====================
|
||||
|
||||
getAllProjects() {
|
||||
return this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all();
|
||||
}
|
||||
|
||||
getProject(id) {
|
||||
return this.db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
createProject(name, description = '') {
|
||||
const stmt = this.db.prepare('INSERT INTO projects (name, description) VALUES (?, ?)');
|
||||
const info = stmt.run(name, description);
|
||||
return { id: info.lastInsertRowid, name, description };
|
||||
}
|
||||
|
||||
updateProject(id, name, description) {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE projects
|
||||
SET name = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`);
|
||||
stmt.run(name, description, id);
|
||||
}
|
||||
|
||||
deleteProject(id) {
|
||||
// Check if this is the last project
|
||||
const count = this.db.prepare('SELECT COUNT(*) as count FROM projects').get();
|
||||
if (count.count <= 1) {
|
||||
throw new Error('Cannot delete the last project');
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare('DELETE FROM projects WHERE id = ?');
|
||||
stmt.run(id);
|
||||
}
|
||||
|
||||
// ==================== RACK OPERATIONS ====================
|
||||
|
||||
getAllRacks(projectId) {
|
||||
return this.db.prepare('SELECT * FROM racks WHERE project_id = ? ORDER BY name').all(projectId);
|
||||
}
|
||||
|
||||
createRack(projectId, name, x, y) {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO racks (project_id, name, x, y)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const info = stmt.run(projectId, name, x, y);
|
||||
|
||||
// Fetch and return the complete rack data
|
||||
return this.db.prepare('SELECT * FROM racks WHERE id = ?').get(info.lastInsertRowid);
|
||||
}
|
||||
|
||||
updateRackPosition(id, x, y) {
|
||||
const stmt = this.db.prepare('UPDATE racks SET x = ?, y = ? WHERE id = ?');
|
||||
stmt.run(x, y, id);
|
||||
}
|
||||
|
||||
updateRackName(id, name) {
|
||||
const stmt = this.db.prepare('UPDATE racks SET name = ? WHERE id = ?');
|
||||
stmt.run(name, id);
|
||||
}
|
||||
|
||||
deleteRack(id) {
|
||||
const stmt = this.db.prepare('DELETE FROM racks WHERE id = ?');
|
||||
stmt.run(id);
|
||||
}
|
||||
|
||||
getNextRackName(projectId, prefix = 'RACK') {
|
||||
const racks = this.db.prepare(`
|
||||
SELECT name FROM racks
|
||||
WHERE project_id = ? AND name LIKE ?
|
||||
ORDER BY name DESC
|
||||
LIMIT 1
|
||||
`).all(projectId, `${prefix}%`);
|
||||
|
||||
if (racks.length === 0) {
|
||||
return `${prefix}01`;
|
||||
}
|
||||
|
||||
// Extract number from last rack name
|
||||
const lastNum = parseInt(racks[0].name.replace(prefix, '')) || 0;
|
||||
const nextNum = (lastNum + 1).toString().padStart(2, '0');
|
||||
return `${prefix}${nextNum}`;
|
||||
}
|
||||
|
||||
// ==================== DEVICE TYPE OPERATIONS ====================
|
||||
|
||||
getAllDeviceTypes() {
|
||||
return this.db.prepare('SELECT * FROM device_types ORDER BY name').all();
|
||||
}
|
||||
|
||||
// ==================== DEVICE OPERATIONS ====================
|
||||
|
||||
getAllDevices(projectId) {
|
||||
return this.db.prepare(`
|
||||
SELECT d.*, dt.name as type_name, dt.ports_count, dt.color
|
||||
FROM devices d
|
||||
JOIN device_types dt ON d.device_type_id = dt.id
|
||||
JOIN racks r ON d.rack_id = r.id
|
||||
WHERE r.project_id = ?
|
||||
ORDER BY d.rack_id, d.position
|
||||
`).all(projectId);
|
||||
}
|
||||
|
||||
createDevice(deviceTypeId, rackId, projectId, position, name) {
|
||||
// Get rack_units from device_type
|
||||
const deviceType = this.db.prepare('SELECT rack_units FROM device_types WHERE id = ?').get(deviceTypeId);
|
||||
const rackUnits = deviceType ? deviceType.rack_units : 1;
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO devices (device_type_id, rack_id, project_id, position, name, rack_units)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
const info = stmt.run(deviceTypeId, rackId, projectId, position, name, rackUnits);
|
||||
return { id: info.lastInsertRowid };
|
||||
}
|
||||
|
||||
deleteDevice(id) {
|
||||
const stmt = this.db.prepare('DELETE FROM devices WHERE id = ?');
|
||||
stmt.run(id);
|
||||
}
|
||||
|
||||
updateDeviceRack(id, rackId, position) {
|
||||
// Get project_id from the new rack
|
||||
const rack = this.db.prepare('SELECT project_id FROM racks WHERE id = ?').get(rackId);
|
||||
if (!rack) {
|
||||
throw new Error('Rack not found');
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE devices SET rack_id = ?, project_id = ?, position = ? WHERE id = ?
|
||||
`);
|
||||
stmt.run(rackId, rack.project_id, position, id);
|
||||
}
|
||||
|
||||
updateDeviceLogicalPosition(id, x, y) {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE devices SET logical_x = ?, logical_y = ? WHERE id = ?
|
||||
`);
|
||||
stmt.run(x, y, id);
|
||||
}
|
||||
|
||||
updateDeviceName(id, name) {
|
||||
const stmt = this.db.prepare('UPDATE devices SET name = ? WHERE id = ?');
|
||||
stmt.run(name, id);
|
||||
}
|
||||
|
||||
updateDeviceRackUnits(id, rackUnits) {
|
||||
const stmt = this.db.prepare('UPDATE devices SET rack_units = ? WHERE id = ?');
|
||||
stmt.run(rackUnits, id);
|
||||
}
|
||||
|
||||
getUsedPorts(deviceId) {
|
||||
const ports = this.db.prepare(`
|
||||
SELECT source_port as port FROM connections WHERE source_device_id = ?
|
||||
UNION
|
||||
SELECT target_port as port FROM connections WHERE target_device_id = ?
|
||||
`).all(deviceId, deviceId);
|
||||
|
||||
return ports.map(p => p.port);
|
||||
}
|
||||
|
||||
// ==================== CONNECTION OPERATIONS ====================
|
||||
|
||||
getAllConnections(projectId) {
|
||||
return this.db.prepare(`
|
||||
SELECT c.* FROM connections c
|
||||
JOIN devices d ON c.source_device_id = d.id
|
||||
JOIN racks r ON d.rack_id = r.id
|
||||
WHERE r.project_id = ?
|
||||
`).all(projectId);
|
||||
}
|
||||
|
||||
createConnection(sourceDeviceId, sourcePort, targetDeviceId, targetPort) {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO connections (source_device_id, source_port, target_device_id, target_port)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const info = stmt.run(sourceDeviceId, sourcePort, targetDeviceId, targetPort);
|
||||
return { id: info.lastInsertRowid };
|
||||
}
|
||||
|
||||
updateConnection(id, sourceDeviceId, sourcePort, targetDeviceId, targetPort) {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE connections
|
||||
SET source_device_id = ?, source_port = ?, target_device_id = ?, target_port = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
stmt.run(sourceDeviceId, sourcePort, targetDeviceId, targetPort, id);
|
||||
}
|
||||
|
||||
updateConnectionWaypoints(id, waypoints, view = null) {
|
||||
const waypointsJson = JSON.stringify(waypoints);
|
||||
|
||||
let query;
|
||||
if (view === 'physical') {
|
||||
query = 'UPDATE connections SET waypoints_physical = ? WHERE id = ?';
|
||||
} else if (view === 'logical') {
|
||||
query = 'UPDATE connections SET waypoints_logical = ? WHERE id = ?';
|
||||
} else {
|
||||
// For backwards compatibility
|
||||
query = 'UPDATE connections SET waypoints_physical = ?, waypoints_logical = ? WHERE id = ?';
|
||||
const stmt = this.db.prepare(query);
|
||||
stmt.run(waypointsJson, waypointsJson, id);
|
||||
return;
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(query);
|
||||
stmt.run(waypointsJson, id);
|
||||
}
|
||||
|
||||
deleteConnection(id) {
|
||||
const stmt = this.db.prepare('DELETE FROM connections WHERE id = ?');
|
||||
stmt.run(id);
|
||||
}
|
||||
|
||||
// ==================== UTILITY METHODS ====================
|
||||
|
||||
/**
|
||||
* Execute a transaction
|
||||
* @param {Function} fn - Function containing database operations
|
||||
* @returns {*} - Return value of the transaction function
|
||||
*/
|
||||
transaction(fn) {
|
||||
return this.db.transaction(fn)();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
close() {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
console.log('Database connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
*/
|
||||
getStats() {
|
||||
const projects = this.db.prepare('SELECT COUNT(*) as count FROM projects').get();
|
||||
const racks = this.db.prepare('SELECT COUNT(*) as count FROM racks').get();
|
||||
const devices = this.db.prepare('SELECT COUNT(*) as count FROM devices').get();
|
||||
const connections = this.db.prepare('SELECT COUNT(*) as count FROM connections').get();
|
||||
|
||||
return {
|
||||
projects: projects.count,
|
||||
racks: racks.count,
|
||||
devices: devices.count,
|
||||
connections: connections.count
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
module.exports = new DatacenterDB();
|
||||
66
server/lib/errorHandler.js
Normal file
66
server/lib/errorHandler.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Error Handling Middleware
|
||||
* Centralized error handling for consistent API responses
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error handler middleware
|
||||
* Catches errors from routes and formats response
|
||||
*/
|
||||
function errorHandler(err, req, res, next) {
|
||||
// Log error for debugging
|
||||
console.error('Error:', err);
|
||||
|
||||
// Default error response
|
||||
let statusCode = 500;
|
||||
let message = 'Internal server error';
|
||||
|
||||
// Handle specific error types
|
||||
if (err.message) {
|
||||
message = err.message;
|
||||
|
||||
// SQLite constraint errors
|
||||
if (err.message.includes('UNIQUE constraint')) {
|
||||
statusCode = 409; // Conflict
|
||||
message = 'A record with that value already exists';
|
||||
} else if (err.message.includes('FOREIGN KEY constraint')) {
|
||||
statusCode = 400; // Bad Request
|
||||
message = 'Invalid reference to related record';
|
||||
} else if (err.message.includes('NOT NULL constraint')) {
|
||||
statusCode = 400;
|
||||
message = 'Required field is missing';
|
||||
} else if (err.message.includes('CHECK constraint')) {
|
||||
statusCode = 400;
|
||||
message = 'Value does not meet validation requirements';
|
||||
}
|
||||
|
||||
// Custom application errors
|
||||
else if (err.message.includes('not found')) {
|
||||
statusCode = 404;
|
||||
} else if (err.message.includes('Cannot delete')) {
|
||||
statusCode = 400;
|
||||
}
|
||||
}
|
||||
|
||||
// Send error response
|
||||
res.status(statusCode).json({
|
||||
error: message,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 Not Found handler
|
||||
* Catches requests to undefined routes
|
||||
*/
|
||||
function notFoundHandler(req, res) {
|
||||
res.status(404).json({
|
||||
error: 'Route not found',
|
||||
path: req.path
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
errorHandler,
|
||||
notFoundHandler
|
||||
};
|
||||
147
server/routes/connections.js
Normal file
147
server/routes/connections.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Connections Routes
|
||||
* All routes related to connection management
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
|
||||
/**
|
||||
* GET /api/connections
|
||||
* Get all connections for a project
|
||||
*/
|
||||
router.get('/', (req, res, next) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const connections = db.getAllConnections(projectId);
|
||||
res.json(connections);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/connections
|
||||
* Create a new connection
|
||||
*/
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!sourceDeviceId) {
|
||||
return res.status(400).json({ error: 'Source device ID is required' });
|
||||
}
|
||||
if (typeof sourcePort !== 'number' || sourcePort < 1) {
|
||||
return res.status(400).json({ error: 'Valid source port number is required' });
|
||||
}
|
||||
if (!targetDeviceId) {
|
||||
return res.status(400).json({ error: 'Target device ID is required' });
|
||||
}
|
||||
if (typeof targetPort !== 'number' || targetPort < 1) {
|
||||
return res.status(400).json({ error: 'Valid target port number is required' });
|
||||
}
|
||||
if (sourceDeviceId === targetDeviceId) {
|
||||
return res.status(400).json({ error: 'Cannot connect device to itself' });
|
||||
}
|
||||
|
||||
const connection = db.createConnection(
|
||||
sourceDeviceId,
|
||||
sourcePort,
|
||||
targetDeviceId,
|
||||
targetPort
|
||||
);
|
||||
res.status(201).json(connection);
|
||||
} catch (err) {
|
||||
// Handle unique constraint violations (port already in use)
|
||||
if (err.message && err.message.includes('UNIQUE constraint')) {
|
||||
return res.status(400).json({ error: 'One or both ports are already in use' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/connections/:id
|
||||
* Update a connection
|
||||
*/
|
||||
router.put('/:id', (req, res, next) => {
|
||||
try {
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!sourceDeviceId) {
|
||||
return res.status(400).json({ error: 'Source device ID is required' });
|
||||
}
|
||||
if (typeof sourcePort !== 'number' || sourcePort < 1) {
|
||||
return res.status(400).json({ error: 'Valid source port number is required' });
|
||||
}
|
||||
if (!targetDeviceId) {
|
||||
return res.status(400).json({ error: 'Target device ID is required' });
|
||||
}
|
||||
if (typeof targetPort !== 'number' || targetPort < 1) {
|
||||
return res.status(400).json({ error: 'Valid target port number is required' });
|
||||
}
|
||||
if (sourceDeviceId === targetDeviceId) {
|
||||
return res.status(400).json({ error: 'Cannot connect device to itself' });
|
||||
}
|
||||
|
||||
db.updateConnection(
|
||||
req.params.id,
|
||||
sourceDeviceId,
|
||||
sourcePort,
|
||||
targetDeviceId,
|
||||
targetPort
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
if (err.message && err.message.includes('UNIQUE constraint')) {
|
||||
return res.status(400).json({ error: 'One or both ports are already in use' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/connections/:id/waypoints
|
||||
* Update connection waypoints
|
||||
*/
|
||||
router.put('/:id/waypoints', (req, res, next) => {
|
||||
try {
|
||||
const { waypoints, view } = req.body;
|
||||
|
||||
if (!waypoints || !Array.isArray(waypoints)) {
|
||||
return res.status(400).json({ error: 'Waypoints must be an array' });
|
||||
}
|
||||
|
||||
// Validate waypoint structure
|
||||
for (const waypoint of waypoints) {
|
||||
if (typeof waypoint.x !== 'number' || typeof waypoint.y !== 'number') {
|
||||
return res.status(400).json({
|
||||
error: 'Each waypoint must have valid x and y coordinates'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
db.updateConnectionWaypoints(req.params.id, waypoints, view);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/connections/:id
|
||||
* Delete a connection
|
||||
*/
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
db.deleteConnection(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
186
server/routes/devices.js
Normal file
186
server/routes/devices.js
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Devices Routes
|
||||
* All routes related to device management
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* GET /api/device-types
|
||||
* Get all device types
|
||||
*/
|
||||
router.get('/types', (req, res, next) => {
|
||||
try {
|
||||
const types = db.getAllDeviceTypes();
|
||||
res.json(types);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/devices
|
||||
* Get all devices for a project
|
||||
*/
|
||||
router.get('/', (req, res, next) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const devices = db.getAllDevices(projectId);
|
||||
res.json(devices);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/devices/:id/used-ports
|
||||
* Get used ports for a device
|
||||
*/
|
||||
router.get('/:id/used-ports', (req, res, next) => {
|
||||
try {
|
||||
const ports = db.getUsedPorts(req.params.id);
|
||||
res.json(ports);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/devices
|
||||
* Create a new device
|
||||
*/
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const { deviceTypeId, rackId, position, name } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!deviceTypeId) {
|
||||
return res.status(400).json({ error: 'Device type ID is required' });
|
||||
}
|
||||
if (!rackId) {
|
||||
return res.status(400).json({ error: 'Rack ID is required' });
|
||||
}
|
||||
if (typeof position !== 'number' || position < 1 || position > config.rack.slots) {
|
||||
return res.status(400).json({
|
||||
error: `Position must be between 1 and ${config.rack.slots}`
|
||||
});
|
||||
}
|
||||
if (!name || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Device name is required' });
|
||||
}
|
||||
|
||||
// Get project_id from the rack
|
||||
const rack = db.db.prepare('SELECT project_id FROM racks WHERE id = ?').get(rackId);
|
||||
if (!rack) {
|
||||
return res.status(404).json({ error: 'Rack not found' });
|
||||
}
|
||||
|
||||
const device = db.createDevice(deviceTypeId, rackId, rack.project_id, position, name.trim());
|
||||
res.status(201).json(device);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/devices/:id/rack
|
||||
* Update device rack and position
|
||||
*/
|
||||
router.put('/:id/rack', (req, res, next) => {
|
||||
try {
|
||||
const { rackId, position } = req.body;
|
||||
|
||||
if (!rackId) {
|
||||
return res.status(400).json({ error: 'Rack ID is required' });
|
||||
}
|
||||
if (typeof position !== 'number' || position < 1 || position > config.rack.slots) {
|
||||
return res.status(400).json({
|
||||
error: `Position must be between 1 and ${config.rack.slots}`
|
||||
});
|
||||
}
|
||||
|
||||
db.updateDeviceRack(req.params.id, rackId, position);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/devices/:id/logical-position
|
||||
* Update device logical view position
|
||||
*/
|
||||
router.put('/:id/logical-position', (req, res, next) => {
|
||||
try {
|
||||
const { x, y } = req.body;
|
||||
|
||||
if (typeof x !== 'number' || typeof y !== 'number') {
|
||||
return res.status(400).json({ error: 'Valid x and y coordinates are required' });
|
||||
}
|
||||
|
||||
db.updateDeviceLogicalPosition(req.params.id, x, y);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/devices/:id/name
|
||||
* Update device name
|
||||
*/
|
||||
router.put('/:id/name', (req, res, next) => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Device name is required' });
|
||||
}
|
||||
|
||||
db.updateDeviceName(req.params.id, name.trim());
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/devices/:id/rack-units
|
||||
* Update device rack units (form factor)
|
||||
*/
|
||||
router.put('/:id/rack-units', (req, res, next) => {
|
||||
try {
|
||||
const { rackUnits } = req.body;
|
||||
const min = config.device.minRackUnits || 1;
|
||||
const max = config.device.maxRackUnits || config.rack.slots;
|
||||
|
||||
if (typeof rackUnits !== 'number' || rackUnits < min || rackUnits > max) {
|
||||
return res.status(400).json({
|
||||
error: `Rack units must be between ${min} and ${max}`
|
||||
});
|
||||
}
|
||||
|
||||
db.updateDeviceRackUnits(req.params.id, rackUnits);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/devices/:id
|
||||
* Delete a device
|
||||
*/
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
db.deleteDevice(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
90
server/routes/projects.js
Normal file
90
server/routes/projects.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Projects Routes
|
||||
* All routes related to project management
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
|
||||
/**
|
||||
* GET /api/projects
|
||||
* Get all projects
|
||||
*/
|
||||
router.get('/', (req, res, next) => {
|
||||
try {
|
||||
const projects = db.getAllProjects();
|
||||
res.json(projects);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/projects/:id
|
||||
* Get a specific project
|
||||
*/
|
||||
router.get('/:id', (req, res, next) => {
|
||||
try {
|
||||
const project = db.getProject(req.params.id);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Project not found' });
|
||||
}
|
||||
res.json(project);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/projects
|
||||
* Create a new project
|
||||
*/
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
const project = db.createProject(name.trim(), description || '');
|
||||
res.status(201).json(project);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/projects/:id
|
||||
* Update a project
|
||||
*/
|
||||
router.put('/:id', (req, res, next) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
db.updateProject(req.params.id, name.trim(), description || '');
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/projects/:id
|
||||
* Delete a project
|
||||
*/
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
db.deleteProject(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
113
server/routes/racks.js
Normal file
113
server/routes/racks.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Racks Routes
|
||||
* All routes related to rack management
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
|
||||
/**
|
||||
* GET /api/racks
|
||||
* Get all racks for a project
|
||||
*/
|
||||
router.get('/', (req, res, next) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const racks = db.getAllRacks(projectId);
|
||||
res.json(racks);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/racks/next-name
|
||||
* Get next available rack name for a prefix
|
||||
*/
|
||||
router.get('/next-name', (req, res, next) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const prefix = req.query.prefix || 'RACK';
|
||||
const name = db.getNextRackName(projectId, prefix);
|
||||
res.json({ name });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/racks
|
||||
* Create a new rack
|
||||
*/
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const { projectId, name, x, y } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!name || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Rack name is required' });
|
||||
}
|
||||
if (typeof x !== 'number' || typeof y !== 'number') {
|
||||
return res.status(400).json({ error: 'Valid x and y coordinates are required' });
|
||||
}
|
||||
|
||||
const rack = db.createRack(projectId || 1, name.trim(), x, y);
|
||||
res.status(201).json(rack);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/racks/:id/position
|
||||
* Update rack position
|
||||
*/
|
||||
router.put('/:id/position', (req, res, next) => {
|
||||
try {
|
||||
const { x, y } = req.body;
|
||||
|
||||
if (typeof x !== 'number' || typeof y !== 'number') {
|
||||
return res.status(400).json({ error: 'Valid x and y coordinates are required' });
|
||||
}
|
||||
|
||||
db.updateRackPosition(req.params.id, x, y);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/racks/:id/name
|
||||
* Update rack name
|
||||
*/
|
||||
router.put('/:id/name', (req, res, next) => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Rack name is required' });
|
||||
}
|
||||
|
||||
db.updateRackName(req.params.id, name.trim());
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/racks/:id
|
||||
* Delete a rack
|
||||
*/
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
db.deleteRack(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
83
server/server.js
Normal file
83
server/server.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Datacenter Designer Server
|
||||
* Express application with modular routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const config = require('./config');
|
||||
const db = require('./db');
|
||||
const { errorHandler, notFoundHandler } = require('./lib/errorHandler');
|
||||
|
||||
// Import route modules
|
||||
const projectsRouter = require('./routes/projects');
|
||||
const racksRouter = require('./routes/racks');
|
||||
const devicesRouter = require('./routes/devices');
|
||||
const connectionsRouter = require('./routes/connections');
|
||||
|
||||
// Initialize Express app
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: config.api.jsonLimit }));
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// Optional: Request logging (development)
|
||||
if (config.logging.enabled && config.server.env === 'development') {
|
||||
app.use((req, res, next) => {
|
||||
console.log(`${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// API Routes
|
||||
app.use('/api/projects', projectsRouter);
|
||||
app.use('/api/racks', racksRouter);
|
||||
app.use('/api/devices', devicesRouter);
|
||||
app.use('/api/device-types', devicesRouter); // Mounted under /devices/types
|
||||
app.use('/api/connections', connectionsRouter);
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
const stats = db.getStats();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
version: '1.0.0',
|
||||
database: stats
|
||||
});
|
||||
});
|
||||
|
||||
// 404 handler - must be after all other routes
|
||||
app.use(notFoundHandler);
|
||||
|
||||
// Error handler - must be last
|
||||
app.use(errorHandler);
|
||||
|
||||
// Initialize database and start server
|
||||
db.init();
|
||||
|
||||
app.listen(config.server.port, () => {
|
||||
console.log(`
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ Datacenter Designer Server ║
|
||||
║ ║
|
||||
║ Running on: http://${config.server.host}:${config.server.port} ║
|
||||
║ Environment: ${config.server.env} ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM signal received: closing HTTP server');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT signal received: closing HTTP server');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user