feat: migrate from static config to database and add authentication
- Replaced static hosts.json and staticConfig.ts with SQLite database (Prisma) - Implemented JWT authentication and Login UI - Added dynamic API routes for hosts, topology, and settings - Updated UI components to fetch and manage state dynamically - Added Settings interface for managing hosts and topology nodes
This commit is contained in:
@@ -1,76 +1,31 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { homedir } from 'os';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { HostConfig } from './types';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const CONFIG_FILE = path.join(__dirname, 'hosts.json');
|
||||
|
||||
function parseEnvHosts(): HostConfig[] {
|
||||
const hostsEnv = process.env.SSH_HOSTS;
|
||||
if (!hostsEnv) return [];
|
||||
|
||||
const hosts: HostConfig[] = [];
|
||||
const entries = hostsEnv.split(',').map(h => h.trim()).filter(Boolean);
|
||||
|
||||
for (const entry of entries) {
|
||||
const [name, ip] = entry.split(':');
|
||||
if (name && ip) {
|
||||
hosts.push({
|
||||
name: name.trim(),
|
||||
ip: ip.trim(),
|
||||
sshUser: process.env.SSH_USER || 'bear',
|
||||
sshKeyPath: process.env.SSH_KEY,
|
||||
sshPort: process.env.SSH_PORT ? parseInt(process.env.SSH_PORT, 10) : 22,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hosts;
|
||||
}
|
||||
|
||||
function parseJsonConfig(): HostConfig[] {
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
console.error('Config file not found:', CONFIG_FILE);
|
||||
return [];
|
||||
}
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getHostConfigs(): Promise<HostConfig[]> {
|
||||
try {
|
||||
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (!data.hosts || !Array.isArray(data.hosts)) {
|
||||
console.error('No hosts array in config');
|
||||
return [];
|
||||
}
|
||||
|
||||
const hosts = data.hosts.map((h: Partial<HostConfig>) => ({
|
||||
name: h.name || '',
|
||||
ip: h.ip || '',
|
||||
const dbConfigs = await prisma.hostConfig.findMany();
|
||||
return dbConfigs.map(h => ({
|
||||
name: h.name,
|
||||
ip: h.ip,
|
||||
sshUser: h.sshUser || 'bear',
|
||||
sshKeyPath: h.sshKeyPath?.replace(/^~/, homedir()),
|
||||
sshPort: h.sshPort || 22,
|
||||
})).filter((h: HostConfig) => h.name && h.ip);
|
||||
|
||||
console.error('Loaded hosts:', JSON.stringify(hosts));
|
||||
return hosts;
|
||||
} catch (e: any) {
|
||||
console.error('Config parse error:', e.message);
|
||||
hostType: h.hostType,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching host configs from DB:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getHostConfigs(): HostConfig[] {
|
||||
const envHosts = parseEnvHosts();
|
||||
if (envHosts.length > 0) {
|
||||
return envHosts;
|
||||
export async function hasConfig(): Promise<boolean> {
|
||||
try {
|
||||
const count = await prisma.hostConfig.count();
|
||||
return count > 0;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parseJsonConfig();
|
||||
}
|
||||
|
||||
export function hasConfig(): boolean {
|
||||
return getHostConfigs().length > 0;
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"hosts": [
|
||||
{
|
||||
"name": "ubuntu",
|
||||
"ip": "192.168.50.61",
|
||||
"sshUser": "bear",
|
||||
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||
"sshPort": 22,
|
||||
"hostType": "docker-host"
|
||||
},
|
||||
{
|
||||
"name": "grizzley",
|
||||
"ip": "192.168.50.84",
|
||||
"sshUser": "bear",
|
||||
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||
"sshPort": 22,
|
||||
"hostType": "docker-host"
|
||||
},
|
||||
{
|
||||
"name": "truenas",
|
||||
"ip": "192.168.50.12",
|
||||
"sshUser": "root",
|
||||
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||
"sshPort": 22,
|
||||
"hostType": "truenas"
|
||||
},
|
||||
{
|
||||
"name": "proxmox",
|
||||
"ip": "192.168.50.11",
|
||||
"sshUser": "root",
|
||||
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||
"sshPort": 22,
|
||||
"hostType": "proxmox"
|
||||
},
|
||||
{
|
||||
"name": "ice",
|
||||
"ip": "192.168.50.197",
|
||||
"sshUser": "bear",
|
||||
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||
"sshPort": 22,
|
||||
"hostType": "docker-host"
|
||||
},
|
||||
{
|
||||
"name": "panda",
|
||||
"ip": "192.168.50.196",
|
||||
"sshUser": "bear",
|
||||
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||
"sshPort": 22,
|
||||
"hostType": "docker-host"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import configRouter from './routes/config';
|
||||
import statsRouter from './routes/stats';
|
||||
import filesRouter from './routes/files';
|
||||
import terminalRouter from './routes/terminal';
|
||||
import authRouter from './routes/auth';
|
||||
import topologyRouter from './routes/topology';
|
||||
import hostsRouter from './routes/hosts';
|
||||
import { requireAuth } from './middleware/auth';
|
||||
import { getHostConfigs } from './config';
|
||||
import { requestLogger, logger } from './middleware/requestLogger';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
@@ -17,10 +21,13 @@ const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const PORT = 3001;
|
||||
|
||||
const allowedOrigins = ['http://localhost:3000', 'http://localhost:4173'];
|
||||
const corsOrigin = process.env.CORS_ORIGIN ? [process.env.CORS_ORIGIN, ...allowedOrigins] : allowedOrigins;
|
||||
|
||||
// --- Socket.IO setup (websocket-engineer skill) ---
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||
origin: corsOrigin,
|
||||
credentials: true,
|
||||
},
|
||||
pingInterval: 25000,
|
||||
@@ -45,7 +52,7 @@ app.use(helmet({
|
||||
|
||||
// CORS — restrict to configured origins
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||
origin: corsOrigin,
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
@@ -85,18 +92,23 @@ app.get('/api/health', (_req, res) => {
|
||||
|
||||
// --- Debug endpoint (dev only) ---
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
app.get('/api/debug-config', (_req, res) => {
|
||||
const hosts = getHostConfigs();
|
||||
app.get('/api/debug-config', async (_req, res) => {
|
||||
const hosts = await getHostConfigs();
|
||||
res.json({ hosts });
|
||||
});
|
||||
}
|
||||
|
||||
// --- Routes ---
|
||||
app.use('/api', discoverRouter);
|
||||
app.use('/api', configRouter);
|
||||
app.use('/api', statsRouter);
|
||||
app.use('/api', filesRouter);
|
||||
app.use('/api', terminalRouter);
|
||||
// --- Public Routes ---
|
||||
app.use('/api', authRouter);
|
||||
|
||||
// --- Protected Routes ---
|
||||
app.use('/api', requireAuth, discoverRouter);
|
||||
app.use('/api', requireAuth, configRouter);
|
||||
app.use('/api', requireAuth, statsRouter);
|
||||
app.use('/api', requireAuth, filesRouter);
|
||||
app.use('/api', requireAuth, terminalRouter);
|
||||
app.use('/api', requireAuth, topologyRouter);
|
||||
app.use('/api', requireAuth, hostsRouter);
|
||||
|
||||
// --- Global error handler (must be last) ---
|
||||
app.use(errorHandler);
|
||||
|
||||
27
server/middleware/auth.ts
Normal file
27
server/middleware/auth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-for-development-only';
|
||||
|
||||
export const requireAuth = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ status: 'error', message: 'Authentication required' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string; username: string };
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ status: 'error', message: 'Invalid or expired token' });
|
||||
}
|
||||
};
|
||||
78
server/routes/auth.ts
Normal file
78
server/routes/auth.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-for-development-only';
|
||||
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // Limit each IP to 5 failed login attempts per window
|
||||
message: { status: 'error', message: 'Too many login attempts, please try again later' },
|
||||
skipSuccessfulRequests: true,
|
||||
});
|
||||
|
||||
// Check if setup is required (no users exist)
|
||||
router.get('/auth/status', async (req, res) => {
|
||||
try {
|
||||
const userCount = await prisma.user.count();
|
||||
res.json({ setupRequired: userCount === 0 });
|
||||
} catch (error) {
|
||||
res.status(500).json({ status: 'error', message: 'Database error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Initial Setup (Only works if no users exist)
|
||||
router.post('/auth/setup', async (req, res) => {
|
||||
try {
|
||||
const userCount = await prisma.user.count();
|
||||
if (userCount > 0) {
|
||||
return res.status(403).json({ status: 'error', message: 'Setup has already been completed' });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password || password.length < 8) {
|
||||
return res.status(400).json({ status: 'error', message: 'Invalid username or password must be at least 8 characters' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const user = await prisma.user.create({
|
||||
data: { username, password: hashedPassword },
|
||||
});
|
||||
|
||||
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '7d' });
|
||||
res.json({ status: 'success', token, user: { id: user.id, username: user.username } });
|
||||
} catch (error) {
|
||||
res.status(500).json({ status: 'error', message: 'Failed to create user' });
|
||||
}
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post('/auth/login', loginLimiter, async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ status: 'error', message: 'Username and password required' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { username } });
|
||||
if (!user) {
|
||||
return res.status(401).json({ status: 'error', message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.password);
|
||||
if (!valid) {
|
||||
return res.status(401).json({ status: 'error', message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '7d' });
|
||||
res.json({ status: 'success', token, user: { id: user.id, username: user.username } });
|
||||
} catch (error) {
|
||||
res.status(500).json({ status: 'error', message: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -163,7 +163,7 @@ router.get('/config/:host/:container', async (req, res) => {
|
||||
|
||||
try {
|
||||
// Find host config
|
||||
const hostConfigs = getHostConfigs();
|
||||
const hostConfigs = await getHostConfigs();
|
||||
const hostConfig = hostConfigs.find(h => h.name === host);
|
||||
|
||||
if (!hostConfig) {
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
import { Router } from 'express';
|
||||
import { execSync } from 'child_process';
|
||||
import { homedir } from 'os';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getHostConfigs } from '../config';
|
||||
import { DiscoveryResponse } from '../types';
|
||||
import { io } from '../index';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface HostDiscoveryResult {
|
||||
name: string;
|
||||
@@ -35,11 +38,11 @@ async function discoverHost(
|
||||
console.error(`DEBUG: ${name} keyPath=${keyPath}, user=${sshUser}`);
|
||||
const keyArg = `-i ${keyPath}`;
|
||||
const portArg = sshPort && sshPort !== 22 ? `-p ${sshPort}` : '';
|
||||
|
||||
|
||||
const dockerCmd = `ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${keyArg} ${portArg} ${sshUser}@${ip} "docker ps --format '{{.Names}}'" 2>/dev/null`;
|
||||
const dockerOutput = execSync(dockerCmd, { encoding: 'utf-8', timeout: 15000 });
|
||||
const containers = dockerOutput.trim().split('\n').filter(c => c.trim());
|
||||
|
||||
|
||||
let services: string[] = [];
|
||||
if (hostType !== 'proxmox') {
|
||||
try {
|
||||
@@ -50,7 +53,7 @@ async function discoverHost(
|
||||
console.error(`DEBUG: ${name} systemd discovery failed`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let vms: Array<{ id: string; name: string; status: string; type: 'lxc' | 'qemu' }> = [];
|
||||
if (hostType === 'proxmox' || name === 'proxmox') {
|
||||
try {
|
||||
@@ -63,7 +66,7 @@ async function discoverHost(
|
||||
vms.push({ id: parts[0], name: parts[1], status: parts[2] || 'unknown', type: 'lxc' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const vmCmd = `ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${keyArg} ${portArg} ${sshUser}@${ip} "qm list" 2>/dev/null`;
|
||||
const vmOutput = execSync(vmCmd, { encoding: 'utf-8', timeout: 10000 });
|
||||
const vmLines = vmOutput.trim().split('\n').slice(1);
|
||||
@@ -77,7 +80,7 @@ async function discoverHost(
|
||||
console.error(`DEBUG: ${name} Proxmox discovery failed`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
name,
|
||||
ip,
|
||||
@@ -97,18 +100,53 @@ async function discoverHost(
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/discover - Discover all hosts via SSH
|
||||
// POST /api/discover - Trigger discovery but return cached immediately
|
||||
router.post('/discover', async (req, res) => {
|
||||
try {
|
||||
const hosts = getHostConfigs();
|
||||
|
||||
// 1. Fetch from cache immediately for fast loading
|
||||
const cachedSetting = await prisma.settings.findUnique({ where: { key: 'last_discovery' } });
|
||||
|
||||
let previousState: DiscoveryResponse = {
|
||||
hosts: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: [],
|
||||
};
|
||||
|
||||
if (cachedSetting && cachedSetting.value) {
|
||||
try {
|
||||
previousState = JSON.parse(cachedSetting.value);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse cached discovery state');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Return cached response to unblock the UI request
|
||||
res.json(previousState);
|
||||
|
||||
// 3. Kick off background discovery
|
||||
runBackgroundDiscovery();
|
||||
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
hosts: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: [error.message || 'Cache retrieval failed'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function runBackgroundDiscovery() {
|
||||
try {
|
||||
const hosts = await getHostConfigs();
|
||||
|
||||
if (hosts.length === 0) {
|
||||
const response: DiscoveryResponse = {
|
||||
hosts: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: ['No hosts configured'],
|
||||
};
|
||||
return res.json(response);
|
||||
await cacheAndEmit(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const results: HostDiscoveryResult[] = [];
|
||||
@@ -134,7 +172,7 @@ router.post('/discover', async (req, res) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const errors: string[] = [];
|
||||
results.forEach((result: HostDiscoveryResult) => {
|
||||
if (!result.online && result.error) {
|
||||
@@ -155,15 +193,26 @@ router.post('/discover', async (req, res) => {
|
||||
errors,
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
await cacheAndEmit(response);
|
||||
} catch (error: any) {
|
||||
const response: DiscoveryResponse = {
|
||||
hosts: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: [error.message || 'Discovery failed'],
|
||||
};
|
||||
res.status(500).json(response);
|
||||
console.error('Background discovery failed:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function cacheAndEmit(response: DiscoveryResponse) {
|
||||
try {
|
||||
// Cache the standard response to the DB
|
||||
await prisma.settings.upsert({
|
||||
where: { key: 'last_discovery' },
|
||||
update: { value: JSON.stringify(response) },
|
||||
create: { key: 'last_discovery', value: JSON.stringify(response) },
|
||||
});
|
||||
|
||||
// Broadcast the full update via Socket.IO
|
||||
io.emit('topology:update', response);
|
||||
} catch (err) {
|
||||
console.error('Failed to cache and emit discovery results:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -136,7 +136,7 @@ router.get('/files/:host/:container', async (req, res) => {
|
||||
try {
|
||||
const { host, container } = req.params;
|
||||
|
||||
const hosts = getHostConfigs();
|
||||
const hosts = await getHostConfigs();
|
||||
const hostConfig = hosts.find(h => h.name.toLowerCase() === host.toLowerCase());
|
||||
|
||||
if (!hostConfig) {
|
||||
@@ -240,7 +240,7 @@ router.get('/files/browse/:host', async (req, res) => {
|
||||
const { host } = req.params;
|
||||
const path = (req.query.path as string) || '/';
|
||||
|
||||
const hosts = getHostConfigs();
|
||||
const hosts = await getHostConfigs();
|
||||
const hostConfig = hosts.find(h => h.name.toLowerCase() === host.toLowerCase());
|
||||
|
||||
if (!hostConfig) {
|
||||
|
||||
50
server/routes/hosts.ts
Normal file
50
server/routes/hosts.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Router } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// GET all host configurations
|
||||
router.get('/hosts', async (req, res) => {
|
||||
try {
|
||||
const hosts = await prisma.hostConfig.findMany();
|
||||
res.json({ status: 'success', hosts });
|
||||
} catch (error) {
|
||||
res.status(500).json({ status: 'error', message: 'Failed to fetch host configs' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST a new host configuration
|
||||
router.post('/hosts', async (req, res) => {
|
||||
try {
|
||||
const host = await prisma.hostConfig.create({ data: req.body });
|
||||
res.json({ status: 'success', host });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to create host config. Ensure name is unique.' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT (update) a host configuration
|
||||
router.put('/hosts/:id', async (req, res) => {
|
||||
try {
|
||||
const host = await prisma.hostConfig.update({
|
||||
where: { id: req.params.id },
|
||||
data: req.body,
|
||||
});
|
||||
res.json({ status: 'success', host });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to update host config' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE a host configuration
|
||||
router.delete('/hosts/:id', async (req, res) => {
|
||||
try {
|
||||
await prisma.hostConfig.delete({ where: { id: req.params.id } });
|
||||
res.json({ status: 'success' });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to delete host config' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -131,7 +131,7 @@ router.get('/stats/:host/:container', async (req, res) => {
|
||||
const { host, container } = req.params;
|
||||
|
||||
// Find host config by name
|
||||
const hosts = getHostConfigs();
|
||||
const hosts = await getHostConfigs();
|
||||
const hostConfig = hosts.find(h => h.name.toLowerCase() === host.toLowerCase());
|
||||
|
||||
if (!hostConfig) {
|
||||
|
||||
@@ -18,7 +18,7 @@ router.post('/terminal/exec', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Missing host or command' });
|
||||
}
|
||||
|
||||
const hosts = getHostConfigs();
|
||||
const hosts = await getHostConfigs();
|
||||
const hostConfig = hosts.find(h => h.name === hostName);
|
||||
|
||||
if (!hostConfig) {
|
||||
@@ -44,7 +44,7 @@ router.post('/terminal/exec', async (req, res) => {
|
||||
|
||||
router.get('/terminal/hosts', async (_req, res) => {
|
||||
try {
|
||||
const hosts = getHostConfigs();
|
||||
const hosts = await getHostConfigs();
|
||||
res.json({
|
||||
hosts: hosts.map(h => ({
|
||||
name: h.name,
|
||||
|
||||
71
server/routes/topology.ts
Normal file
71
server/routes/topology.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Router } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// GET all nodes and edges
|
||||
router.get('/topology', async (req, res) => {
|
||||
try {
|
||||
const nodes = await prisma.networkNode.findMany();
|
||||
const edges = await prisma.networkEdge.findMany();
|
||||
res.json({ status: 'success', nodes, edges });
|
||||
} catch (error) {
|
||||
res.status(500).json({ status: 'error', message: 'Failed to fetch topology' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST a new node
|
||||
router.post('/topology/nodes', async (req, res) => {
|
||||
try {
|
||||
const node = await prisma.networkNode.create({ data: req.body });
|
||||
res.json({ status: 'success', node });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to create node' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT (update) a node
|
||||
router.put('/topology/nodes/:id', async (req, res) => {
|
||||
try {
|
||||
const node = await prisma.networkNode.update({
|
||||
where: { id: req.params.id },
|
||||
data: req.body,
|
||||
});
|
||||
res.json({ status: 'success', node });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to update node' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE a node
|
||||
router.delete('/topology/nodes/:id', async (req, res) => {
|
||||
try {
|
||||
await prisma.networkNode.delete({ where: { id: req.params.id } });
|
||||
res.json({ status: 'success' });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to delete node' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST a new edge
|
||||
router.post('/topology/edges', async (req, res) => {
|
||||
try {
|
||||
const edge = await prisma.networkEdge.create({ data: req.body });
|
||||
res.json({ status: 'success', edge });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to create edge' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE an edge
|
||||
router.delete('/topology/edges/:id', async (req, res) => {
|
||||
try {
|
||||
await prisma.networkEdge.delete({ where: { id: req.params.id } });
|
||||
res.json({ status: 'success' });
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Failed to delete edge' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user