basic setup for rewriting requests to another path

This commit is contained in:
Pallavi Kumari
2025-09-28 11:25:11 +05:30
parent 70914e836f
commit 149a4b916b
8 changed files with 557 additions and 145 deletions

View File

@@ -105,7 +105,9 @@ const addTargetSchema = z.object({
port: z.coerce.number().int().positive(),
siteId: z.number().int().positive(),
path: z.string().optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
rewritePath: z.string().optional().nullable(),
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable()
}).refine(
(data) => {
// If path is provided, pathMatchType must be provided
@@ -138,7 +140,23 @@ const addTargetSchema = z.object({
{
message: "Invalid path configuration"
}
);
)
.refine(
(data) => {
// If rewritePath is provided, rewritePathType must be provided
if (data.rewritePath && !data.rewritePathType) {
return false;
}
// If rewritePathType is provided, rewritePath must be provided
if (data.rewritePathType && !data.rewritePath) {
return false;
}
return true;
},
{
message: "Invalid rewrite path configuration"
}
);
const targetsSettingsSchema = z.object({
stickySession: z.boolean()
@@ -259,8 +277,10 @@ export default function ReverseProxyTargets(props: {
method: resource.http ? "http" : null,
port: "" as any as number,
path: null,
pathMatchType: null
}
pathMatchType: null,
rewritePath: null,
rewritePathType: null,
} as z.infer<typeof addTargetSchema>
});
const watchedIp = addTargetForm.watch("ip");
@@ -436,6 +456,8 @@ export default function ReverseProxyTargets(props: {
...data,
path: data.path || null,
pathMatchType: data.pathMatchType || null,
rewritePath: data.rewritePath || null,
rewritePathType: data.rewritePathType || null,
siteType: site?.type || null,
enabled: true,
targetId: new Date().getTime(),
@@ -449,7 +471,9 @@ export default function ReverseProxyTargets(props: {
method: resource.http ? "http" : null,
port: "" as any as number,
path: null,
pathMatchType: null
pathMatchType: null,
rewritePath: null,
rewritePathType: null,
});
}
@@ -469,11 +493,11 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site?.type || null
}
...target,
...data,
updated: true,
siteType: site?.type || null
}
: target
)
);
@@ -494,7 +518,9 @@ export default function ReverseProxyTargets(props: {
enabled: target.enabled,
siteId: target.siteId,
path: target.path,
pathMatchType: target.pathMatchType
pathMatchType: target.pathMatchType,
rewritePath: target.rewritePath,
rewritePathType: target.rewritePathType
};
if (target.new) {
@@ -590,7 +616,7 @@ export default function ReverseProxyTargets(props: {
}
}}
>
+ {t("matchPath")}
+ {t("matchPath")}
</Button>
);
}
@@ -693,7 +719,7 @@ export default function ReverseProxyTargets(props: {
className={cn(
"justify-between flex-1",
!row.original.siteId &&
"text-muted-foreground"
"text-muted-foreground"
)}
>
{row.original.siteId
@@ -772,31 +798,31 @@ export default function ReverseProxyTargets(props: {
},
...(resource.http
? [
{
accessorKey: "method",
header: t("method"),
cell: ({ row }: { row: Row<LocalTarget> }) => (
<Select
defaultValue={row.original.method ?? ""}
onValueChange={(value) =>
updateTarget(row.original.targetId, {
...row.original,
method: value
})
}
>
<SelectTrigger>
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
)
}
]
{
accessorKey: "method",
header: t("method"),
cell: ({ row }: { row: Row<LocalTarget> }) => (
<Select
defaultValue={row.original.method ?? ""}
onValueChange={(value) =>
updateTarget(row.original.targetId, {
...row.original,
method: value
})
}
>
<SelectTrigger>
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
)
}
]
: []),
{
accessorKey: "ip",
@@ -856,6 +882,102 @@ export default function ReverseProxyTargets(props: {
/>
)
},
{
accessorKey: "rewritePath",
header: t("rewritePath"),
cell: ({ row }) => {
const [showRewritePathInput, setShowRewritePathInput] = useState(
!!(row.original.rewritePath || row.original.rewritePathType)
);
if (!showRewritePathInput) {
const noPathMatch =
!row.original.path && !row.original.pathMatchType;
return (
<Button
variant="outline"
disabled={noPathMatch}
onClick={() => {
setShowRewritePathInput(true);
// Set default rewritePathType when first showing path input
if (!row.original.rewritePathType) {
updateTarget(row.original.targetId, {
...row.original,
rewritePathType: "prefix"
});
}
}}
>
+ {t("rewritePath")}
</Button>
);
}
return (
<div className="flex gap-2 min-w-[200px] items-center">
<Button
variant="outline"
onClick={() => {
setShowRewritePathInput(false);
updateTarget(row.original.targetId, {
...row.original,
rewritePath: null,
rewritePathType: null
});
}}
>
×
</Button>
<MoveRight className="ml-4 h-4 w-4" />
<Select
defaultValue={row.original.rewritePathType || "prefix"}
onValueChange={(value) =>
updateTarget(row.original.targetId, {
...row.original,
rewritePathType: value as "exact" | "prefix" | "regex"
})
}
>
<SelectTrigger className="w-25">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="prefix">Prefix</SelectItem>
<SelectItem value="exact">Exact</SelectItem>
<SelectItem value="regex">Regex</SelectItem>
<SelectItem value="stripPrefix">Strip Prefix</SelectItem>
</SelectContent>
</Select>
<Input
placeholder={
row.original.rewritePathType === "regex"
? "^/api/.*"
: "/path"
}
defaultValue={row.original.rewritePath || ""}
className="flex-1 min-w-[150px]"
onBlur={(e) => {
const value = e.target.value.trim();
if (!value) {
setShowRewritePathInput(false);
updateTarget(row.original.targetId, {
...row.original,
rewritePath: null,
rewritePathType: null
});
} else {
updateTarget(row.original.targetId, {
...row.original,
rewritePath: value
});
}
}}
/>
</div>
);
}
},
// {
// accessorKey: "protocol",
// header: t('targetProtocol'),
@@ -968,21 +1090,21 @@ export default function ReverseProxyTargets(props: {
className={cn(
"justify-between flex-1",
!field.value &&
"text-muted-foreground"
"text-muted-foreground"
)}
>
{field.value
? sites.find(
(
site
) =>
site.siteId ===
field.value
)
?.name
(
site
) =>
site.siteId ===
field.value
)
?.name
: t(
"siteSelect"
)}
"siteSelect"
)}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
@@ -1048,34 +1170,34 @@ export default function ReverseProxyTargets(props: {
);
return selectedSite &&
selectedSite.type ===
"newt"
"newt"
? (() => {
const dockerState =
getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={
selectedSite
}
containers={
dockerState.containers
}
isAvailable={
dockerState.isAvailable
}
onContainerSelect={
handleContainerSelect
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()
const dockerState =
getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={
selectedSite
}
containers={
dockerState.containers
}
isAvailable={
dockerState.isAvailable
}
onContainerSelect={
handleContainerSelect
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()
: null;
})()}
</div>
@@ -1303,12 +1425,12 @@ export default function ReverseProxyTargets(props: {
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
)
)}