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:
92
server.ts
92
server.ts
@ -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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user