mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Add prefix to ui and resource
This commit is contained in:
@@ -20,11 +20,15 @@ resources:
|
|||||||
- X-Another-Header: another-value
|
- X-Another-Header: another-value
|
||||||
targets:
|
targets:
|
||||||
- site: lively-yosemite-toad
|
- site: lively-yosemite-toad
|
||||||
|
path: /path
|
||||||
|
pathMatchType: prefix
|
||||||
hostname: localhost
|
hostname: localhost
|
||||||
method: http
|
method: http
|
||||||
port: 8000
|
port: 8000
|
||||||
- site: slim-alpine-chipmunk
|
- site: slim-alpine-chipmunk
|
||||||
hostname: localhost
|
hostname: localhost
|
||||||
|
path: /yoman
|
||||||
|
pathMatchType: exact
|
||||||
method: http
|
method: http
|
||||||
port: 8001
|
port: 8001
|
||||||
resource-nice-id2:
|
resource-nice-id2:
|
||||||
|
|||||||
@@ -98,7 +98,9 @@ export async function updateResources(
|
|||||||
method: targetData.method,
|
method: targetData.method,
|
||||||
port: targetData.port,
|
port: targetData.port,
|
||||||
enabled: targetData.enabled,
|
enabled: targetData.enabled,
|
||||||
internalPort: internalPortToCreate
|
internalPort: internalPortToCreate,
|
||||||
|
path: targetData.path,
|
||||||
|
pathMatchType: targetData.pathMatchType
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -121,6 +123,7 @@ export async function updateResources(
|
|||||||
const protocol =
|
const protocol =
|
||||||
resourceData.protocol == "http" ? "tcp" : resourceData.protocol;
|
resourceData.protocol == "http" ? "tcp" : resourceData.protocol;
|
||||||
const resourceEnabled = resourceData.enabled == undefined || resourceData.enabled == null ? true : resourceData.enabled;
|
const resourceEnabled = resourceData.enabled == undefined || resourceData.enabled == null ? true : resourceData.enabled;
|
||||||
|
const resourceSsl = resourceData.ssl == undefined || resourceData.ssl == null ? true : resourceData.ssl;
|
||||||
let headers = "";
|
let headers = "";
|
||||||
for (const headerObj of resourceData.headers || []) {
|
for (const headerObj of resourceData.headers || []) {
|
||||||
for (const [key, value] of Object.entries(headerObj)) {
|
for (const [key, value] of Object.entries(headerObj)) {
|
||||||
@@ -165,7 +168,7 @@ export async function updateResources(
|
|||||||
domainId: domain ? domain.domainId : null,
|
domainId: domain ? domain.domainId : null,
|
||||||
enabled: resourceEnabled,
|
enabled: resourceEnabled,
|
||||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||||
ssl: resourceData.ssl ? true : false,
|
ssl: resourceSsl,
|
||||||
setHostHeader: resourceData["host-header"] || null,
|
setHostHeader: resourceData["host-header"] || null,
|
||||||
tlsServerName: resourceData["tls-server-name"] || null,
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
emailWhitelistEnabled: resourceData.auth?.[
|
emailWhitelistEnabled: resourceData.auth?.[
|
||||||
@@ -311,7 +314,9 @@ export async function updateResources(
|
|||||||
ip: targetData.hostname,
|
ip: targetData.hostname,
|
||||||
method: http ? targetData.method : null,
|
method: http ? targetData.method : null,
|
||||||
port: targetData.port,
|
port: targetData.port,
|
||||||
enabled: targetData.enabled
|
enabled: targetData.enabled,
|
||||||
|
path: targetData.path,
|
||||||
|
pathMatchType: targetData.pathMatchType
|
||||||
})
|
})
|
||||||
.where(eq(targets.targetId, existingTarget.targetId))
|
.where(eq(targets.targetId, existingTarget.targetId))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -395,7 +400,7 @@ export async function updateResources(
|
|||||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||||
setHostHeader: resourceData["host-header"] || null,
|
setHostHeader: resourceData["host-header"] || null,
|
||||||
tlsServerName: resourceData["tls-server-name"] || null,
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
ssl: resourceData.ssl ? true : false,
|
ssl: resourceSsl,
|
||||||
headers: headers || null
|
headers: headers || null
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const TargetSchema = z.object({
|
|||||||
port: z.number().int().min(1).max(65535),
|
port: z.number().int().min(1).max(65535),
|
||||||
enabled: z.boolean().optional().default(true),
|
enabled: z.boolean().optional().default(true),
|
||||||
"internal-port": z.number().int().min(1).max(65535).optional(),
|
"internal-port": z.number().int().min(1).max(65535).optional(),
|
||||||
|
path: z.string().optional(),
|
||||||
|
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
});
|
});
|
||||||
export type TargetData = z.infer<typeof TargetSchema>;
|
export type TargetData = z.infer<typeof TargetSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ const createTargetSchema = z
|
|||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.string().optional().nullable(),
|
method: z.string().optional().nullable(),
|
||||||
port: z.number().int().min(1).max(65535),
|
port: z.number().int().min(1).max(65535),
|
||||||
enabled: z.boolean().default(true)
|
enabled: z.boolean().default(true),
|
||||||
|
path: z.string().optional().nullable(),
|
||||||
|
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ function queryTargets(resourceId: number) {
|
|||||||
enabled: targets.enabled,
|
enabled: targets.enabled,
|
||||||
resourceId: targets.resourceId,
|
resourceId: targets.resourceId,
|
||||||
siteId: targets.siteId,
|
siteId: targets.siteId,
|
||||||
siteType: sites.type
|
siteType: sites.type,
|
||||||
|
path: targets.path,
|
||||||
|
pathMatchType: targets.pathMatchType
|
||||||
})
|
})
|
||||||
.from(targets)
|
.from(targets)
|
||||||
.leftJoin(sites, eq(sites.siteId, targets.siteId))
|
.leftJoin(sites, eq(sites.siteId, targets.siteId))
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ const updateTargetBodySchema = z
|
|||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.string().min(1).max(10).optional().nullable(),
|
method: z.string().min(1).max(10).optional().nullable(),
|
||||||
port: z.number().int().min(1).max(65535).optional(),
|
port: z.number().int().min(1).max(65535).optional(),
|
||||||
enabled: z.boolean().optional()
|
enabled: z.boolean().optional(),
|
||||||
|
path: z.string().optional().nullable(),
|
||||||
|
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
|||||||
@@ -101,8 +101,42 @@ const addTargetSchema = z.object({
|
|||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.string().nullable(),
|
method: z.string().nullable(),
|
||||||
port: z.coerce.number().int().positive(),
|
port: z.coerce.number().int().positive(),
|
||||||
siteId: z.number().int().positive()
|
siteId: z.number().int().positive(),
|
||||||
});
|
path: z.string().optional().nullable(),
|
||||||
|
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
|
}).refine(
|
||||||
|
(data) => {
|
||||||
|
// If path is provided, pathMatchType must be provided
|
||||||
|
if (data.path && !data.pathMatchType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If pathMatchType is provided, path must be provided
|
||||||
|
if (data.pathMatchType && !data.path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate path based on pathMatchType
|
||||||
|
if (data.path && data.pathMatchType) {
|
||||||
|
switch (data.pathMatchType) {
|
||||||
|
case "exact":
|
||||||
|
case "prefix":
|
||||||
|
// Path should start with /
|
||||||
|
return data.path.startsWith("/");
|
||||||
|
case "regex":
|
||||||
|
// Validate regex
|
||||||
|
try {
|
||||||
|
new RegExp(data.path);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid path configuration"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const targetsSettingsSchema = z.object({
|
const targetsSettingsSchema = z.object({
|
||||||
stickySession: z.boolean()
|
stickySession: z.boolean()
|
||||||
@@ -221,7 +255,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: "",
|
ip: "",
|
||||||
method: resource.http ? "http" : null,
|
method: resource.http ? "http" : null,
|
||||||
port: "" as any as number
|
port: "" as any as number,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
} as z.infer<typeof addTargetSchema>
|
} as z.infer<typeof addTargetSchema>
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -396,6 +432,8 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
...data,
|
...data,
|
||||||
|
path: data.path || null,
|
||||||
|
pathMatchType: data.pathMatchType || null,
|
||||||
siteType: site?.type || null,
|
siteType: site?.type || null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
targetId: new Date().getTime(),
|
targetId: new Date().getTime(),
|
||||||
@@ -407,7 +445,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
addTargetForm.reset({
|
addTargetForm.reset({
|
||||||
ip: "",
|
ip: "",
|
||||||
method: resource.http ? "http" : null,
|
method: resource.http ? "http" : null,
|
||||||
port: "" as any as number
|
port: "" as any as number,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +490,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
port: target.port,
|
port: target.port,
|
||||||
method: target.method,
|
method: target.method,
|
||||||
enabled: target.enabled,
|
enabled: target.enabled,
|
||||||
siteId: target.siteId
|
siteId: target.siteId,
|
||||||
|
path: target.path,
|
||||||
|
pathMatchType: target.pathMatchType
|
||||||
};
|
};
|
||||||
|
|
||||||
if (target.new) {
|
if (target.new) {
|
||||||
@@ -720,6 +762,87 @@ export default function ReverseProxyTargets(props: {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "path",
|
||||||
|
header: t("path"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const [showPathInput, setShowPathInput] = useState(
|
||||||
|
!!(row.original.path || row.original.pathMatchType)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showPathInput) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowPathInput(true)}
|
||||||
|
>
|
||||||
|
+ Path
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 min-w-[200px]">
|
||||||
|
<Select
|
||||||
|
defaultValue={row.original.pathMatchType || "exact"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
pathMatchType: value as "exact" | "prefix" | "regex"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-25">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="exact">Exact</SelectItem>
|
||||||
|
<SelectItem value="prefix">Prefix</SelectItem>
|
||||||
|
<SelectItem value="regex">Regex</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
placeholder={
|
||||||
|
row.original.pathMatchType === "regex"
|
||||||
|
? "^/api/.*"
|
||||||
|
: "/path"
|
||||||
|
}
|
||||||
|
defaultValue={row.original.path || ""}
|
||||||
|
className="flex-1 min-w-[150px]"
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = e.target.value.trim();
|
||||||
|
if (!value) {
|
||||||
|
setShowPathInput(false);
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
path: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowPathInput(false);
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// accessorKey: "protocol",
|
// accessorKey: "protocol",
|
||||||
// header: t('targetProtocol'),
|
// header: t('targetProtocol'),
|
||||||
|
|||||||
@@ -112,8 +112,42 @@ const addTargetSchema = z.object({
|
|||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.string().nullable(),
|
method: z.string().nullable(),
|
||||||
port: z.coerce.number().int().positive(),
|
port: z.coerce.number().int().positive(),
|
||||||
siteId: z.number().int().positive()
|
siteId: z.number().int().positive(),
|
||||||
});
|
path: z.string().optional().nullable(),
|
||||||
|
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
|
}).refine(
|
||||||
|
(data) => {
|
||||||
|
// If path is provided, pathMatchType must be provided
|
||||||
|
if (data.path && !data.pathMatchType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If pathMatchType is provided, path must be provided
|
||||||
|
if (data.pathMatchType && !data.path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate path based on pathMatchType
|
||||||
|
if (data.path && data.pathMatchType) {
|
||||||
|
switch (data.pathMatchType) {
|
||||||
|
case "exact":
|
||||||
|
case "prefix":
|
||||||
|
// Path should start with /
|
||||||
|
return data.path.startsWith("/");
|
||||||
|
case "regex":
|
||||||
|
// Validate regex
|
||||||
|
try {
|
||||||
|
new RegExp(data.path);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid path configuration"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
|
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
|
||||||
type HttpResourceFormValues = z.infer<typeof httpResourceFormSchema>;
|
type HttpResourceFormValues = z.infer<typeof httpResourceFormSchema>;
|
||||||
@@ -202,7 +236,9 @@ export default function Page() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: "",
|
ip: "",
|
||||||
method: baseForm.watch("http") ? "http" : null,
|
method: baseForm.watch("http") ? "http" : null,
|
||||||
port: "" as any as number
|
port: "" as any as number,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
} as z.infer<typeof addTargetSchema>
|
} as z.infer<typeof addTargetSchema>
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -273,6 +309,8 @@ export default function Page() {
|
|||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
...data,
|
...data,
|
||||||
|
path: data.path || null,
|
||||||
|
pathMatchType: data.pathMatchType || null,
|
||||||
siteType: site?.type || null,
|
siteType: site?.type || null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
targetId: new Date().getTime(),
|
targetId: new Date().getTime(),
|
||||||
@@ -284,7 +322,9 @@ export default function Page() {
|
|||||||
addTargetForm.reset({
|
addTargetForm.reset({
|
||||||
ip: "",
|
ip: "",
|
||||||
method: baseForm.watch("http") ? "http" : null,
|
method: baseForm.watch("http") ? "http" : null,
|
||||||
port: "" as any as number
|
port: "" as any as number,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +410,9 @@ export default function Page() {
|
|||||||
port: target.port,
|
port: target.port,
|
||||||
method: target.method,
|
method: target.method,
|
||||||
enabled: target.enabled,
|
enabled: target.enabled,
|
||||||
siteId: target.siteId
|
siteId: target.siteId,
|
||||||
|
path: target.path,
|
||||||
|
pathMatchType: target.pathMatchType
|
||||||
};
|
};
|
||||||
|
|
||||||
await api.put(`/resource/${id}/target`, data);
|
await api.put(`/resource/${id}/target`, data);
|
||||||
@@ -666,6 +708,87 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "path",
|
||||||
|
header: t("path"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const [showPathInput, setShowPathInput] = useState(
|
||||||
|
!!(row.original.path || row.original.pathMatchType)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showPathInput) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowPathInput(true)}
|
||||||
|
>
|
||||||
|
+ Path
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 min-w-[200px]">
|
||||||
|
<Select
|
||||||
|
defaultValue={row.original.pathMatchType || "exact"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
pathMatchType: value as "exact" | "prefix" | "regex"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-25">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="exact">Exact</SelectItem>
|
||||||
|
<SelectItem value="prefix">Prefix</SelectItem>
|
||||||
|
<SelectItem value="regex">Regex</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
placeholder={
|
||||||
|
row.original.pathMatchType === "regex"
|
||||||
|
? "^/api/.*"
|
||||||
|
: "/path"
|
||||||
|
}
|
||||||
|
defaultValue={row.original.path || ""}
|
||||||
|
className="flex-1 min-w-[150px]"
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = e.target.value.trim();
|
||||||
|
if (!value) {
|
||||||
|
setShowPathInput(false);
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
path: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowPathInput(false);
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
path: null,
|
||||||
|
pathMatchType: null
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "enabled",
|
accessorKey: "enabled",
|
||||||
header: t("enabled"),
|
header: t("enabled"),
|
||||||
|
|||||||
Reference in New Issue
Block a user