refactor(db): rename redirect_path→redirect, add uid/addLog helpers, simplify Caddy CRUD
- Rename caddy.redirect_path to caddy.redirect across schema, server, frontend and docs - Remove obsolete ALTER TABLE migration (fresh-install model has no migrations) - Move uid() from server.ts to server-db.ts for shared use - Add addLog() general helper (prepared statement, shared timestamp support) and replace ~24 inline INSERT INTO logs calls throughout server.ts - Caddy CRUD now takes CaddyRouteInput object instead of positional arguments; add/update reuse getCaddyRouteById() to avoid duplicate SELECT
This commit is contained in:
@ -282,7 +282,7 @@ CREATE TABLE IF NOT EXISTS caddy (
|
||||
upstream TEXT NOT NULL,
|
||||
tls INTEGER NOT NULL DEFAULT 1,
|
||||
compress INTEGER NOT NULL DEFAULT 1,
|
||||
redirect_path TEXT NOT NULL DEFAULT '', -- optional 'redir / <path>' for the bare root
|
||||
redirect TEXT NOT NULL DEFAULT '', -- optional 'redir / <path>' for the bare root
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
@ -464,8 +464,8 @@ Manual: POST /api/semaphore/trigger/{bookingId} body { type: 'setup'|'teardown
|
||||
```
|
||||
buildCaddyfile():
|
||||
{ local_certs } # global block
|
||||
per custom route { [encode] [tls internal] [redir / <redirect_path>] reverse_proxy <upstream> { … } }
|
||||
redirect_path set → `redir / <path>` redirects only the bare root '/'
|
||||
per custom route { [encode] [tls internal] [redir / <redirect>] reverse_proxy <upstream> { … } }
|
||||
redirect set → `redir / <path>` redirects only the bare root '/'
|
||||
(other paths pass through; e.g. CheckMK served at /<site>/check_mk/)
|
||||
every reverse_proxy block carries standard forwarding headers:
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
@ -716,7 +716,7 @@ Backup : ghostgrid.db + ghostgrid.db-wal + ghostgrid.db-shm
|
||||
```
|
||||
GhostGrid/
|
||||
+-- server.ts # Express app: all routes, auth, integrations, background jobs
|
||||
+-- server-db.ts # SQLite connection, full schema, settings/Caddy helpers
|
||||
+-- server-db.ts # SQLite connection, full schema, settings/Caddy/log helpers (uid, addLog)
|
||||
+-- index.html # Vite HTML entry (#root > src/main.tsx); links /favicon.svg
|
||||
+-- public/
|
||||
| +-- favicon.svg # app favicon (GhostGrid logo; served at site root by Vite)
|
||||
|
||||
67
server-db.ts
67
server-db.ts
@ -3,6 +3,9 @@ import path from 'path';
|
||||
|
||||
export const DB_FILE = path.join(process.cwd(), 'ghostgrid.db');
|
||||
|
||||
/** App-generated primary key: `${prefix}-${epochMs}-${rand}` (e.g. `log-…`, `dev-…`). */
|
||||
export const uid = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
|
||||
console.log(`[Database] Connecting to SQLite database at: ${DB_FILE}`);
|
||||
const db = new Database(DB_FILE);
|
||||
|
||||
@ -91,14 +94,11 @@ db.exec(`
|
||||
upstream TEXT NOT NULL,
|
||||
tls INTEGER NOT NULL DEFAULT 1,
|
||||
compress INTEGER NOT NULL DEFAULT 1,
|
||||
redirect_path TEXT NOT NULL DEFAULT '',
|
||||
redirect TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
|
||||
// redirect_path was added later; ensure it exists on databases created before it.
|
||||
try { db.exec("ALTER TABLE caddy ADD COLUMN redirect_path TEXT NOT NULL DEFAULT ''"); } catch { /* column already exists */ }
|
||||
|
||||
// Seed default settings — INSERT OR IGNORE writes a key only if it is absent.
|
||||
const DEFAULT_SETTINGS: Record<string, string> = {
|
||||
azure_enabled: 'false',
|
||||
@ -117,7 +117,7 @@ const DEFAULT_SETTINGS: Record<string, string> = {
|
||||
semaphore_api_token: '',
|
||||
semaphore_project_id: '',
|
||||
caddy_enabled: 'false',
|
||||
caddy_admin_url: 'http://localhost:2019',
|
||||
caddy_admin_url: 'http://127.0.0.1:2019',
|
||||
};
|
||||
|
||||
const seedSetting = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
|
||||
@ -137,39 +137,68 @@ export function getAllSettings(): Record<string, string> {
|
||||
return Object.fromEntries(rows.map(r => [r.key, r.value]));
|
||||
}
|
||||
|
||||
const insertLog = db.prepare(
|
||||
'INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
);
|
||||
|
||||
/**
|
||||
* Append a logbook entry. `deviceId`/`userId` default to NULL; `timestamp`
|
||||
* defaults to now (pass one to share a single timestamp across a batch).
|
||||
* Returns the generated log id.
|
||||
*/
|
||||
export function addLog(
|
||||
type: string,
|
||||
message: string,
|
||||
opts: { deviceId?: string | null; userId?: string | null; timestamp?: string } = {},
|
||||
): string {
|
||||
const id = uid('log');
|
||||
insertLog.run(id, opts.timestamp ?? new Date().toISOString(), type, message, opts.deviceId ?? null, opts.userId ?? null);
|
||||
return id;
|
||||
}
|
||||
|
||||
/** A reverse-proxy route as stored (booleans are SQLite 0/1 integers). */
|
||||
export interface CaddyRoute {
|
||||
id: number;
|
||||
hostname: string;
|
||||
upstream: string;
|
||||
tls: number;
|
||||
compress: number;
|
||||
redirect_path: string;
|
||||
redirect: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Fields a caller supplies to create or update a route (JS booleans). */
|
||||
export interface CaddyRouteInput {
|
||||
hostname: string;
|
||||
upstream: string;
|
||||
tls: boolean;
|
||||
compress: boolean;
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
export function getCaddyRoutes(): CaddyRoute[] {
|
||||
return db.prepare('SELECT * FROM caddy ORDER BY id ASC').all() as CaddyRoute[];
|
||||
}
|
||||
|
||||
export function addCaddyRoute(hostname: string, upstream: string, tls: boolean, compress: boolean, redirectPath = ''): CaddyRoute {
|
||||
const { lastInsertRowid } = db.prepare(
|
||||
'INSERT INTO caddy (hostname, upstream, tls, compress, redirect_path) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(hostname, upstream, tls ? 1 : 0, compress ? 1 : 0, redirectPath);
|
||||
return db.prepare('SELECT * FROM caddy WHERE id = ?').get(lastInsertRowid) as CaddyRoute;
|
||||
export function getCaddyRouteById(id: number): CaddyRoute | undefined {
|
||||
return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute | undefined;
|
||||
}
|
||||
|
||||
export function updateCaddyRoute(id: number, hostname: string, upstream: string, tls: boolean, compress: boolean, redirectPath = ''): CaddyRoute {
|
||||
db.prepare('UPDATE caddy SET hostname = ?, upstream = ?, tls = ?, compress = ?, redirect_path = ? WHERE id = ?')
|
||||
.run(hostname, upstream, tls ? 1 : 0, compress ? 1 : 0, redirectPath, id);
|
||||
return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute;
|
||||
export function addCaddyRoute(route: CaddyRouteInput): CaddyRoute {
|
||||
const { lastInsertRowid } = db.prepare(
|
||||
'INSERT INTO caddy (hostname, upstream, tls, compress, redirect) VALUES (?, ?, ?, ?, ?)',
|
||||
).run(route.hostname, route.upstream, route.tls ? 1 : 0, route.compress ? 1 : 0, route.redirect ?? '');
|
||||
return getCaddyRouteById(Number(lastInsertRowid))!;
|
||||
}
|
||||
|
||||
export function updateCaddyRoute(id: number, route: CaddyRouteInput): CaddyRoute {
|
||||
db.prepare('UPDATE caddy SET hostname = ?, upstream = ?, tls = ?, compress = ?, redirect = ? WHERE id = ?')
|
||||
.run(route.hostname, route.upstream, route.tls ? 1 : 0, route.compress ? 1 : 0, route.redirect ?? '', id);
|
||||
return getCaddyRouteById(id)!;
|
||||
}
|
||||
|
||||
export function deleteCaddyRoute(id: number): void {
|
||||
db.prepare('DELETE FROM caddy WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
export function getCaddyRouteById(id: number): CaddyRoute | undefined {
|
||||
return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute | undefined;
|
||||
}
|
||||
|
||||
export default db;
|
||||
155
server.ts
155
server.ts
@ -7,11 +7,9 @@ import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import DatabaseConstructor from 'better-sqlite3';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import db, { getSetting, setSetting, getAllSettings, getCaddyRoutes, addCaddyRoute, updateCaddyRoute, deleteCaddyRoute, getCaddyRouteById, DB_FILE } from './server-db';
|
||||
import db, { uid, addLog, getSetting, setSetting, getAllSettings, getCaddyRoutes, addCaddyRoute, updateCaddyRoute, deleteCaddyRoute, getCaddyRouteById, DB_FILE } 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';
|
||||
|
||||
@ -87,10 +85,10 @@ function buildCaddyfile(): string {
|
||||
lines.push(`${route.hostname} {`);
|
||||
if (route.compress) lines.push(' encode zstd gzip');
|
||||
if (route.tls) lines.push(' tls internal');
|
||||
if (route.redirect_path) {
|
||||
if (route.redirect) {
|
||||
// Redirect only the bare root ('/') to the given path — other paths pass
|
||||
// through to the backend unchanged (e.g. CheckMK at /<site>/check_mk/).
|
||||
const target = route.redirect_path.startsWith('/') ? route.redirect_path : `/${route.redirect_path}`;
|
||||
const target = route.redirect.startsWith('/') ? route.redirect : `/${route.redirect}`;
|
||||
lines.push(` redir / ${target}`);
|
||||
}
|
||||
lines.push(` reverse_proxy ${route.upstream} {`);
|
||||
@ -138,24 +136,21 @@ function importCaddyfileRoutes(userId?: string): void {
|
||||
const upstream = upstreamMatch[1];
|
||||
const tls = /tls\s+internal/.test(block);
|
||||
const compress = /encode/.test(block);
|
||||
addCaddyRoute(hostname, upstream, tls, compress);
|
||||
addCaddyRoute({ hostname, upstream, tls, compress });
|
||||
imported.push(`${hostname} → ${upstream}`);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (imported.length > 0) {
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(uid('log'), new Date().toISOString(), 'system',
|
||||
`Caddy: imported ${imported.length} route(s) from Caddyfile — ${imported.join(', ')}`,
|
||||
null, userId ?? null);
|
||||
addLog('system', `Caddy: imported ${imported.length} route(s) from Caddyfile — ${imported.join(', ')}`, { userId });
|
||||
}
|
||||
}
|
||||
|
||||
async function pushCaddyConfig(): Promise<void> {
|
||||
if (!IS_PRODUCTION) return;
|
||||
if (getSetting('caddy_enabled') !== 'true') return;
|
||||
const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019';
|
||||
const adminUrl = getSetting('caddy_admin_url') || 'http://127.0.0.1:2019';
|
||||
const body = buildCaddyfile();
|
||||
const res = await fetch(`${adminUrl}/load`, {
|
||||
method: 'POST',
|
||||
@ -241,9 +236,7 @@ async function startServer() {
|
||||
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 };
|
||||
|
||||
const logId = uid("log");
|
||||
db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`)
|
||||
.run(logId, new Date().toISOString(), 'system', `${row.name} logged in.`, null, row.id);
|
||||
addLog('system', `${row.name} logged in.`, { userId: row.id });
|
||||
|
||||
res.json({ token, user });
|
||||
} catch (err: any) {
|
||||
@ -269,7 +262,7 @@ async function startServer() {
|
||||
const clientId = getSetting('azure_client_id');
|
||||
const tenantId = getSetting('azure_tenant_id');
|
||||
const secret = getSetting('azure_client_secret');
|
||||
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
|
||||
const appUrl = process.env.APP_URL || `http://127.0.0.1:${PORT}`;
|
||||
const effectiveRedirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
|
||||
const cmkApiUrl = getSetting('checkmk_api_url') || process.env.CHECKMK_API_URL || '';
|
||||
res.json({
|
||||
@ -287,7 +280,7 @@ async function startServer() {
|
||||
if (!msalClient) {
|
||||
return res.redirect('/?auth_error=Azure+login+not+configured');
|
||||
}
|
||||
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
|
||||
const appUrl = process.env.APP_URL || `http://127.0.0.1:${PORT}`;
|
||||
const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
|
||||
try {
|
||||
const authCodeUrl = await msalClient.getAuthCodeUrl({
|
||||
@ -315,7 +308,7 @@ async function startServer() {
|
||||
if (!msalClient) {
|
||||
return res.redirect('/?auth_error=Azure+login+not+configured');
|
||||
}
|
||||
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
|
||||
const appUrl = process.env.APP_URL || `http://127.0.0.1:${PORT}`;
|
||||
const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
|
||||
try {
|
||||
const result = await msalClient.acquireTokenByCode({
|
||||
@ -457,11 +450,9 @@ async function startServer() {
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, hostname, ip, location || '', notes || '', type, status || 'unknown', emergencySheet || '', 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);
|
||||
addLog('maintenance',
|
||||
`Provisioned a new host candidate "${hostname}" (${ip}) inside location ${location || 'Inventory'}.`,
|
||||
{ deviceId: id, userId: req.user!.userId });
|
||||
|
||||
const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device;
|
||||
res.status(201).json(device);
|
||||
@ -480,11 +471,10 @@ async function startServer() {
|
||||
WHERE id = ?
|
||||
`).run(hostname, ip, location, notes, type, status, emergencySheet, 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);
|
||||
addLog('maintenance',
|
||||
`${operatorText}refining the device specifications for "${hostname}".`,
|
||||
{ deviceId: id, userId: req.user!.userId });
|
||||
|
||||
const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device;
|
||||
res.json(device);
|
||||
@ -513,11 +503,9 @@ async function startServer() {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
addLog('maintenance',
|
||||
`Permanently removed the host device "${dev.hostname || id}" from the inventory records.`,
|
||||
{ userId: req.user!.userId });
|
||||
|
||||
res.json({ success: true, message: 'Device deleted successfully and cleaned from topologies.' });
|
||||
} catch (err: any) {
|
||||
@ -555,11 +543,9 @@ async function startServer() {
|
||||
db.prepare(`INSERT INTO labs (id, name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(id, name, description || '', contactPerson || '', location || '', JSON.stringify(deviceIds), JSON.stringify(topology || []), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || '');
|
||||
|
||||
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);
|
||||
addLog('maintenance',
|
||||
`Released a new lab template profile "${name}" (${location || 'Staging Area'}) for engineering teams.`,
|
||||
{ userId: 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), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '' });
|
||||
@ -576,10 +562,9 @@ async function startServer() {
|
||||
db.prepare(`UPDATE labs SET name = ?, description = ?, contactPerson = ?, location = ?, deviceIds = ?, topology = ?, semaphoreSetupTemplateId = ?, semaphoreTeardownTemplateId = ? WHERE id = ?`)
|
||||
.run(name, description, contactPerson, location, JSON.stringify(deviceIds), JSON.stringify(topology), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || '', 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);
|
||||
addLog('maintenance',
|
||||
`Modified the active topology mapping schema for the "${name}" lab template.`,
|
||||
{ userId: 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), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '' });
|
||||
@ -597,10 +582,9 @@ async function startServer() {
|
||||
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);
|
||||
addLog('booking',
|
||||
`Withdrew the lab testing template "${lab.name || id}".`,
|
||||
{ userId: req.user!.userId });
|
||||
|
||||
res.json({ success: true, message: 'Lab template deleted and future reservations cancelled.' });
|
||||
} catch (err: any) {
|
||||
@ -642,14 +626,13 @@ async function startServer() {
|
||||
.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);
|
||||
addLog('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({
|
||||
@ -673,11 +656,9 @@ async function startServer() {
|
||||
|
||||
if (status === 'cancelled') {
|
||||
const lab = db.prepare('SELECT name, semaphoreTeardownTemplateId FROM labs WHERE id = ?').get(booking.labId) as { name: string; semaphoreTeardownTemplateId: 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);
|
||||
addLog('booking',
|
||||
`${operatorName || 'An operator'} canceled the reservation for lab template "${lab?.name || 'Unknown'}" and released associated nodes back into the pool.`,
|
||||
{ userId: req.user!.userId });
|
||||
|
||||
// Trigger teardown if booking had already started and teardown not yet triggered
|
||||
const now = new Date();
|
||||
@ -718,10 +699,9 @@ async function startServer() {
|
||||
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);
|
||||
addLog('booking',
|
||||
`Permanently deleted the reservation records for lab "${lab?.name || 'Unknown'}".`,
|
||||
{ userId: req.user!.userId });
|
||||
|
||||
res.json({ success: true, message: 'Reservation and its logs have been resolved correctly.' });
|
||||
} catch (err: any) {
|
||||
@ -748,9 +728,7 @@ async function startServer() {
|
||||
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 id = addLog(type, message, { deviceId: deviceId || null, userId: userId || null });
|
||||
|
||||
const log = db.prepare('SELECT * FROM logs WHERE id = ?').get(id) as LogEntry;
|
||||
res.status(201).json(log);
|
||||
@ -929,8 +907,7 @@ async function startServer() {
|
||||
const CHECKMK_API_SECRET = getSetting('checkmk_api_secret') || process.env.CHECKMK_API_SECRET;
|
||||
|
||||
if (!CHECKMK_API_URL || !CHECKMK_API_SECRET) {
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'system', 'CheckMK sync skipped — API URL or secret not configured. Check Settings.');
|
||||
addLog('system', 'CheckMK sync skipped — API URL or secret not configured. Check Settings.', { timestamp: now });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -958,8 +935,7 @@ async function startServer() {
|
||||
} catch (err: any) {
|
||||
const msg = `CheckMK sync failed — could not fetch host list: ${err?.message ?? err}`;
|
||||
console.error('[CheckMK]', msg);
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'system', msg);
|
||||
addLog('system', msg, { timestamp: now });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -975,8 +951,7 @@ async function startServer() {
|
||||
if (!cmkHost) {
|
||||
if (dev.status !== 'unknown') {
|
||||
db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ?, cmkHostname = ? WHERE id = ?').run('unknown', now, '', dev.id);
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) not found in monitoring — status set to unknown.`, dev.id);
|
||||
addLog('status', `CheckMK: ${dev.hostname} (${dev.ip}) not found in monitoring — status set to unknown.`, { deviceId: dev.id, timestamp: now });
|
||||
}
|
||||
counts.unknown++;
|
||||
continue;
|
||||
@ -993,22 +968,20 @@ async function startServer() {
|
||||
const newStatus = state === 0 ? 'online' : state === 1 || state === 2 ? 'offline' : 'unknown';
|
||||
db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ?, cmkHostname = ? WHERE id = ?').run(newStatus, now, cmkHost, dev.id);
|
||||
if (dev.status !== newStatus) {
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, dev.id);
|
||||
addLog('status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, { deviceId: dev.id, timestamp: now });
|
||||
}
|
||||
counts[newStatus as 'online' | 'offline' | 'unknown']++;
|
||||
} catch (err: any) {
|
||||
const msg = `CheckMK: status sync failed for ${dev.hostname} (${dev.ip}) — ${err?.message ?? err}`;
|
||||
console.error('[CheckMK]', msg);
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'system', msg, dev.id);
|
||||
addLog('system', msg, { deviceId: dev.id, timestamp: now });
|
||||
counts.unknown++;
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'system',
|
||||
`CheckMK sync completed — ${counts.online} online, ${counts.offline} offline, ${counts.unknown} unknown (${rows.length} devices total).`);
|
||||
addLog('system',
|
||||
`CheckMK sync completed — ${counts.online} online, ${counts.offline} offline, ${counts.unknown} unknown (${rows.length} devices total).`,
|
||||
{ timestamp: now });
|
||||
}
|
||||
|
||||
async function scheduleSync() {
|
||||
@ -1034,14 +1007,12 @@ async function startServer() {
|
||||
// as CheckMK. Template IDs are configured per lab template.
|
||||
// -------------------------------------------------------------
|
||||
async function triggerSemaphoreTask(templateId: number, extraVars: Record<string, string>): Promise<number | null> {
|
||||
const now = new Date().toISOString();
|
||||
const apiUrl = getSetting('semaphore_api_url');
|
||||
const token = getSetting('semaphore_api_token');
|
||||
const projectId = getSetting('semaphore_project_id');
|
||||
|
||||
if (!apiUrl || !token || !projectId) {
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'system', 'Semaphore trigger skipped — API URL, token, or project ID not configured. Check Settings.');
|
||||
addLog('system', 'Semaphore trigger skipped — API URL, token, or project ID not configured. Check Settings.');
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1064,14 +1035,12 @@ async function startServer() {
|
||||
}
|
||||
const data = await res.json() as { id?: number };
|
||||
const jobId = data?.id ?? null;
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'booking', `Semaphore: triggered template #${templateId} > job #${jobId} (booking ${extraVars.booking_id}).`);
|
||||
addLog('booking', `Semaphore: triggered template #${templateId} > job #${jobId} (booking ${extraVars.booking_id}).`);
|
||||
return jobId;
|
||||
} catch (err: any) {
|
||||
const msg = `Semaphore trigger failed for template #${templateId} — ${err?.message ?? err}`;
|
||||
console.error('[Semaphore]', msg);
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)')
|
||||
.run(uid('log'), now, 'system', msg);
|
||||
addLog('system', msg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1187,7 +1156,7 @@ async function startServer() {
|
||||
// -------------------------------------------------------------
|
||||
app.get('/api/caddy/status', requireAuth, async (_req, res) => {
|
||||
try {
|
||||
const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019';
|
||||
const adminUrl = getSetting('caddy_admin_url') || 'http://127.0.0.1:2019';
|
||||
const r = await fetch(`${adminUrl}/config/`, { signal: AbortSignal.timeout(2000) });
|
||||
res.json({ available: r.ok });
|
||||
} catch {
|
||||
@ -1206,16 +1175,14 @@ async function startServer() {
|
||||
app.post('/api/caddy/routes', requireAuth, async (req, res) => {
|
||||
try {
|
||||
if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' });
|
||||
const { hostname, upstream, tls, compress, redirectPath } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirectPath?: string;
|
||||
const { hostname, upstream, tls, compress, redirect } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirect?: string;
|
||||
};
|
||||
if (!hostname || !upstream) return res.status(400).json({ error: 'hostname and upstream are required.' });
|
||||
if (getCaddyRoutes().some(r => r.hostname === hostname.trim()))
|
||||
return res.status(409).json({ error: `Route for ${hostname.trim()} already exists.` });
|
||||
const route = addCaddyRoute(hostname.trim(), upstream.trim(), tls !== false, compress !== false, (redirectPath ?? '').trim());
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(uid('log'), new Date().toISOString(), 'system',
|
||||
`Caddy route added: ${hostname.trim()} → ${upstream.trim()}`, null, req.user!.userId);
|
||||
const route = addCaddyRoute({ hostname: hostname.trim(), upstream: upstream.trim(), tls: tls !== false, compress: compress !== false, redirect: (redirect ?? '').trim() });
|
||||
addLog('system', `Caddy route added: ${hostname.trim()} → ${upstream.trim()}`, { userId: req.user!.userId });
|
||||
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route add:', err.message));
|
||||
res.json(route);
|
||||
} catch (err: any) {
|
||||
@ -1228,14 +1195,12 @@ async function startServer() {
|
||||
if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' });
|
||||
const id = Number(req.params.id);
|
||||
if (!id) return res.status(400).json({ error: 'Invalid route id.' });
|
||||
const { hostname, upstream, tls, compress, redirectPath } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirectPath?: string;
|
||||
const { hostname, upstream, tls, compress, redirect } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirect?: string;
|
||||
};
|
||||
if (!hostname || !upstream) return res.status(400).json({ error: 'hostname and upstream are required.' });
|
||||
const route = updateCaddyRoute(id, hostname.trim(), upstream.trim(), tls !== false, compress !== false, (redirectPath ?? '').trim());
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(uid('log'), new Date().toISOString(), 'system',
|
||||
`Caddy route updated: ${hostname.trim()} → ${upstream.trim()}`, null, req.user!.userId);
|
||||
const route = updateCaddyRoute(id, { hostname: hostname.trim(), upstream: upstream.trim(), tls: tls !== false, compress: compress !== false, redirect: (redirect ?? '').trim() });
|
||||
addLog('system', `Caddy route updated: ${hostname.trim()} → ${upstream.trim()}`, { userId: req.user!.userId });
|
||||
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route update:', err.message));
|
||||
res.json(route);
|
||||
} catch (err: any) {
|
||||
@ -1251,9 +1216,7 @@ async function startServer() {
|
||||
const existing = getCaddyRouteById(id);
|
||||
deleteCaddyRoute(id);
|
||||
if (existing) {
|
||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(uid('log'), new Date().toISOString(), 'system',
|
||||
`Caddy route deleted: ${existing.hostname} → ${existing.upstream}`, null, req.user!.userId);
|
||||
addLog('system', `Caddy route deleted: ${existing.hostname} → ${existing.upstream}`, { userId: req.user!.userId });
|
||||
}
|
||||
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route delete:', err.message));
|
||||
res.status(204).send();
|
||||
|
||||
@ -35,7 +35,7 @@ interface CaddyRoute {
|
||||
upstream: string;
|
||||
tls: number;
|
||||
compress: number;
|
||||
redirect_path: string;
|
||||
redirect: string;
|
||||
}
|
||||
|
||||
interface DbInfo {
|
||||
@ -384,7 +384,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
try {
|
||||
const res = await authFetch('/api/caddy/routes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ hostname: newHostname.trim(), upstream: newUpstream.trim(), tls: newTls, compress: newCompress, redirectPath: newRedirect.trim() }),
|
||||
body: JSON.stringify({ hostname: newHostname.trim(), upstream: newUpstream.trim(), tls: newTls, compress: newCompress, redirect: newRedirect.trim() }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json();
|
||||
@ -424,7 +424,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
setEditUpstream(r.upstream);
|
||||
setEditTls(r.tls === 1);
|
||||
setEditCompress(r.compress === 1);
|
||||
setEditRedirect(r.redirect_path || '');
|
||||
setEditRedirect(r.redirect || '');
|
||||
}
|
||||
|
||||
function handleEditCancel() {
|
||||
@ -437,7 +437,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
try {
|
||||
const res = await authFetch(`/api/caddy/routes/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ hostname: editHostname.trim(), upstream: editUpstream.trim(), tls: editTls, compress: editCompress, redirectPath: editRedirect.trim() }),
|
||||
body: JSON.stringify({ hostname: editHostname.trim(), upstream: editUpstream.trim(), tls: editTls, compress: editCompress, redirect: editRedirect.trim() }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json();
|
||||
@ -1013,7 +1013,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
<span className="text-[11px] font-mono text-slate-400 truncate">{r.upstream}</span>
|
||||
{r.tls ? <span className="text-[9px] font-semibold text-sky-500 font-mono">TLS</span> : null}
|
||||
{r.compress ? <span className="text-[9px] font-semibold text-slate-500 font-mono">GZ</span> : null}
|
||||
{r.redirect_path ? <span className="text-[9px] font-mono text-slate-500 truncate">↳ {r.redirect_path}</span> : null}
|
||||
{r.redirect ? <span className="text-[9px] font-mono text-slate-500 truncate">↳ {r.redirect}</span> : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-3 shrink-0">
|
||||
<button type="button" onClick={() => handleEditStart(r)}
|
||||
|
||||
Reference in New Issue
Block a user