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:
Brückner
2026-06-10 10:22:39 +02:00
parent a2d515992c
commit 49cd0ae4f6
4 changed files with 54 additions and 28 deletions

View File

@ -35,6 +35,7 @@ interface CaddyRoute {
upstream: string;
tls: number;
compress: number;
redirect_path: string;
}
interface DbInfo {
@ -200,12 +201,14 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
const [newUpstream, setNewUpstream] = useState('');
const [newTls, setNewTls] = useState(true);
const [newCompress, setNewCompress] = useState(true);
const [newRedirect, setNewRedirect] = useState('');
const [editingRouteId, setEditingRouteId] = useState<number | null>(null);
const [editHostname, setEditHostname] = useState('');
const [editUpstream, setEditUpstream] = useState('');
const [editTls, setEditTls] = useState(true);
const [editCompress, setEditCompress] = useState(true);
const [editRedirect, setEditRedirect] = useState('');
const [savingRoute, setSavingRoute] = useState(false);
const [dbInfo, setDbInfo] = useState<DbInfo | null>(null);
@ -381,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 }),
body: JSON.stringify({ hostname: newHostname.trim(), upstream: newUpstream.trim(), tls: newTls, compress: newCompress, redirectPath: newRedirect.trim() }),
});
if (!res.ok) {
const d = await res.json();
@ -392,6 +395,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
setNewUpstream('');
setNewTls(true);
setNewCompress(true);
setNewRedirect('');
await loadCaddyRoutes();
} catch {
setError('Network error adding route.');
@ -420,6 +424,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
setEditUpstream(r.upstream);
setEditTls(r.tls === 1);
setEditCompress(r.compress === 1);
setEditRedirect(r.redirect_path || '');
}
function handleEditCancel() {
@ -432,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 }),
body: JSON.stringify({ hostname: editHostname.trim(), upstream: editUpstream.trim(), tls: editTls, compress: editCompress, redirectPath: editRedirect.trim() }),
});
if (!res.ok) {
const d = await res.json();
@ -965,6 +970,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{caddyRoutes.map((r: CaddyRoute) => (
<div key={r.id} className="bg-slate-900/60 border border-slate-700/60 rounded-xl px-4 py-2.5">
{editingRouteId === r.id ? (
<div className="space-y-2">
<div className="flex items-end gap-2">
<div className="flex-1 min-w-0">
<Label>Hostname</Label>
@ -997,6 +1003,8 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<X className="w-3.5 h-3.5" />
</button>
</div>
<Input value={editRedirect} onChange={setEditRedirect} placeholder="Root redirect (optional), e.g. /monitoring/check_mk/" monospace />
</div>
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 min-w-0">
@ -1005,6 +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">&#8627; {r.redirect_path}</span> : null}
</div>
<div className="flex items-center gap-1 ml-3 shrink-0">
<button type="button" onClick={() => handleEditStart(r)}
@ -1022,7 +1031,8 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
))}
{/* Add route form */}
<div className="flex items-end gap-2 pt-1">
<div className="space-y-2 pt-1">
<div className="flex items-end gap-2">
<div className="flex-1 min-w-0">
<Label>Hostname</Label>
<Input value={newHostname} onChange={setNewHostname} placeholder="semaphore" monospace />
@ -1061,6 +1071,8 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{addingRoute ? 'Adding…' : 'Add'}
</button>
</div>
<Input value={newRedirect} onChange={setNewRedirect} placeholder="Root redirect (optional), e.g. /monitoring/check_mk/" monospace />
</div>
</div>
)}
</div>