First commit

This commit is contained in:
Stefano Manfredi
2025-10-27 11:57:38 +00:00
commit 3431a121a9
34 changed files with 17474 additions and 0 deletions

116
server/config.js Normal file
View 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
View 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();

View 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
};

View 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
View 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
View 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
View 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
View 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);
});