Initial commit
This commit is contained in:
546
server.ts
Normal file
546
server.ts
Normal file
@ -0,0 +1,546 @@
|
||||
import 'dotenv/config';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import path from 'path';
|
||||
import { createServer as createViteServer } from 'vite';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import db from './server-db';
|
||||
import { Device, LabTemplate, Booking, LogEntry, User, QuickLink } from './src/types';
|
||||
|
||||
const uid = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'ghostgrid-dev-secret-change-in-production';
|
||||
const JWT_EXPIRY = '24h';
|
||||
|
||||
interface JwtPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: JwtPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader?.split(' ')[1];
|
||||
if (!token) {
|
||||
res.status(401).json({ error: 'Authentication required.' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Invalid or expired token.' });
|
||||
}
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const PORT = Number(process.env.PORT) || 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// AUTH API
|
||||
// -------------------------------------------------------------
|
||||
app.post('/api/auth/register', (req, res) => {
|
||||
try {
|
||||
const { name, email, password } = req.body;
|
||||
if (!name || !email || !password) {
|
||||
return res.status(400).json({ error: 'Name, email and password are required.' });
|
||||
}
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({ error: 'Password must be at least 8 characters.' });
|
||||
}
|
||||
|
||||
const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'An account with this email already exists.' });
|
||||
}
|
||||
|
||||
const passwordHash = bcrypt.hashSync(password, 10);
|
||||
const id = uid("u");
|
||||
|
||||
db.prepare('INSERT INTO users (id, name, role, email, password_hash) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(id, name, 'User', email, passwordHash);
|
||||
|
||||
const user = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(id) as User;
|
||||
const token = jwt.sign({ userId: id, email }, JWT_SECRET, { expiresIn: JWT_EXPIRY });
|
||||
|
||||
res.status(201).json({ token, user });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password are required.' });
|
||||
}
|
||||
|
||||
const row = db.prepare('SELECT * FROM users WHERE email = ?').get(email) as (User & { password_hash: string }) | undefined;
|
||||
if (!row || !bcrypt.compareSync(password, row.password_hash)) {
|
||||
return res.status(401).json({ error: 'Invalid email or password.' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId: row.id, email: row.email }, JWT_SECRET, { expiresIn: JWT_EXPIRY });
|
||||
const user: User = { id: row.id, name: row.name, role: row.role, email: row.email };
|
||||
|
||||
res.json({ token, user });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/auth/me', requireAuth, (req, res) => {
|
||||
try {
|
||||
const user = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(req.user!.userId) as User | undefined;
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found.' });
|
||||
}
|
||||
res.json(user);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RESTFUL API: Users
|
||||
// -------------------------------------------------------------
|
||||
app.get('/api/users', requireAuth, (_req, res) => {
|
||||
try {
|
||||
const users = db.prepare('SELECT id, name, role, email FROM users').all() as User[];
|
||||
res.json(users);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RESTFUL API: Devices / Inventory
|
||||
// -------------------------------------------------------------
|
||||
app.get('/api/devices', requireAuth, (_req, res) => {
|
||||
try {
|
||||
const devices = db.prepare('SELECT * FROM devices').all() as Device[];
|
||||
res.json(devices);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/devices', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl, lastCheckedAt } = req.body;
|
||||
if (!hostname || !ip || !type) {
|
||||
return res.status(400).json({ error: 'Missing required device specifications.' });
|
||||
}
|
||||
|
||||
const id = uid("dev");
|
||||
db.prepare(`
|
||||
INSERT INTO devices (id, hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl, lastCheckedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, hostname, ip, location || '', notes || '', type, status || 'unknown', emergencySheet || '', checkMkUrl || '', lastCheckedAt || null);
|
||||
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'maintenance',
|
||||
`Provisioned a new host candidate "${hostname}" (${ip}) inside location ${location || 'Inventory'}.`,
|
||||
id, req.user!.userId);
|
||||
|
||||
const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device;
|
||||
res.status(201).json(device);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/devices/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl, lastCheckedAt, operatorName } = req.body;
|
||||
|
||||
db.prepare(`
|
||||
UPDATE devices SET hostname = ?, ip = ?, location = ?, notes = ?, type = ?, status = ?, emergencySheet = ?, checkMkUrl = ?, lastCheckedAt = ?
|
||||
WHERE id = ?
|
||||
`).run(hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl ?? '', lastCheckedAt ?? null, id);
|
||||
|
||||
const logId = uid("log");
|
||||
const operatorText = operatorName ? `${operatorName} finished ` : 'Updated ';
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'maintenance',
|
||||
`${operatorText}refining the device specifications for "${hostname}".`, id, req.user!.userId);
|
||||
|
||||
const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device;
|
||||
res.json(device);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/devices/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const dev = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device;
|
||||
if (!dev) return res.status(404).json({ error: 'Device not found.' });
|
||||
|
||||
db.prepare('DELETE FROM devices WHERE id = ?').run(id);
|
||||
|
||||
const labs = db.prepare('SELECT * FROM labs').all() as any[];
|
||||
const updateLabStmt = db.prepare('UPDATE labs SET deviceIds = ?, topology = ? WHERE id = ?');
|
||||
for (const lab of labs) {
|
||||
const deviceIds: string[] = JSON.parse(lab.deviceIds);
|
||||
const topology: any[] = JSON.parse(lab.topology);
|
||||
updateLabStmt.run(
|
||||
JSON.stringify(deviceIds.filter(dId => dId !== id)),
|
||||
JSON.stringify(topology.filter(t => t.fromDevice !== id && t.toDevice !== id)),
|
||||
lab.id
|
||||
);
|
||||
}
|
||||
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'maintenance',
|
||||
`Permanently removed the host device "${dev.hostname || id}" from the inventory records.`,
|
||||
null, req.user!.userId);
|
||||
|
||||
res.json({ success: true, message: 'Device deleted successfully and cleaned from topologies.' });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RESTFUL API: Lab Templates
|
||||
// -------------------------------------------------------------
|
||||
app.get('/api/labs', requireAuth, (_req, res) => {
|
||||
try {
|
||||
const rows = db.prepare('SELECT * FROM labs').all() as any[];
|
||||
const labs: LabTemplate[] = rows.map(r => ({
|
||||
id: r.id, name: r.name, description: r.description,
|
||||
contactPerson: r.contactPerson, location: r.location,
|
||||
deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology)
|
||||
}));
|
||||
res.json(labs);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/labs', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { name, description, contactPerson, location, deviceIds, topology } = req.body;
|
||||
if (!name || !deviceIds || !Array.isArray(deviceIds)) {
|
||||
return res.status(400).json({ error: 'Missing name or associated device configurations.' });
|
||||
}
|
||||
|
||||
const id = uid("lab");
|
||||
db.prepare(`INSERT INTO labs (id, name, description, contactPerson, location, deviceIds, topology) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(id, name, description || '', contactPerson || '', location || '', JSON.stringify(deviceIds), JSON.stringify(topology || []));
|
||||
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'maintenance',
|
||||
`Released a new lab template profile "${name}" (${location || 'Staging Area'}) for engineering teams.`,
|
||||
req.user!.userId);
|
||||
|
||||
const r = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any;
|
||||
res.status(201).json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology) });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/labs/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { name, description, contactPerson, location, deviceIds, topology } = req.body;
|
||||
|
||||
db.prepare(`UPDATE labs SET name = ?, description = ?, contactPerson = ?, location = ?, deviceIds = ?, topology = ? WHERE id = ?`)
|
||||
.run(name, description, contactPerson, location, JSON.stringify(deviceIds), JSON.stringify(topology), id);
|
||||
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'maintenance',
|
||||
`Modified the active topology mapping schema for the "${name}" lab template.`, req.user!.userId);
|
||||
|
||||
const r = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any;
|
||||
res.json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology) });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/labs/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const lab = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any;
|
||||
if (!lab) return res.status(404).json({ error: 'Lab template not found.' });
|
||||
|
||||
db.prepare('DELETE FROM labs WHERE id = ?').run(id);
|
||||
db.prepare(`UPDATE bookings SET status = 'cancelled' WHERE labId = ? AND status = 'upcoming'`).run(id);
|
||||
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'booking',
|
||||
`Withdrew the lab testing template "${lab.name || id}".`, req.user!.userId);
|
||||
|
||||
res.json({ success: true, message: 'Lab template deleted and future reservations cancelled.' });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RESTFUL API: Bookings / Reservations
|
||||
// -------------------------------------------------------------
|
||||
app.get('/api/bookings', requireAuth, (_req, res) => {
|
||||
try {
|
||||
const rows = db.prepare('SELECT * FROM bookings').all() as any[];
|
||||
const bookings: Booking[] = rows.map(r => ({
|
||||
id: r.id, labId: r.labId, userId: r.userId,
|
||||
startDateTime: r.startDateTime, endDateTime: r.endDateTime,
|
||||
notes: r.notes || '', status: r.status as any,
|
||||
notified: r.notified === 1, emailSent: r.emailSent === 1
|
||||
}));
|
||||
res.json(bookings);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/bookings', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { labId, userId, startDateTime, endDateTime, notes, status, operatorName } = req.body;
|
||||
if (!labId || !userId || !startDateTime || !endDateTime) {
|
||||
return res.status(400).json({ error: 'Missing reservation timestamps or laboratory ID.' });
|
||||
}
|
||||
|
||||
const id = uid("book");
|
||||
db.prepare(`INSERT INTO bookings (id, labId, userId, startDateTime, endDateTime, notes, status, notified, emailSent) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 1)`)
|
||||
.run(id, labId, userId, startDateTime, endDateTime, notes || '', status || 'upcoming');
|
||||
|
||||
const lab = db.prepare('SELECT name FROM labs WHERE id = ?').get(labId) as { name: string } | undefined;
|
||||
const logId = uid("log");
|
||||
const operatorText = operatorName || 'An operator';
|
||||
const startF = new Date(startDateTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||
const endF = new Date(endDateTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'booking',
|
||||
`${operatorText} booked the lab template "${lab?.name || 'Unknown'}" from ${startF} to ${endF}.`, userId);
|
||||
|
||||
const r = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any;
|
||||
res.status(201).json({
|
||||
booking: { id: r.id, labId: r.labId, userId: r.userId, startDateTime: r.startDateTime, endDateTime: r.endDateTime, notes: r.notes || '', status: r.status, notified: r.notified === 1, emailSent: r.emailSent === 1 },
|
||||
alertGenerated: `Reservation successfully approved: Lab "${lab?.name || 'Scenario'}" is reserved for your exclusive usage during this window.`
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/bookings/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { status, operatorName } = req.body;
|
||||
|
||||
const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any;
|
||||
if (!booking) return res.status(404).json({ error: 'Reservation not found.' });
|
||||
|
||||
db.prepare('UPDATE bookings SET status = ? WHERE id = ?').run(status, id);
|
||||
|
||||
if (status === 'cancelled') {
|
||||
const lab = db.prepare('SELECT name FROM labs WHERE id = ?').get(booking.labId) as { name: string } | undefined;
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'booking',
|
||||
`${operatorName || 'An operator'} canceled the reservation for lab template "${lab?.name || 'Unknown'}" and released associated nodes back into the pool.`,
|
||||
req.user!.userId);
|
||||
}
|
||||
|
||||
const r = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any;
|
||||
res.json({ id: r.id, labId: r.labId, userId: r.userId, startDateTime: r.startDateTime, endDateTime: r.endDateTime, notes: r.notes || '', status: r.status, notified: r.notified === 1, emailSent: r.emailSent === 1 });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/bookings/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any;
|
||||
if (!booking) return res.status(404).json({ error: 'Reservation not found.' });
|
||||
|
||||
db.prepare('DELETE FROM bookings WHERE id = ?').run(id);
|
||||
const lab = db.prepare('SELECT name FROM labs WHERE id = ?').get(booking.labId) as { name: string } | undefined;
|
||||
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'booking',
|
||||
`Permanently deleted the reservation records for lab "${lab?.name || 'Unknown'}".`, req.user!.userId);
|
||||
|
||||
res.json({ success: true, message: 'Reservation and its logs have been resolved correctly.' });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RESTFUL API: Logs
|
||||
// -------------------------------------------------------------
|
||||
app.get('/api/logs', requireAuth, (_req, res) => {
|
||||
try {
|
||||
const logs = db.prepare('SELECT * FROM logs ORDER BY timestamp DESC').all() as LogEntry[];
|
||||
res.json(logs);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/logs', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { type, message, deviceId, userId } = req.body;
|
||||
if (!message || !type) {
|
||||
return res.status(400).json({ error: 'Missing log message or classification type.' });
|
||||
}
|
||||
|
||||
const id = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`)
|
||||
.run(id, new Date().toISOString(), type, message, deviceId || null, userId || null);
|
||||
|
||||
const log = db.prepare('SELECT * FROM logs WHERE id = ?').get(id) as LogEntry;
|
||||
res.status(201).json(log);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RESTFUL API: Quick Links (shared link dashboard)
|
||||
// -------------------------------------------------------------
|
||||
app.get('/api/links', requireAuth, (_req, res) => {
|
||||
try {
|
||||
const links = db.prepare('SELECT * FROM links ORDER BY category ASC, title ASC').all() as QuickLink[];
|
||||
res.json(links);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/links', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { title, url, description, category, color } = req.body;
|
||||
if (!title || !url) {
|
||||
return res.status(400).json({ error: 'A title and a URL are required.' });
|
||||
}
|
||||
|
||||
const id = uid("link");
|
||||
const createdAt = new Date().toISOString();
|
||||
db.prepare(`INSERT INTO links (id, title, url, description, category, color, createdBy, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(id, title, url, description || '', category || '', color || 'emerald', req.user!.userId, createdAt);
|
||||
|
||||
const link = db.prepare('SELECT * FROM links WHERE id = ?').get(id) as QuickLink;
|
||||
res.status(201).json(link);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/links/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { title, url, description, category, color } = req.body;
|
||||
const existing = db.prepare('SELECT id FROM links WHERE id = ?').get(id);
|
||||
if (!existing) return res.status(404).json({ error: 'Link not found.' });
|
||||
|
||||
db.prepare(`UPDATE links SET title = ?, url = ?, description = ?, category = ?, color = ? WHERE id = ?`)
|
||||
.run(title, url, description || '', category || '', color || 'emerald', id);
|
||||
|
||||
const link = db.prepare('SELECT * FROM links WHERE id = ?').get(id) as QuickLink;
|
||||
res.json(link);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/links/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const existing = db.prepare('SELECT id FROM links WHERE id = ?').get(id);
|
||||
if (!existing) return res.status(404).json({ error: 'Link not found.' });
|
||||
|
||||
db.prepare('DELETE FROM links WHERE id = ?').run(id);
|
||||
res.json({ success: true, message: 'Link removed from the shared dashboard.' });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// VITE / STATIC SERVING
|
||||
// -------------------------------------------------------------
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: 'spa',
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), 'dist');
|
||||
app.use(express.static(distPath));
|
||||
app.get('*', (_req, res) => {
|
||||
res.sendFile(path.join(distPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CYCLIC CHECKMK STATUS SYNC
|
||||
// The device status shown in the UI is owned by CheckMK, not the app.
|
||||
// This job runs on an interval and reconciles each *linked* device's status
|
||||
// from the CheckMK REST API. The frontend additionally polls /api/devices,
|
||||
// so anything written here surfaces in the inventory & booking screens.
|
||||
// Until CHECKMK_API_URL/SECRET are configured, devices stay 'unknown' (and
|
||||
// therefore not bookable) - which is the intended safe default.
|
||||
// -------------------------------------------------------------
|
||||
const CHECKMK_SYNC_INTERVAL_MS = Number(process.env.CHECKMK_SYNC_INTERVAL_MS) || 60_000;
|
||||
const CHECKMK_API_URL = process.env.CHECKMK_API_URL; // e.g. https://checkmk.internal/<site>/check_mk/api/1.0
|
||||
const CHECKMK_API_SECRET = process.env.CHECKMK_API_SECRET; // automation user secret
|
||||
|
||||
async function syncCheckMkStatuses() {
|
||||
if (!CHECKMK_API_URL || !CHECKMK_API_SECRET) return; // not configured - leave statuses as 'unknown'
|
||||
const rows = db.prepare("SELECT id, hostname, checkMkUrl FROM devices WHERE checkMkUrl != ''")
|
||||
.all() as { id: string; hostname: string; checkMkUrl: string }[];
|
||||
for (const dev of rows) {
|
||||
try {
|
||||
// TODO(checkmk): query the host's hard state from the CheckMK API using the
|
||||
// automation secret, map 0 (UP) -> 'online' and anything else -> 'offline':
|
||||
// const res = await fetch(`${CHECKMK_API_URL}/objects/host/${host}/actions/...`,
|
||||
// { headers: { Authorization: `Bearer automation ${CHECKMK_API_SECRET}` } });
|
||||
// const state = (await res.json()).extensions.state === 0 ? 'online' : 'offline';
|
||||
// db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?')
|
||||
// .run(state, new Date().toISOString(), dev.id);
|
||||
} catch (err) {
|
||||
console.error(`[CheckMK] Status sync failed for ${dev.hostname}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
setInterval(syncCheckMkStatuses, CHECKMK_SYNC_INTERVAL_MS);
|
||||
syncCheckMkStatuses();
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[Server] Core Server running at http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer().catch(err => {
|
||||
console.error('[Server] Critical Crash during bootstrap:', err);
|
||||
});
|
||||
Reference in New Issue
Block a user