feat(caddy): route edit, system log entries, fix routes load timing

Add inline edit for custom routes (Pencil icon → inline form with all fields).
Log route add/update/delete/import to the logs table (type: system) so
operations appear in the Logbook. Fix loadCaddyRoutes() called without await
after settings save, causing a race between the success message and route list.
This commit is contained in:
Brückner
2026-06-08 13:04:01 +02:00
parent 00cf5dd02d
commit f66b1ca456
3 changed files with 148 additions and 23 deletions

View File

@ -7,7 +7,7 @@ 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, deleteCaddyRoute, DB_FILE } from './server-db';
import db, { 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)}`;
@ -89,11 +89,12 @@ function buildCaddyfile(): string {
return lines.join('\n');
}
function importCaddyfileRoutes(): void {
function importCaddyfileRoutes(userId: string): void {
if (getCaddyRoutes().length > 0) return;
const caddyfilePath = '/etc/caddy/Caddyfile';
if (!fs.existsSync(caddyfilePath)) return;
const lines = fs.readFileSync(caddyfilePath, 'utf-8').split('\n');
const imported: string[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i].trim();
@ -113,11 +114,17 @@ function importCaddyfileRoutes(): void {
const tls = /tls\s+internal/.test(block);
const compress = /encode/.test(block);
addCaddyRoute(hostname, upstream, tls, compress);
console.log(`[Caddy] Imported route from Caddyfile: ${hostname}${upstream}`);
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);
}
}
async function pushCaddyConfig(): Promise<void> {
@ -143,7 +150,7 @@ async function startServer() {
if (cnt === 0) {
const passwordHash = bcrypt.hashSync('admin', 10);
db.prepare('INSERT INTO users (id, name, role, email, password_hash) VALUES (?, ?, ?, ?, ?)')
.run(uid('u'), 'Admin', 'Admin', 'admin@ghostgrid.local', passwordHash);
.run(uid('u'), 'admin', 'admin', 'admin@ghostgrid.local', passwordHash);
console.log('[Init] Default admin user created — login: admin@ghostgrid.local / admin');
}
@ -331,7 +338,7 @@ async function startServer() {
}
}
if (!caddyWasEnabled && updates.caddy_enabled === 'true') {
importCaddyfileRoutes();
importCaddyfileRoutes(req.user!.userId);
}
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after settings save:', err.message));
res.json(maskSettings(getAllSettings()));
@ -1182,6 +1189,9 @@ async function startServer() {
};
if (!hostname || !upstream) return res.status(400).json({ error: 'hostname and upstream are required.' });
const route = addCaddyRoute(hostname.trim(), upstream.trim(), tls !== false, compress !== false);
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);
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route add:', err.message));
res.json(route);
} catch (err: any) {
@ -1189,11 +1199,36 @@ async function startServer() {
}
});
app.put('/api/caddy/routes/:id', requireAuth, async (req, res) => {
try {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'Invalid route id.' });
const { hostname, upstream, tls, compress } = req.body as {
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean;
};
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);
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);
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route update:', err.message));
res.json(route);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.delete('/api/caddy/routes/:id', requireAuth, (req, res) => {
try {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'Invalid route id.' });
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);
}
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route delete:', err.message));
res.status(204).send();
} catch (err: any) {