From d51e7f7e40f640e8fb78842e535fce1bb7975d10 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 11 Sep 2025 21:20:33 -0700 Subject: [PATCH] Add prefix to ui and resource --- blueprint.yaml | 4 + server/lib/blueprints/resources.ts | 13 +- server/lib/blueprints/types.ts | 2 + server/routers/target/createTarget.ts | 4 +- server/routers/target/listTargets.ts | 4 +- server/routers/target/updateTarget.ts | 4 +- .../resources/[niceId]/proxy/page.tsx | 133 +++++++++++++++++- .../settings/resources/create/page.tsx | 133 +++++++++++++++++- 8 files changed, 280 insertions(+), 17 deletions(-) diff --git a/blueprint.yaml b/blueprint.yaml index 8a0d208f..6854e55f 100644 --- a/blueprint.yaml +++ b/blueprint.yaml @@ -20,11 +20,15 @@ resources: - X-Another-Header: another-value targets: - site: lively-yosemite-toad + path: /path + pathMatchType: prefix hostname: localhost method: http port: 8000 - site: slim-alpine-chipmunk hostname: localhost + path: /yoman + pathMatchType: exact method: http port: 8001 resource-nice-id2: diff --git a/server/lib/blueprints/resources.ts b/server/lib/blueprints/resources.ts index 1ca928ef..bc7eda08 100644 --- a/server/lib/blueprints/resources.ts +++ b/server/lib/blueprints/resources.ts @@ -98,7 +98,9 @@ export async function updateResources( method: targetData.method, port: targetData.port, enabled: targetData.enabled, - internalPort: internalPortToCreate + internalPort: internalPortToCreate, + path: targetData.path, + pathMatchType: targetData.pathMatchType }) .returning(); @@ -121,6 +123,7 @@ export async function updateResources( const protocol = resourceData.protocol == "http" ? "tcp" : resourceData.protocol; const resourceEnabled = resourceData.enabled == undefined || resourceData.enabled == null ? true : resourceData.enabled; + const resourceSsl = resourceData.ssl == undefined || resourceData.ssl == null ? true : resourceData.ssl; let headers = ""; for (const headerObj of resourceData.headers || []) { for (const [key, value] of Object.entries(headerObj)) { @@ -165,7 +168,7 @@ export async function updateResources( domainId: domain ? domain.domainId : null, enabled: resourceEnabled, sso: resourceData.auth?.["sso-enabled"] || false, - ssl: resourceData.ssl ? true : false, + ssl: resourceSsl, setHostHeader: resourceData["host-header"] || null, tlsServerName: resourceData["tls-server-name"] || null, emailWhitelistEnabled: resourceData.auth?.[ @@ -311,7 +314,9 @@ export async function updateResources( ip: targetData.hostname, method: http ? targetData.method : null, port: targetData.port, - enabled: targetData.enabled + enabled: targetData.enabled, + path: targetData.path, + pathMatchType: targetData.pathMatchType }) .where(eq(targets.targetId, existingTarget.targetId)) .returning(); @@ -395,7 +400,7 @@ export async function updateResources( sso: resourceData.auth?.["sso-enabled"] || false, setHostHeader: resourceData["host-header"] || null, tlsServerName: resourceData["tls-server-name"] || null, - ssl: resourceData.ssl ? true : false, + ssl: resourceSsl, headers: headers || null }) .returning(); diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 7dd85e12..d668c894 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -13,6 +13,8 @@ export const TargetSchema = z.object({ port: z.number().int().min(1).max(65535), enabled: z.boolean().optional().default(true), "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; diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index f58c236e..fb85f566 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -30,7 +30,9 @@ const createTargetSchema = z ip: z.string().refine(isTargetValid), method: z.string().optional().nullable(), 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(); diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index eab8f1c8..ca1159d2 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -44,7 +44,9 @@ function queryTargets(resourceId: number) { enabled: targets.enabled, resourceId: targets.resourceId, siteId: targets.siteId, - siteType: sites.type + siteType: sites.type, + path: targets.path, + pathMatchType: targets.pathMatchType }) .from(targets) .leftJoin(sites, eq(sites.siteId, targets.siteId)) diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 47300619..928a1a55 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -26,7 +26,9 @@ const updateTargetBodySchema = z ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), 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() .refine((data) => Object.keys(data).length > 0, { diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 2c98b07a..9d5f5ce3 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -101,8 +101,42 @@ const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), 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({ stickySession: z.boolean() @@ -221,7 +255,9 @@ export default function ReverseProxyTargets(props: { defaultValues: { ip: "", method: resource.http ? "http" : null, - port: "" as any as number + port: "" as any as number, + path: null, + pathMatchType: null } as z.infer }); @@ -396,6 +432,8 @@ export default function ReverseProxyTargets(props: { const newTarget: LocalTarget = { ...data, + path: data.path || null, + pathMatchType: data.pathMatchType || null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), @@ -407,7 +445,9 @@ export default function ReverseProxyTargets(props: { addTargetForm.reset({ ip: "", 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, method: target.method, enabled: target.enabled, - siteId: target.siteId + siteId: target.siteId, + path: target.path, + pathMatchType: target.pathMatchType }; 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 ( + + ); + } + + return ( +
+ + { + 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 + }); + } + }} + /> + +
+ ); + } + }, // { // accessorKey: "protocol", // header: t('targetProtocol'), diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index c28f5a5f..5876cadf 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -112,8 +112,42 @@ const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), 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; type HttpResourceFormValues = z.infer; @@ -202,7 +236,9 @@ export default function Page() { defaultValues: { ip: "", method: baseForm.watch("http") ? "http" : null, - port: "" as any as number + port: "" as any as number, + path: null, + pathMatchType: null } as z.infer }); @@ -273,6 +309,8 @@ export default function Page() { const newTarget: LocalTarget = { ...data, + path: data.path || null, + pathMatchType: data.pathMatchType || null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), @@ -284,7 +322,9 @@ export default function Page() { addTargetForm.reset({ ip: "", 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, method: target.method, enabled: target.enabled, - siteId: target.siteId + siteId: target.siteId, + path: target.path, + pathMatchType: target.pathMatchType }; 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 ( + + ); + } + + return ( +
+ + { + 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 + }); + } + }} + /> + +
+ ); + } + }, { accessorKey: "enabled", header: t("enabled"),