feat(caddy): optional root redirect per route
Add a redirect_path column to the caddy table and an optional 'root redirect' field in the route form. When set, buildCaddyfile emits 'redir / <path>' so the bare host (e.g. checkmk.domain.local/) redirects to a sub-path (e.g. /monitoring/check_mk/) while every other path still passes through to the backend — the safe pattern for apps like CheckMK that bake their site path into absolute URLs. Defensive ALTER TABLE keeps existing databases working.
This commit is contained in:
18
server.ts
18
server.ts
@ -87,6 +87,12 @@ 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) {
|
||||
// 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}`;
|
||||
lines.push(` redir / ${target}`);
|
||||
}
|
||||
lines.push(` reverse_proxy ${route.upstream} {`);
|
||||
// Standard forwarding headers for every backend. Caddy already sets the
|
||||
// X-Forwarded-* family and the Host header by default; these make them
|
||||
@ -1200,13 +1206,13 @@ async function startServer() {
|
||||
app.post('/api/caddy/routes', requireAuth, async (req, res) => {
|
||||
try {
|
||||
if (!IS_CADDY_MANAGER) return res.status(403).json({ error: 'Caddy is managed by another instance.' });
|
||||
const { hostname, upstream, tls, compress } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean;
|
||||
const { hostname, upstream, tls, compress, redirectPath } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirectPath?: 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);
|
||||
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);
|
||||
@ -1222,11 +1228,11 @@ async function startServer() {
|
||||
if (!IS_CADDY_MANAGER) 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 } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean;
|
||||
const { hostname, upstream, tls, compress, redirectPath } = req.body as {
|
||||
hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirectPath?: 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);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user