diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index d17ad499..009bbaff 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -99,6 +99,7 @@ const updateRawResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), proxyPort: z.number().int().min(1).max(65535).optional(), + stickySession: z.boolean().optional(), enabled: z.boolean().optional() }) .strict() diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index e6488b9f..2fd656ba 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -41,7 +41,7 @@ export async function traefikConfigProvider( orgId: orgs.orgId }, enabled: resources.enabled, - stickySession: resources.stickySessionk, + stickySession: resources.stickySession, tlsServerName: resources.tlsServerName, setHostHeader: resources.setHostHeader }) @@ -288,7 +288,7 @@ export async function traefikConfigProvider( ? { sticky: { cookie: { - name: "pangolin_sticky", + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies secure: resource.ssl, httpOnly: true } diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 49f3d834..9819be59 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -234,7 +234,7 @@ export default function GeneralPage() { loading={loadingSave} disabled={loadingSave} > - Save Settings + Save General Settings diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index 2428472e..bfb4f08b 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -57,7 +57,8 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { const api = createApiClient(useEnvContext()); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedResource, setSelectedResource] = useState(); + const [selectedResource, setSelectedResource] = + useState(); const deleteResource = (resourceId: number) => { api.delete(`/resource/${resourceId}`) @@ -238,7 +239,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { Not Protected ) : ( - -- + - )} ); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index cf75a426..f1e152d5 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -109,42 +109,8 @@ const TransferFormSchema = z.object({ siteId: z.number() }); -const AdvancedFormSchema = z - .object({ - http: z.boolean(), - tlsServerName: z.string().optional(), - setHostHeader: z.string().optional() - }) - .refine( - (data) => { - if (data.tlsServerName) { - return tlsNameSchema.safeParse(data.tlsServerName).success; - } - return true; - }, - { - message: - "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", - path: ["tlsServerName"] - } - ) - .refine( - (data) => { - if (data.setHostHeader) { - return tlsNameSchema.safeParse(data.setHostHeader).success; - } - return true; - }, - { - message: - "Invalid custom Host Header value. Use domain name format, or save empty to unset the custom Host Header", - path: ["tlsServerName"] - } - ); - type GeneralFormValues = z.infer; type TransferFormValues = z.infer; -type AdvancedFormValues = z.infer; export default function GeneralForm() { const [formKey, setFormKey] = useState(0); @@ -185,20 +151,6 @@ export default function GeneralForm() { mode: "onChange" }); - const advancedForm = useForm({ - resolver: zodResolver(AdvancedFormSchema), - defaultValues: { - http: resource.http, - tlsServerName: resource.http - ? resource.tlsServerName || "" - : undefined, - setHostHeader: resource.http - ? resource.setHostHeader || "" - : undefined - }, - mode: "onChange" - }); - const transferForm = useForm({ resolver: zodResolver(TransferFormSchema), defaultValues: { @@ -327,46 +279,6 @@ export default function GeneralForm() { setTransferLoading(false); } - async function onSubmitAdvanced(data: AdvancedFormValues) { - setSaveLoading(true); - - const res = await api - .post>( - `resource/${resource?.resourceId}`, - { - tlsServerName: data.http ? data.tlsServerName : undefined, - setHostHeader: data.http ? data.setHostHeader : undefined - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: "Failed to update resource", - description: formatAxiosError( - e, - "An error occurred while updating the resource" - ) - }); - }); - - if (res && res.status === 200) { - toast({ - title: "Resource updated", - description: "The resource has been updated successfully" - }); - - const resource = res.data.data; - - updateResource({ - tlsServerName: data.tlsServerName, - setHostHeader: data.setHostHeader - }); - - router.refresh(); - } - setSaveLoading(false); - } - async function toggleResourceEnabled(val: boolean) { const res = await api .post>( @@ -684,82 +596,11 @@ export default function GeneralForm() { disabled={saveLoading} form="general-settings-form" > - Save Settings + Save General Settings - {resource.http && ( - <> - - - - Advanced - - - Adjust advanced settings for the resource, - like customize the Host Header or set a TLS - Server Name for SNI based routing. - - - - -
- - - TLS Server Name (optional) - - ( - - - - - - - )} - /> - - - Custom Host Header (optional) - - ( - - - - - - - )} - /> - - -
-
- - - - -
- - )} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx index edaf7962..edb21303 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx @@ -86,8 +86,8 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { href: `/{orgId}/settings/resources/{resourceId}/general` }, { - title: "Connectivity", - href: `/{orgId}/settings/resources/{resourceId}/connectivity` + title: "Proxy", + href: `/{orgId}/settings/resources/{resourceId}/proxy` } ]; diff --git a/src/app/[orgId]/settings/resources/[resourceId]/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/page.tsx index 8eb27e4e..a0d45a94 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/page.tsx @@ -5,6 +5,6 @@ export default async function ResourcePage(props: { }) { const params = await props.params; redirect( - `/${params.orgId}/settings/resources/${params.resourceId}/connectivity` + `/${params.orgId}/settings/resources/${params.resourceId}/proxy` ); } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx similarity index 62% rename from src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx rename to src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 18bb8f34..8c027d30 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -60,17 +60,22 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter + SettingsSectionFooter, + SettingsSectionForm } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { useRouter } from "next/navigation"; import { isTargetValid } from "@server/lib/validators"; +import { tlsNameSchema } from "@server/lib/schemas"; const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), port: z.coerce.number().int().positive() - // protocol: z.string(), +}); + +const targetsSettingsSchema = z.object({ + stickySession: z.boolean() }); type LocalTarget = Omit< @@ -81,6 +86,47 @@ type LocalTarget = Omit< "protocol" >; +const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: + "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." + } + ) +}); + +const tlsSettingsSchema = z.object({ + ssl: z.boolean(), + tlsServerName: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: + "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." + } + ) +}); + +type ProxySettingsValues = z.infer; +type TlsSettingsValues = z.infer; +type TargetsSettingsValues = z.infer; + export default function ReverseProxyTargets(props: { params: Promise<{ resourceId: number }>; }) { @@ -93,10 +139,10 @@ export default function ReverseProxyTargets(props: { const [targets, setTargets] = useState([]); const [site, setSite] = useState(); const [targetsToRemove, setTargetsToRemove] = useState([]); - const [sslEnabled, setSslEnabled] = useState(resource.ssl); - const [stickySession, setStickySession] = useState(resource.stickySession); - const [loading, setLoading] = useState(false); + const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); + const [targetsLoading, setTargetsLoading] = useState(false); + const [proxySettingsLoading, setProxySettingsLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); const router = useRouter(); @@ -110,6 +156,28 @@ export default function ReverseProxyTargets(props: { } as z.infer }); + const tlsSettingsForm = useForm({ + resolver: zodResolver(tlsSettingsSchema), + defaultValues: { + ssl: resource.ssl, + tlsServerName: resource.tlsServerName || "" + } + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "" + } + }); + + const targetsSettingsForm = useForm({ + resolver: zodResolver(targetsSettingsSchema), + defaultValues: { + stickySession: resource.stickySession + } + }); + useEffect(() => { const fetchTargets = async () => { try { @@ -230,13 +298,12 @@ export default function ReverseProxyTargets(props: { async function saveTargets() { try { - setLoading(true); + setTargetsLoading(true); for (let target of targets) { const data = { ip: target.ip, port: target.port, - // protocol: target.protocol, method: target.method, enabled: target.enabled }; @@ -249,27 +316,22 @@ export default function ReverseProxyTargets(props: { } else if (target.updated) { await api.post(`/target/${target.targetId}`, data); } - - setTargets([ - ...targets.map((t) => { - let res = { - ...t, - new: false, - updated: false - }; - return res; - }) - ]); } for (const targetId of targetsToRemove) { await api.delete(`/target/${targetId}`); - setTargets(targets.filter((t) => t.targetId !== targetId)); } + // Save sticky session setting + const stickySessionData = targetsSettingsForm.getValues(); + await api.post(`/resource/${params.resourceId}`, { + stickySession: stickySessionData.stickySession + }); + updateResource({ stickySession: stickySessionData.stickySession }); + toast({ title: "Targets updated", - description: "Targets updated successfully" + description: "Targets and settings updated successfully" }); setTargetsToRemove([]); @@ -278,72 +340,75 @@ export default function ReverseProxyTargets(props: { console.error(err); toast({ variant: "destructive", - title: "Operation failed", + title: "Failed to update targets", description: formatAxiosError( err, - "An error occurred during the save operation" + "An error occurred while updating targets" ) }); - } - - setLoading(false); - } - - async function saveSsl(val: boolean) { - const res = await api - .post(`/resource/${params.resourceId}`, { - ssl: val - }) - .catch((err) => { - console.error(err); - toast({ - variant: "destructive", - title: "Failed to update SSL configuration", - description: formatAxiosError( - err, - "An error occurred while updating the SSL configuration" - ) - }); - }); - - if (res && res.status === 200) { - setSslEnabled(val); - updateResource({ ssl: val }); - - toast({ - title: "SSL Configuration", - description: "SSL configuration updated successfully" - }); - router.refresh(); + } finally { + setTargetsLoading(false); } } - async function saveStickySession(val: boolean) { - const res = await api - .post(`/resource/${params.resourceId}`, { - stickySession: val - }) - .catch((err) => { - console.error(err); - toast({ - variant: "destructive", - title: "Failed to update sticky session configuration", - description: formatAxiosError( - err, - "An error occurred while updating the sticky session configuration" - ) - }); + async function saveTlsSettings(data: TlsSettingsValues) { + try { + setHttpsTlsLoading(true); + await api.post(`/resource/${params.resourceId}`, { + ssl: data.ssl, + tlsServerName: data.tlsServerName || undefined + }); + updateResource({ + ...resource, + ssl: data.ssl, + tlsServerName: data.tlsServerName || undefined }); - - if (res && res.status === 200) { - setStickySession(val); - updateResource({ stickySession: val }); - toast({ - title: "Sticky Session Configuration", - description: "Sticky session configuration updated successfully" + title: "TLS settings updated", + description: "Your TLS settings have been updated successfully" }); - router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update TLS settings", + description: formatAxiosError( + err, + "An error occurred while updating TLS settings" + ) + }); + } finally { + setHttpsTlsLoading(false); + } + } + + async function saveProxySettings(data: ProxySettingsValues) { + try { + setProxySettingsLoading(true); + await api.post(`/resource/${params.resourceId}`, { + setHostHeader: data.setHostHeader || undefined + }); + updateResource({ + ...resource, + setHostHeader: data.setHostHeader || undefined + }); + toast({ + title: "Proxy settings updated", + description: + "Your proxy settings have been updated successfully" + }); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update proxy settings", + description: formatAxiosError( + err, + "An error occurred while updating proxy settings" + ) + }); + } finally { + setProxySettingsLoading(false); } } @@ -486,46 +551,128 @@ export default function ReverseProxyTargets(props: { - Advanced Configuration + HTTPS & TLS Settings - Configure advanced settings for your resource + Configure TLS settings for your resource - {targets.length >= 2 && ( - { - await saveStickySession(val); - }} - /> - )} - { - await saveSsl(val); - }} - /> + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + ( + + + TLS Server Name (SNI) + + + + + + The TLS Server Name to use + for SNI. Leave empty to use + the default. + + + + )} + /> + + +
+ + +
)} - {/* Targets Section */} + - Target Configuration + Targets Configuration Set up targets to route traffic to your services + +
+ + {targets.length >= 2 && ( + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + )} + + +
+
+ + {resource.http && ( + + + + Additional Proxy Settings + + + Configure how your resource handles proxy settings + + + + + + + ( + + + Custom Host Header + + + + + + The Host header to set when + proxying requests. Leave + empty to use the default. + + + + )} + /> + + + + + + + +
+ )} ); } diff --git a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx index fb90ba02..c4da2336 100644 --- a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx +++ b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx @@ -227,11 +227,11 @@ export default function CreateSiteForm({ mbIn: data.type == "wireguard" || data.type == "newt" ? "0 MB" - : "--", + : "-", mbOut: data.type == "wireguard" || data.type == "newt" ? "0 MB" - : "--", + : "-", orgId: orgId as string, type: data.type as any, online: false diff --git a/src/app/[orgId]/settings/sites/SitesTable.tsx b/src/app/[orgId]/settings/sites/SitesTable.tsx index c0cc5801..c032800f 100644 --- a/src/app/[orgId]/settings/sites/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/SitesTable.tsx @@ -163,7 +163,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); } } else { - return --; + return -; } } }, diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 66a7ddd1..f107d960 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -134,7 +134,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - Save Settings + Save General Settings
diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 917a74a3..442328b4 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -25,7 +25,7 @@ export default async function SitesPage(props: SitesPageProps) { function formatSize(mb: number, type: string): string { if (type === "local") { - return "--"; // because we are not able to track the data use in a local site right now + return "-"; // because we are not able to track the data use in a local site right now } if (mb >= 1024 * 1024) { return `${(mb / (1024 * 1024)).toFixed(2)} TB`; diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 7e23db1b..44af4434 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -499,7 +499,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - Save Settings + Save General Settings diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 3f170fbe..9740759a 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -37,8 +37,8 @@ export function Breadcrumbs() { // label = "Roles"; // } else if (segment === "invitations") { // label = "Invitations"; - // } else if (segment === "connectivity") { - // label = "Connectivity"; + // } else if (segment === "proxy") { + // label = "proxy"; // } else if (segment === "authentication") { // label = "Authentication"; // }