From e36c1b99a5c7d67297988f986d772e17bdc2a585 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Thu, 25 Sep 2025 18:00:00 +1000 Subject: [PATCH] Redirection hosts ui --- .../src/api/backend/getRedirectionHost.ts | 4 +- .../Table/Formatter/EventFormatter.tsx | 10 +- frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useRedirectionHost.ts | 69 ++++ frontend/src/locale/lang/en.json | 12 + frontend/src/locale/src/en.json | 36 +++ frontend/src/modals/RedirectionHostModal.tsx | 304 ++++++++++++++++++ frontend/src/modals/StreamModal.tsx | 106 +++--- frontend/src/modals/UserModal.tsx | 2 +- frontend/src/modals/index.ts | 1 + frontend/src/pages/Dashboard/index.tsx | 5 - .../pages/Nginx/RedirectionHosts/Empty.tsx | 18 +- .../pages/Nginx/RedirectionHosts/Table.tsx | 47 ++- .../Nginx/RedirectionHosts/TableWrapper.tsx | 82 ++++- 14 files changed, 626 insertions(+), 71 deletions(-) create mode 100644 frontend/src/hooks/useRedirectionHost.ts create mode 100644 frontend/src/modals/RedirectionHostModal.tsx diff --git a/frontend/src/api/backend/getRedirectionHost.ts b/frontend/src/api/backend/getRedirectionHost.ts index 9b188741..01df988a 100644 --- a/frontend/src/api/backend/getRedirectionHost.ts +++ b/frontend/src/api/backend/getRedirectionHost.ts @@ -1,8 +1,8 @@ import * as api from "./base"; import type { HostExpansion } from "./expansions"; -import type { ProxyHost } from "./models"; +import type { RedirectionHost } from "./models"; -export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise { +export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise { return await api.get({ url: `/nginx/redirection-hosts/${id}`, params: { diff --git a/frontend/src/components/Table/Formatter/EventFormatter.tsx b/frontend/src/components/Table/Formatter/EventFormatter.tsx index 7cd595d3..1dc0fe69 100644 --- a/frontend/src/components/Table/Formatter/EventFormatter.tsx +++ b/frontend/src/components/Table/Formatter/EventFormatter.tsx @@ -1,4 +1,4 @@ -import { IconBoltOff, IconDisc, IconUser } from "@tabler/icons-react"; +import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconUser } from "@tabler/icons-react"; import type { AuditLog } from "src/api/backend"; import { DateTimeFormat, intl } from "src/locale"; @@ -10,6 +10,8 @@ const getEventValue = (event: AuditLog) => { switch (event.objectType) { case "user": return event.meta?.name; + case "proxy-host": + case "redirection-host": case "dead-host": return event.meta?.domainNames?.join(", ") || "N/A"; case "stream": @@ -37,6 +39,12 @@ const getIcon = (row: AuditLog) => { case "user": ico = ; break; + case "proxy-host": + ico = ; + break; + case "redirection-host": + ico = ; + break; case "dead-host": ico = ; break; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 2c1e73db..37011c1a 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -8,6 +8,7 @@ export * from "./useDnsProviders"; export * from "./useHealth"; export * from "./useHostReport"; export * from "./useProxyHosts"; +export * from "./useRedirectionHost"; export * from "./useRedirectionHosts"; export * from "./useStream"; export * from "./useStreams"; diff --git a/frontend/src/hooks/useRedirectionHost.ts b/frontend/src/hooks/useRedirectionHost.ts new file mode 100644 index 00000000..62aabc98 --- /dev/null +++ b/frontend/src/hooks/useRedirectionHost.ts @@ -0,0 +1,69 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + createRedirectionHost, + getRedirectionHost, + type RedirectionHost, + updateRedirectionHost, +} from "src/api/backend"; + +const fetchRedirectionHost = (id: number | "new") => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + ownerUserId: 0, + domainNames: [], + forwardDomainName: "", + preservePath: false, + certificateId: 0, + sslForced: false, + advancedConfig: "", + meta: {}, + http2Support: false, + forwardScheme: "auto", + forwardHttpCode: 301, + blockExploits: false, + enabled: true, + hstsEnabled: false, + hstsSubdomains: false, + } as RedirectionHost); + } + return getRedirectionHost(id, ["owner"]); +}; + +const useRedirectionHost = (id: number | "new", options = {}) => { + return useQuery({ + queryKey: ["redirection-host", id], + queryFn: () => fetchRedirectionHost(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetRedirectionHost = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: RedirectionHost) => + values.id ? updateRedirectionHost(values) : createRedirectionHost(values), + onMutate: (values: RedirectionHost) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["redirection-host", values.id]); + queryClient.setQueryData(["redirection-host", values.id], (old: RedirectionHost) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["redirection-host", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: RedirectionHost) => { + queryClient.invalidateQueries({ queryKey: ["redirection-host", id] }); + queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + }, + }); +}; + +export { useRedirectionHost, useSetRedirectionHost }; diff --git a/frontend/src/locale/lang/en.json b/frontend/src/locale/lang/en.json index 79db31bc..fb024f5c 100644 --- a/frontend/src/locale/lang/en.json +++ b/frontend/src/locale/lang/en.json @@ -72,17 +72,25 @@ "error.passwords-must-match": "Passwords must match", "error.required": "This is required", "event.created-dead-host": "Created 404 Host", + "event.created-redirection-host": "Created Redirection Host", "event.created-stream": "Created Stream", "event.created-user": "Created User", "event.deleted-dead-host": "Deleted 404 Host", "event.deleted-stream": "Deleted Stream", "event.deleted-user": "Deleted User", "event.disabled-dead-host": "Disabled 404 Host", + "event.disabled-redirection-host": "Disabled Redirection Host", "event.disabled-stream": "Disabled Stream", "event.enabled-dead-host": "Enabled 404 Host", + "event.enabled-redirection-host": "Enabled Redirection Host", "event.enabled-stream": "Enabled Stream", + "event.updated-redirection-host": "Updated Redirection Host", "event.updated-user": "Updated User", "footer.github-fork": "Fork me on Github", + "host.flags.block-exploits": "Block Common Exploits", + "host.flags.preserve-path": "Preserve Path", + "host.flags.protocols": "Protocols", + "host.flags.title": "Options", "hosts.title": "Hosts", "http-only": "HTTP Only", "lets-encrypt": "Let's Encrypt", @@ -99,6 +107,7 @@ "notification.host-deleted": "Host has been deleted", "notification.host-disabled": "Host has been disabled", "notification.host-enabled": "Host has been enabled", + "notification.redirection-host-saved": "Redirection Host has been saved", "notification.stream-deleted": "Stream has been deleted", "notification.stream-disabled": "Stream has been disabled", "notification.stream-enabled": "Stream has been enabled", @@ -124,6 +133,9 @@ "proxy-hosts.count": "{count} Proxy Hosts", "proxy-hosts.empty": "There are no Proxy Hosts", "proxy-hosts.title": "Proxy Hosts", + "redirect-host.forward-domain": "Forward Domain", + "redirect-host.forward-scheme": "Scheme", + "redirection-host.new": "New Redirection Host", "redirection-hosts.actions-title": "Redirection Host #{id}", "redirection-hosts.add": "Add Redirection Host", "redirection-hosts.count": "{count} Redirection Hosts", diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 7c5afff8..ed34e861 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -215,6 +215,18 @@ "event.created-dead-host": { "defaultMessage": "Created 404 Host" }, + "event.created-redirection-host": { + "defaultMessage": "Created Redirection Host" + }, + "event.updated-redirection-host": { + "defaultMessage": "Updated Redirection Host" + }, + "event.enabled-redirection-host": { + "defaultMessage": "Enabled Redirection Host" + }, + "event.disabled-redirection-host": { + "defaultMessage": "Disabled Redirection Host" + }, "event.created-stream": { "defaultMessage": "Created Stream" }, @@ -251,6 +263,18 @@ "empty-subtitle": { "defaultMessage": "Why don't you create one?" }, + "host.flags.title": { + "defaultMessage": "Options" + }, + "host.flags.block-exploits": { + "defaultMessage": "Block Common Exploits" + }, + "host.flags.preserve-path": { + "defaultMessage": "Preserve Path" + }, + "host.flags.protocols": { + "defaultMessage": "Protocols" + }, "hosts.title": { "defaultMessage": "Hosts" }, @@ -299,6 +323,9 @@ "notification.host-enabled": { "defaultMessage": "Host has been enabled" }, + "notification.redirection-host-saved": { + "defaultMessage": "Redirection Host has been saved" + }, "notification.stream-deleted": { "defaultMessage": "Stream has been deleted" }, @@ -374,6 +401,12 @@ "proxy-hosts.title": { "defaultMessage": "Proxy Hosts" }, + "redirect-host.forward-scheme": { + "defaultMessage": "Scheme" + }, + "redirect-host.forward-domain": { + "defaultMessage": "Forward Domain" + }, "redirection-hosts.actions-title": { "defaultMessage": "Redirection Host #{id}" }, @@ -386,6 +419,9 @@ "redirection-hosts.empty": { "defaultMessage": "There are no Redirection Hosts" }, + "redirection-host.new": { + "defaultMessage": "New Redirection Host" + }, "redirection-hosts.title": { "defaultMessage": "Redirection Hosts" }, diff --git a/frontend/src/modals/RedirectionHostModal.tsx b/frontend/src/modals/RedirectionHostModal.tsx new file mode 100644 index 00000000..b3f28617 --- /dev/null +++ b/frontend/src/modals/RedirectionHostModal.tsx @@ -0,0 +1,304 @@ +import { IconSettings } from "@tabler/icons-react"; +import { Field, Form, Formik } from "formik"; +import { useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { + Button, + DomainNamesField, + Loading, + NginxConfigField, + SSLCertificateField, + SSLOptionsFields, +} from "src/components"; +import { useRedirectionHost, useSetRedirectionHost } from "src/hooks"; +import { intl } from "src/locale"; +import { validateString } from "src/modules/Validations"; +import { showSuccess } from "src/notifications"; + +interface Props { + id: number | "new"; + onClose: () => void; +} +export function RedirectionHostModal({ id, onClose }: Props) { + const { data, isLoading, error } = useRedirectionHost(id); + const { mutate: setRedirectionHost } = useSetRedirectionHost(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + ...values, + }; + + setRedirectionHost(payload, { + onError: (err: any) => setErrorMsg(err.message), + onSuccess: () => { + showSuccess(intl.formatMessage({ id: "notification.redirection-host-saved" })); + onClose(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + + {() => ( +
+ + + {intl.formatMessage({ + id: data?.id ? "redirection-host.edit" : "redirection-host.new", + })} + + + + setErrorMsg(null)} dismissible> + {errorMsg} + + +
+ +
+
+
+ +
+
+ + {({ field, form }: any) => ( +
+ + + {form.errors.forwardScheme ? ( +
+ {form.errors.forwardScheme && + form.touched.forwardScheme + ? form.errors.forwardScheme + : null} +
+ ) : null} +
+ )} +
+
+
+ + {({ field, form }: any) => ( +
+ + + {form.errors.forwardDomainName ? ( +
+ {form.errors.forwardDomainName && + form.touched.forwardDomainName + ? form.errors.forwardDomainName + : null} +
+ ) : null} +
+ )} +
+
+
+
+

+ {intl.formatMessage({ id: "host.flags.title" })} +

+
+
+ +
+
+ +
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ + + + +
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/modals/StreamModal.tsx b/frontend/src/modals/StreamModal.tsx index a5b019e6..035a413f 100644 --- a/frontend/src/modals/StreamModal.tsx +++ b/frontend/src/modals/StreamModal.tsx @@ -205,54 +205,84 @@ export function StreamModal({ id, onClose }: Props) { -
-
Protocols
- - {({ field }: any) => ( -
diff --git a/frontend/src/modals/UserModal.tsx b/frontend/src/modals/UserModal.tsx index cc521f41..069c8a71 100644 --- a/frontend/src/modals/UserModal.tsx +++ b/frontend/src/modals/UserModal.tsx @@ -167,7 +167,7 @@ export function UserModal({ userId, onClose }: Props) {
{currentUser && data && currentUser?.id !== data?.id ? (
-

{intl.formatMessage({ id: "user.flags.title" })}

+

{intl.formatMessage({ id: "user.flags.title" })}