feat: add structured redirects and path prefix rewrite for proxy hosts
Adds two new UI-configurable Caddy patterns that previously required raw JSON: - Per-path redirect rules (from/to/status) emitted as a subroute handler before auth so .well-known paths work without login; supports full URLs, cross-domain targets, and wildcard path patterns (e.g. /.well-known/*) - Path prefix rewrite that prepends a segment to every request before proxying (e.g. /recipes → upstream sees /recipes/original/path) Config is stored in the existing meta JSON column (no schema migration). Includes integration tests for meta serialization and E2E functional tests against a real Caddy instance covering relative/absolute destinations, all 3xx status codes, and various wildcard combinations. Adds traefik/whoami to the test stack to verify rewritten paths actually reach the upstream. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ import { UpstreamInput } from "./UpstreamInput";
|
||||
import { GeoBlockFields } from "./GeoBlockFields";
|
||||
import { WafFields } from "./WafFields";
|
||||
import { MtlsFields } from "./MtlsConfig";
|
||||
import { RedirectsFields } from "./RedirectsFields";
|
||||
import { RewriteFields } from "./RewriteFields";
|
||||
import type { CaCertificate } from "@/src/lib/models/ca-certificates";
|
||||
|
||||
export function CreateHostDialog({
|
||||
@@ -108,6 +110,8 @@ export function CreateHostDialog({
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<RedirectsFields initialData={initialData?.redirects} />
|
||||
<RewriteFields initialData={initialData?.rewrite} />
|
||||
<TextField
|
||||
name="custom_pre_handlers_json"
|
||||
label="Custom Pre-Handlers (JSON)"
|
||||
@@ -212,6 +216,8 @@ export function EditHostDialog({
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<RedirectsFields initialData={host.redirects} />
|
||||
<RewriteFields initialData={host.rewrite} />
|
||||
<TextField
|
||||
name="custom_pre_handlers_json"
|
||||
label="Custom Pre-Handlers (JSON)"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box, Button, IconButton, MenuItem, Select, Stack,
|
||||
Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography,
|
||||
} from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import type { RedirectRule } from "@/src/lib/models/proxy-hosts";
|
||||
|
||||
type Props = { initialData?: RedirectRule[] };
|
||||
|
||||
export function RedirectsFields({ initialData = [] }: Props) {
|
||||
const [rules, setRules] = useState<RedirectRule[]>(initialData);
|
||||
|
||||
const addRule = () =>
|
||||
setRules((r) => [...r, { from: "", to: "", status: 301 }]);
|
||||
|
||||
const removeRule = (i: number) =>
|
||||
setRules((r) => r.filter((_, idx) => idx !== i));
|
||||
|
||||
const updateRule = (i: number, key: keyof RedirectRule, value: string | number) =>
|
||||
setRules((r) => r.map((rule, idx) => (idx === i ? { ...rule, [key]: value } : rule)));
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Redirects
|
||||
</Typography>
|
||||
<input type="hidden" name="redirects_json" value={JSON.stringify(rules)} />
|
||||
{rules.length > 0 && (
|
||||
<Table size="small" sx={{ mb: 1 }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>From Path</TableCell>
|
||||
<TableCell>To URL / Path</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rules.map((rule, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="/.well-known/carddav"
|
||||
value={rule.from}
|
||||
onChange={(e) => updateRule(i, "from", e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="/remote.php/dav/"
|
||||
value={rule.to}
|
||||
onChange={(e) => updateRule(i, "to", e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: 90 }}>
|
||||
<Select
|
||||
size="small"
|
||||
value={rule.status}
|
||||
onChange={(e) => updateRule(i, "status", Number(e.target.value))}
|
||||
>
|
||||
{[301, 302, 307, 308].map((s) => (
|
||||
<MenuItem key={s} value={s}>{s}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: 40 }}>
|
||||
<IconButton size="small" onClick={() => removeRule(i)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<Button size="small" startIcon={<AddIcon />} onClick={addRule}>
|
||||
Add Redirect
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TextField } from "@mui/material";
|
||||
import type { RewriteConfig } from "@/src/lib/models/proxy-hosts";
|
||||
|
||||
type Props = { initialData?: RewriteConfig | null };
|
||||
|
||||
export function RewriteFields({ initialData }: Props) {
|
||||
return (
|
||||
<TextField
|
||||
name="rewrite_path_prefix"
|
||||
label="Path Prefix Rewrite"
|
||||
placeholder="/recipes"
|
||||
defaultValue={initialData?.path_prefix ?? ""}
|
||||
helperText="Prepend this prefix to every request before proxying (e.g. /recipes → /recipes/original/path)"
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user