refactor(caddy): remove redundant GhostGrid domain fields, keep only custom routes

caddy_prod_domain and caddy_dev_domain are already handled by the Proxmox deploy
process. The Caddy integration is a generic TLS proxy for additional services
(Semaphore, Netbox, etc.) — the custom routes list is the sole mechanism.
This commit is contained in:
Brückner
2026-06-08 08:45:24 +02:00
parent 7afb4829bc
commit f1200425af
4 changed files with 1214 additions and 94 deletions

View File

@ -5,7 +5,7 @@ import { createServer as createViteServer } from 'vite';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { ConfidentialClientApplication } from '@azure/msal-node';
import db, { getSetting, setSetting, getAllSettings } from './server-db';
import db, { getSetting, setSetting, getAllSettings, getCaddyRoutes, addCaddyRoute, deleteCaddyRoute } 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)}`;
@ -71,6 +71,37 @@ function maskSettings(raw: Record<string, string>): Record<string, string> {
return out;
}
function buildCaddyfile(): string {
const customRoutes = getCaddyRoutes();
const lines: string[] = ['{\n local_certs\n}', ''];
for (const route of customRoutes) {
lines.push(`${route.hostname} {`);
if (route.compress) lines.push(' encode zstd gzip');
if (route.tls) lines.push(' tls internal');
lines.push(` reverse_proxy ${route.upstream}`);
lines.push('}', '');
}
return lines.join('\n');
}
async function pushCaddyConfig(): Promise<void> {
if (getSetting('caddy_enabled') !== 'true') return;
const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019';
const body = buildCaddyfile();
const res = await fetch(`${adminUrl}/load`, {
method: 'POST',
headers: { 'Content-Type': 'text/caddyfile' },
body,
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`Caddy /load returned ${res.status}: ${text}`);
}
}
async function startServer() {
const app = express();
const PORT = Number(process.env.PORT) || 3000;
@ -249,13 +280,15 @@ async function startServer() {
const allowed = ['azure_enabled', 'azure_client_id', 'azure_tenant_id', 'azure_client_secret',
'azure_redirect_uri', 'azure_allowed_group', 'checkmk_enabled', 'checkmk_api_url', 'checkmk_api_user',
'checkmk_api_secret', 'checkmk_sync_interval_ms',
'semaphore_enabled', 'semaphore_api_url', 'semaphore_api_token', 'semaphore_project_id'];
'semaphore_enabled', 'semaphore_api_url', 'semaphore_api_token', 'semaphore_project_id',
'caddy_enabled', 'caddy_admin_url'];
const updates = req.body as Record<string, string>;
for (const key of allowed) {
if (key in updates && updates[key] !== '__SET__') {
setSetting(key, String(updates[key]));
}
}
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after settings save:', err.message));
res.json(maskSettings(getAllSettings()));
} catch (err: any) {
res.status(500).json({ error: err.message });
@ -722,7 +755,7 @@ async function startServer() {
// in Settings take effect on the next cycle without a server restart.
// -------------------------------------------------------------
function checkmkHttpHint(status: number): string {
if (status === 401) return 'HTTP 401 Unauthorized - wrong automation user or secret (check Settings CheckMK)';
if (status === 401) return 'HTTP 401 Unauthorized - wrong automation user or secret (check Settings > CheckMK)';
if (status === 403) return 'HTTP 403 Forbidden - automation user lacks permission in CheckMK';
if (status === 404) return 'HTTP 404 Not Found - API URL incorrect or site name wrong';
return `HTTP ${status}`;
@ -746,7 +779,7 @@ async function startServer() {
const authHeader = `Bearer ${CHECKMK_API_USER} ${CHECKMK_API_SECRET}`;
const headers = { Authorization: authHeader, Accept: 'application/json' };
// Step 1: build IP hostname map from host configuration
// Step 1: build IP > hostname map from host configuration
// Checks both attributes (explicitly set) and effective_attributes (inherited).
let ipToHostname: Map<string, string>;
try {
@ -874,7 +907,7 @@ 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}).`);
.run(uid('log'), now, '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}`;
@ -991,6 +1024,55 @@ async function startServer() {
}
});
// -------------------------------------------------------------
// CADDY API
// -------------------------------------------------------------
app.get('/api/caddy/status', requireAuth, async (_req, res) => {
try {
const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019';
const r = await fetch(`${adminUrl}/config/`, { signal: AbortSignal.timeout(2000) });
res.json({ available: r.ok });
} catch {
res.json({ available: false });
}
});
app.get('/api/caddy/routes', requireAuth, (_req, res) => {
try {
res.json({ system: [], custom: getCaddyRoutes() });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/caddy/routes', requireAuth, async (req, res) => {
try {
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 = addCaddyRoute(hostname.trim(), upstream.trim(), tls !== false, compress !== false);
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route add:', 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.' });
deleteCaddyRoute(id);
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route delete:', err.message));
res.status(204).send();
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config on startup:', err.message));
app.listen(PORT, '0.0.0.0', () => {
console.log(`[Server] Core Server running at http://0.0.0.0:${PORT}`);
});