You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-11-04 04:11:42 +03:00 
			
		
		
		
	Redirection hosts ui
This commit is contained in:
		@@ -1,8 +1,8 @@
 | 
				
			|||||||
import * as api from "./base";
 | 
					import * as api from "./base";
 | 
				
			||||||
import type { HostExpansion } from "./expansions";
 | 
					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<ProxyHost> {
 | 
					export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise<RedirectionHost> {
 | 
				
			||||||
	return await api.get({
 | 
						return await api.get({
 | 
				
			||||||
		url: `/nginx/redirection-hosts/${id}`,
 | 
							url: `/nginx/redirection-hosts/${id}`,
 | 
				
			||||||
		params: {
 | 
							params: {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 type { AuditLog } from "src/api/backend";
 | 
				
			||||||
import { DateTimeFormat, intl } from "src/locale";
 | 
					import { DateTimeFormat, intl } from "src/locale";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,6 +10,8 @@ const getEventValue = (event: AuditLog) => {
 | 
				
			|||||||
	switch (event.objectType) {
 | 
						switch (event.objectType) {
 | 
				
			||||||
		case "user":
 | 
							case "user":
 | 
				
			||||||
			return event.meta?.name;
 | 
								return event.meta?.name;
 | 
				
			||||||
 | 
							case "proxy-host":
 | 
				
			||||||
 | 
							case "redirection-host":
 | 
				
			||||||
		case "dead-host":
 | 
							case "dead-host":
 | 
				
			||||||
			return event.meta?.domainNames?.join(", ") || "N/A";
 | 
								return event.meta?.domainNames?.join(", ") || "N/A";
 | 
				
			||||||
		case "stream":
 | 
							case "stream":
 | 
				
			||||||
@@ -37,6 +39,12 @@ const getIcon = (row: AuditLog) => {
 | 
				
			|||||||
		case "user":
 | 
							case "user":
 | 
				
			||||||
			ico = <IconUser size={16} className={c} />;
 | 
								ico = <IconUser size={16} className={c} />;
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
 | 
							case "proxy-host":
 | 
				
			||||||
 | 
								ico = <IconBolt size={16} className={c} />;
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case "redirection-host":
 | 
				
			||||||
 | 
								ico = <IconArrowsCross size={16} className={c} />;
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
		case "dead-host":
 | 
							case "dead-host":
 | 
				
			||||||
			ico = <IconBoltOff size={16} className={c} />;
 | 
								ico = <IconBoltOff size={16} className={c} />;
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ export * from "./useDnsProviders";
 | 
				
			|||||||
export * from "./useHealth";
 | 
					export * from "./useHealth";
 | 
				
			||||||
export * from "./useHostReport";
 | 
					export * from "./useHostReport";
 | 
				
			||||||
export * from "./useProxyHosts";
 | 
					export * from "./useProxyHosts";
 | 
				
			||||||
 | 
					export * from "./useRedirectionHost";
 | 
				
			||||||
export * from "./useRedirectionHosts";
 | 
					export * from "./useRedirectionHosts";
 | 
				
			||||||
export * from "./useStream";
 | 
					export * from "./useStream";
 | 
				
			||||||
export * from "./useStreams";
 | 
					export * from "./useStreams";
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										69
									
								
								frontend/src/hooks/useRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								frontend/src/hooks/useRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<RedirectionHost, Error>({
 | 
				
			||||||
 | 
							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 };
 | 
				
			||||||
@@ -72,17 +72,25 @@
 | 
				
			|||||||
  "error.passwords-must-match": "Passwords must match",
 | 
					  "error.passwords-must-match": "Passwords must match",
 | 
				
			||||||
  "error.required": "This is required",
 | 
					  "error.required": "This is required",
 | 
				
			||||||
  "event.created-dead-host": "Created 404 Host",
 | 
					  "event.created-dead-host": "Created 404 Host",
 | 
				
			||||||
 | 
					  "event.created-redirection-host": "Created Redirection Host",
 | 
				
			||||||
  "event.created-stream": "Created Stream",
 | 
					  "event.created-stream": "Created Stream",
 | 
				
			||||||
  "event.created-user": "Created User",
 | 
					  "event.created-user": "Created User",
 | 
				
			||||||
  "event.deleted-dead-host": "Deleted 404 Host",
 | 
					  "event.deleted-dead-host": "Deleted 404 Host",
 | 
				
			||||||
  "event.deleted-stream": "Deleted Stream",
 | 
					  "event.deleted-stream": "Deleted Stream",
 | 
				
			||||||
  "event.deleted-user": "Deleted User",
 | 
					  "event.deleted-user": "Deleted User",
 | 
				
			||||||
  "event.disabled-dead-host": "Disabled 404 Host",
 | 
					  "event.disabled-dead-host": "Disabled 404 Host",
 | 
				
			||||||
 | 
					  "event.disabled-redirection-host": "Disabled Redirection Host",
 | 
				
			||||||
  "event.disabled-stream": "Disabled Stream",
 | 
					  "event.disabled-stream": "Disabled Stream",
 | 
				
			||||||
  "event.enabled-dead-host": "Enabled 404 Host",
 | 
					  "event.enabled-dead-host": "Enabled 404 Host",
 | 
				
			||||||
 | 
					  "event.enabled-redirection-host": "Enabled Redirection Host",
 | 
				
			||||||
  "event.enabled-stream": "Enabled Stream",
 | 
					  "event.enabled-stream": "Enabled Stream",
 | 
				
			||||||
 | 
					  "event.updated-redirection-host": "Updated Redirection Host",
 | 
				
			||||||
  "event.updated-user": "Updated User",
 | 
					  "event.updated-user": "Updated User",
 | 
				
			||||||
  "footer.github-fork": "Fork me on Github",
 | 
					  "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",
 | 
					  "hosts.title": "Hosts",
 | 
				
			||||||
  "http-only": "HTTP Only",
 | 
					  "http-only": "HTTP Only",
 | 
				
			||||||
  "lets-encrypt": "Let's Encrypt",
 | 
					  "lets-encrypt": "Let's Encrypt",
 | 
				
			||||||
@@ -99,6 +107,7 @@
 | 
				
			|||||||
  "notification.host-deleted": "Host has been deleted",
 | 
					  "notification.host-deleted": "Host has been deleted",
 | 
				
			||||||
  "notification.host-disabled": "Host has been disabled",
 | 
					  "notification.host-disabled": "Host has been disabled",
 | 
				
			||||||
  "notification.host-enabled": "Host has been enabled",
 | 
					  "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-deleted": "Stream has been deleted",
 | 
				
			||||||
  "notification.stream-disabled": "Stream has been disabled",
 | 
					  "notification.stream-disabled": "Stream has been disabled",
 | 
				
			||||||
  "notification.stream-enabled": "Stream has been enabled",
 | 
					  "notification.stream-enabled": "Stream has been enabled",
 | 
				
			||||||
@@ -124,6 +133,9 @@
 | 
				
			|||||||
  "proxy-hosts.count": "{count} Proxy Hosts",
 | 
					  "proxy-hosts.count": "{count} Proxy Hosts",
 | 
				
			||||||
  "proxy-hosts.empty": "There are no Proxy Hosts",
 | 
					  "proxy-hosts.empty": "There are no Proxy Hosts",
 | 
				
			||||||
  "proxy-hosts.title": "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.actions-title": "Redirection Host #{id}",
 | 
				
			||||||
  "redirection-hosts.add": "Add Redirection Host",
 | 
					  "redirection-hosts.add": "Add Redirection Host",
 | 
				
			||||||
  "redirection-hosts.count": "{count} Redirection Hosts",
 | 
					  "redirection-hosts.count": "{count} Redirection Hosts",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -215,6 +215,18 @@
 | 
				
			|||||||
	"event.created-dead-host": {
 | 
						"event.created-dead-host": {
 | 
				
			||||||
		"defaultMessage": "Created 404 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": {
 | 
						"event.created-stream": {
 | 
				
			||||||
		"defaultMessage": "Created Stream"
 | 
							"defaultMessage": "Created Stream"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -251,6 +263,18 @@
 | 
				
			|||||||
	"empty-subtitle": {
 | 
						"empty-subtitle": {
 | 
				
			||||||
		"defaultMessage": "Why don't you create one?"
 | 
							"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": {
 | 
						"hosts.title": {
 | 
				
			||||||
		"defaultMessage": "Hosts"
 | 
							"defaultMessage": "Hosts"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -299,6 +323,9 @@
 | 
				
			|||||||
	"notification.host-enabled": {
 | 
						"notification.host-enabled": {
 | 
				
			||||||
		"defaultMessage": "Host has been enabled"
 | 
							"defaultMessage": "Host has been enabled"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						"notification.redirection-host-saved": {
 | 
				
			||||||
 | 
							"defaultMessage": "Redirection Host has been saved"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	"notification.stream-deleted": {
 | 
						"notification.stream-deleted": {
 | 
				
			||||||
		"defaultMessage": "Stream has been deleted"
 | 
							"defaultMessage": "Stream has been deleted"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -374,6 +401,12 @@
 | 
				
			|||||||
	"proxy-hosts.title": {
 | 
						"proxy-hosts.title": {
 | 
				
			||||||
		"defaultMessage": "Proxy Hosts"
 | 
							"defaultMessage": "Proxy Hosts"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						"redirect-host.forward-scheme": {
 | 
				
			||||||
 | 
							"defaultMessage": "Scheme"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"redirect-host.forward-domain": {
 | 
				
			||||||
 | 
							"defaultMessage": "Forward Domain"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	"redirection-hosts.actions-title": {
 | 
						"redirection-hosts.actions-title": {
 | 
				
			||||||
		"defaultMessage": "Redirection Host #{id}"
 | 
							"defaultMessage": "Redirection Host #{id}"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -386,6 +419,9 @@
 | 
				
			|||||||
	"redirection-hosts.empty": {
 | 
						"redirection-hosts.empty": {
 | 
				
			||||||
		"defaultMessage": "There are no Redirection Hosts"
 | 
							"defaultMessage": "There are no Redirection Hosts"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						"redirection-host.new": {
 | 
				
			||||||
 | 
							"defaultMessage": "New Redirection Host"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	"redirection-hosts.title": {
 | 
						"redirection-hosts.title": {
 | 
				
			||||||
		"defaultMessage": "Redirection Hosts"
 | 
							"defaultMessage": "Redirection Hosts"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										304
									
								
								frontend/src/modals/RedirectionHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								frontend/src/modals/RedirectionHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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<string | null>(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 (
 | 
				
			||||||
 | 
							<Modal show onHide={onClose} animation={false}>
 | 
				
			||||||
 | 
								{!isLoading && error && (
 | 
				
			||||||
 | 
									<Alert variant="danger" className="m-3">
 | 
				
			||||||
 | 
										{error?.message || "Unknown error"}
 | 
				
			||||||
 | 
									</Alert>
 | 
				
			||||||
 | 
								)}
 | 
				
			||||||
 | 
								{isLoading && <Loading noLogo />}
 | 
				
			||||||
 | 
								{!isLoading && data && (
 | 
				
			||||||
 | 
									<Formik
 | 
				
			||||||
 | 
										initialValues={
 | 
				
			||||||
 | 
											{
 | 
				
			||||||
 | 
												// Details tab
 | 
				
			||||||
 | 
												domainNames: data?.domainNames || [],
 | 
				
			||||||
 | 
												forwardDomainName: data?.forwardDomainName || "",
 | 
				
			||||||
 | 
												forwardScheme: data?.forwardScheme || "auto",
 | 
				
			||||||
 | 
												forwardHttpCode: data?.forwardHttpCode || 301,
 | 
				
			||||||
 | 
												preservePath: data?.preservePath || false,
 | 
				
			||||||
 | 
												blockExploits: data?.blockExploits || false,
 | 
				
			||||||
 | 
												// SSL tab
 | 
				
			||||||
 | 
												certificateId: data?.certificateId || 0,
 | 
				
			||||||
 | 
												sslForced: data?.sslForced || false,
 | 
				
			||||||
 | 
												http2Support: data?.http2Support || false,
 | 
				
			||||||
 | 
												hstsEnabled: data?.hstsEnabled || false,
 | 
				
			||||||
 | 
												hstsSubdomains: data?.hstsSubdomains || false,
 | 
				
			||||||
 | 
												// Advanced tab
 | 
				
			||||||
 | 
												advancedConfig: data?.advancedConfig || "",
 | 
				
			||||||
 | 
												meta: data?.meta || {},
 | 
				
			||||||
 | 
											} as any
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										onSubmit={onSubmit}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										{() => (
 | 
				
			||||||
 | 
											<Form>
 | 
				
			||||||
 | 
												<Modal.Header closeButton>
 | 
				
			||||||
 | 
													<Modal.Title>
 | 
				
			||||||
 | 
														{intl.formatMessage({
 | 
				
			||||||
 | 
															id: data?.id ? "redirection-host.edit" : "redirection-host.new",
 | 
				
			||||||
 | 
														})}
 | 
				
			||||||
 | 
													</Modal.Title>
 | 
				
			||||||
 | 
												</Modal.Header>
 | 
				
			||||||
 | 
												<Modal.Body className="p-0">
 | 
				
			||||||
 | 
													<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
 | 
				
			||||||
 | 
														{errorMsg}
 | 
				
			||||||
 | 
													</Alert>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
													<div className="card m-0 border-0">
 | 
				
			||||||
 | 
														<div className="card-header">
 | 
				
			||||||
 | 
															<ul className="nav nav-tabs card-header-tabs" data-bs-toggle="tabs">
 | 
				
			||||||
 | 
																<li className="nav-item" role="presentation">
 | 
				
			||||||
 | 
																	<a
 | 
				
			||||||
 | 
																		href="#tab-details"
 | 
				
			||||||
 | 
																		className="nav-link active"
 | 
				
			||||||
 | 
																		data-bs-toggle="tab"
 | 
				
			||||||
 | 
																		aria-selected="true"
 | 
				
			||||||
 | 
																		role="tab"
 | 
				
			||||||
 | 
																	>
 | 
				
			||||||
 | 
																		{intl.formatMessage({ id: "column.details" })}
 | 
				
			||||||
 | 
																	</a>
 | 
				
			||||||
 | 
																</li>
 | 
				
			||||||
 | 
																<li className="nav-item" role="presentation">
 | 
				
			||||||
 | 
																	<a
 | 
				
			||||||
 | 
																		href="#tab-ssl"
 | 
				
			||||||
 | 
																		className="nav-link"
 | 
				
			||||||
 | 
																		data-bs-toggle="tab"
 | 
				
			||||||
 | 
																		aria-selected="false"
 | 
				
			||||||
 | 
																		tabIndex={-1}
 | 
				
			||||||
 | 
																		role="tab"
 | 
				
			||||||
 | 
																	>
 | 
				
			||||||
 | 
																		{intl.formatMessage({ id: "column.ssl" })}
 | 
				
			||||||
 | 
																	</a>
 | 
				
			||||||
 | 
																</li>
 | 
				
			||||||
 | 
																<li className="nav-item ms-auto" role="presentation">
 | 
				
			||||||
 | 
																	<a
 | 
				
			||||||
 | 
																		href="#tab-advanced"
 | 
				
			||||||
 | 
																		className="nav-link"
 | 
				
			||||||
 | 
																		title="Settings"
 | 
				
			||||||
 | 
																		data-bs-toggle="tab"
 | 
				
			||||||
 | 
																		aria-selected="false"
 | 
				
			||||||
 | 
																		tabIndex={-1}
 | 
				
			||||||
 | 
																		role="tab"
 | 
				
			||||||
 | 
																	>
 | 
				
			||||||
 | 
																		<IconSettings size={20} />
 | 
				
			||||||
 | 
																	</a>
 | 
				
			||||||
 | 
																</li>
 | 
				
			||||||
 | 
															</ul>
 | 
				
			||||||
 | 
														</div>
 | 
				
			||||||
 | 
														<div className="card-body">
 | 
				
			||||||
 | 
															<div className="tab-content">
 | 
				
			||||||
 | 
																<div className="tab-pane active show" id="tab-details" role="tabpanel">
 | 
				
			||||||
 | 
																	<DomainNamesField isWildcardPermitted />
 | 
				
			||||||
 | 
																	<div className="row">
 | 
				
			||||||
 | 
																		<div className="col-md-4">
 | 
				
			||||||
 | 
																			<Field name="forwardScheme">
 | 
				
			||||||
 | 
																				{({ field, form }: any) => (
 | 
				
			||||||
 | 
																					<div className="mb-3">
 | 
				
			||||||
 | 
																						<label
 | 
				
			||||||
 | 
																							className="form-label"
 | 
				
			||||||
 | 
																							htmlFor="forwardScheme"
 | 
				
			||||||
 | 
																						>
 | 
				
			||||||
 | 
																							{intl.formatMessage({
 | 
				
			||||||
 | 
																								id: "redirect-host.forward-scheme",
 | 
				
			||||||
 | 
																							})}
 | 
				
			||||||
 | 
																						</label>
 | 
				
			||||||
 | 
																						<select
 | 
				
			||||||
 | 
																							id="forwardScheme"
 | 
				
			||||||
 | 
																							className={`form-control ${form.errors.forwardScheme && form.touched.forwardScheme ? "is-invalid" : ""}`}
 | 
				
			||||||
 | 
																							required
 | 
				
			||||||
 | 
																							{...field}
 | 
				
			||||||
 | 
																						>
 | 
				
			||||||
 | 
																							<option value="$scheme">Auto</option>
 | 
				
			||||||
 | 
																							<option value="http">http</option>
 | 
				
			||||||
 | 
																							<option value="https">https</option>
 | 
				
			||||||
 | 
																						</select>
 | 
				
			||||||
 | 
																						{form.errors.forwardScheme ? (
 | 
				
			||||||
 | 
																							<div className="invalid-feedback">
 | 
				
			||||||
 | 
																								{form.errors.forwardScheme &&
 | 
				
			||||||
 | 
																								form.touched.forwardScheme
 | 
				
			||||||
 | 
																									? form.errors.forwardScheme
 | 
				
			||||||
 | 
																									: null}
 | 
				
			||||||
 | 
																							</div>
 | 
				
			||||||
 | 
																						) : null}
 | 
				
			||||||
 | 
																					</div>
 | 
				
			||||||
 | 
																				)}
 | 
				
			||||||
 | 
																			</Field>
 | 
				
			||||||
 | 
																		</div>
 | 
				
			||||||
 | 
																		<div className="col-md-8">
 | 
				
			||||||
 | 
																			<Field
 | 
				
			||||||
 | 
																				name="forwardDomainName"
 | 
				
			||||||
 | 
																				validate={validateString(1, 255)}
 | 
				
			||||||
 | 
																			>
 | 
				
			||||||
 | 
																				{({ field, form }: any) => (
 | 
				
			||||||
 | 
																					<div className="mb-3">
 | 
				
			||||||
 | 
																						<label
 | 
				
			||||||
 | 
																							className="form-label"
 | 
				
			||||||
 | 
																							htmlFor="forwardDomainName"
 | 
				
			||||||
 | 
																						>
 | 
				
			||||||
 | 
																							{intl.formatMessage({
 | 
				
			||||||
 | 
																								id: "redirect-host.forward-domain",
 | 
				
			||||||
 | 
																							})}
 | 
				
			||||||
 | 
																						</label>
 | 
				
			||||||
 | 
																						<input
 | 
				
			||||||
 | 
																							id="forwardDomainName"
 | 
				
			||||||
 | 
																							type="text"
 | 
				
			||||||
 | 
																							className={`form-control ${form.errors.forwardDomainName && form.touched.forwardDomainName ? "is-invalid" : ""}`}
 | 
				
			||||||
 | 
																							required
 | 
				
			||||||
 | 
																							placeholder="example.com"
 | 
				
			||||||
 | 
																							{...field}
 | 
				
			||||||
 | 
																						/>
 | 
				
			||||||
 | 
																						{form.errors.forwardDomainName ? (
 | 
				
			||||||
 | 
																							<div className="invalid-feedback">
 | 
				
			||||||
 | 
																								{form.errors.forwardDomainName &&
 | 
				
			||||||
 | 
																								form.touched.forwardDomainName
 | 
				
			||||||
 | 
																									? form.errors.forwardDomainName
 | 
				
			||||||
 | 
																									: null}
 | 
				
			||||||
 | 
																							</div>
 | 
				
			||||||
 | 
																						) : null}
 | 
				
			||||||
 | 
																					</div>
 | 
				
			||||||
 | 
																				)}
 | 
				
			||||||
 | 
																			</Field>
 | 
				
			||||||
 | 
																		</div>
 | 
				
			||||||
 | 
																	</div>
 | 
				
			||||||
 | 
																	<div className="my-3">
 | 
				
			||||||
 | 
																		<h4 className="py-2">
 | 
				
			||||||
 | 
																			{intl.formatMessage({ id: "host.flags.title" })}
 | 
				
			||||||
 | 
																		</h4>
 | 
				
			||||||
 | 
																		<div className="divide-y">
 | 
				
			||||||
 | 
																			<div>
 | 
				
			||||||
 | 
																				<label className="row" htmlFor="preservePath">
 | 
				
			||||||
 | 
																					<span className="col">
 | 
				
			||||||
 | 
																						{intl.formatMessage({
 | 
				
			||||||
 | 
																							id: "host.flags.preserve-path",
 | 
				
			||||||
 | 
																						})}
 | 
				
			||||||
 | 
																					</span>
 | 
				
			||||||
 | 
																					<span className="col-auto">
 | 
				
			||||||
 | 
																						<Field name="preservePath" type="checkbox">
 | 
				
			||||||
 | 
																							{({ field }: any) => (
 | 
				
			||||||
 | 
																								<label className="form-check form-check-single form-switch">
 | 
				
			||||||
 | 
																									<input
 | 
				
			||||||
 | 
																										{...field}
 | 
				
			||||||
 | 
																										id="preservePath"
 | 
				
			||||||
 | 
																										className="form-check-input"
 | 
				
			||||||
 | 
																										type="checkbox"
 | 
				
			||||||
 | 
																									/>
 | 
				
			||||||
 | 
																								</label>
 | 
				
			||||||
 | 
																							)}
 | 
				
			||||||
 | 
																						</Field>
 | 
				
			||||||
 | 
																					</span>
 | 
				
			||||||
 | 
																				</label>
 | 
				
			||||||
 | 
																			</div>
 | 
				
			||||||
 | 
																			<div>
 | 
				
			||||||
 | 
																				<label className="row" htmlFor="blockExploits">
 | 
				
			||||||
 | 
																					<span className="col">
 | 
				
			||||||
 | 
																						{intl.formatMessage({
 | 
				
			||||||
 | 
																							id: "host.flags.block-exploits",
 | 
				
			||||||
 | 
																						})}
 | 
				
			||||||
 | 
																					</span>
 | 
				
			||||||
 | 
																					<span className="col-auto">
 | 
				
			||||||
 | 
																						<Field name="blockExploits" type="checkbox">
 | 
				
			||||||
 | 
																							{({ field }: any) => (
 | 
				
			||||||
 | 
																								<label className="form-check form-check-single form-switch">
 | 
				
			||||||
 | 
																									<input
 | 
				
			||||||
 | 
																										{...field}
 | 
				
			||||||
 | 
																										id="blockExploits"
 | 
				
			||||||
 | 
																										className="form-check-input"
 | 
				
			||||||
 | 
																										type="checkbox"
 | 
				
			||||||
 | 
																									/>
 | 
				
			||||||
 | 
																								</label>
 | 
				
			||||||
 | 
																							)}
 | 
				
			||||||
 | 
																						</Field>
 | 
				
			||||||
 | 
																					</span>
 | 
				
			||||||
 | 
																				</label>
 | 
				
			||||||
 | 
																			</div>
 | 
				
			||||||
 | 
																		</div>
 | 
				
			||||||
 | 
																	</div>
 | 
				
			||||||
 | 
																</div>
 | 
				
			||||||
 | 
																<div className="tab-pane" id="tab-ssl" role="tabpanel">
 | 
				
			||||||
 | 
																	<SSLCertificateField
 | 
				
			||||||
 | 
																		name="certificateId"
 | 
				
			||||||
 | 
																		label="ssl-certificate"
 | 
				
			||||||
 | 
																		allowNew
 | 
				
			||||||
 | 
																	/>
 | 
				
			||||||
 | 
																	<SSLOptionsFields />
 | 
				
			||||||
 | 
																</div>
 | 
				
			||||||
 | 
																<div className="tab-pane" id="tab-advanced" role="tabpanel">
 | 
				
			||||||
 | 
																	<NginxConfigField />
 | 
				
			||||||
 | 
																</div>
 | 
				
			||||||
 | 
															</div>
 | 
				
			||||||
 | 
														</div>
 | 
				
			||||||
 | 
													</div>
 | 
				
			||||||
 | 
												</Modal.Body>
 | 
				
			||||||
 | 
												<Modal.Footer>
 | 
				
			||||||
 | 
													<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
 | 
				
			||||||
 | 
														{intl.formatMessage({ id: "cancel" })}
 | 
				
			||||||
 | 
													</Button>
 | 
				
			||||||
 | 
													<Button
 | 
				
			||||||
 | 
														type="submit"
 | 
				
			||||||
 | 
														actionType="primary"
 | 
				
			||||||
 | 
														className="ms-auto"
 | 
				
			||||||
 | 
														data-bs-dismiss="modal"
 | 
				
			||||||
 | 
														isLoading={isSubmitting}
 | 
				
			||||||
 | 
														disabled={isSubmitting}
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
 | 
														{intl.formatMessage({ id: "save" })}
 | 
				
			||||||
 | 
													</Button>
 | 
				
			||||||
 | 
												</Modal.Footer>
 | 
				
			||||||
 | 
											</Form>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									</Formik>
 | 
				
			||||||
 | 
								)}
 | 
				
			||||||
 | 
							</Modal>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -205,54 +205,84 @@ export function StreamModal({ id, onClose }: Props) {
 | 
				
			|||||||
														</Field>
 | 
																			</Field>
 | 
				
			||||||
													</div>
 | 
																		</div>
 | 
				
			||||||
												</div>
 | 
																	</div>
 | 
				
			||||||
												<div className="mb-3">
 | 
																	<div className="my-3">
 | 
				
			||||||
													<div className="form-label">Protocols</div>
 | 
																		<h3 className="py-2">
 | 
				
			||||||
													<Field name="tcpForwarding">
 | 
																			{intl.formatMessage({ id: "host.flags.protocols" })}
 | 
				
			||||||
														{({ field }: any) => (
 | 
																		</h3>
 | 
				
			||||||
															<label className="form-check form-switch">
 | 
																		<div className="divide-y">
 | 
				
			||||||
																<input
 | 
																			<div>
 | 
				
			||||||
																	className="form-check-input"
 | 
																				<label className="row" htmlFor="tcpForwarding">
 | 
				
			||||||
																	type="checkbox"
 | 
																					<span className="col">
 | 
				
			||||||
																	name={field.name}
 | 
					 | 
				
			||||||
																	checked={field.value}
 | 
					 | 
				
			||||||
																	onChange={(e: any) => {
 | 
					 | 
				
			||||||
																		setFieldValue(field.name, e.target.checked);
 | 
					 | 
				
			||||||
																		if (!e.target.checked) {
 | 
					 | 
				
			||||||
																			setFieldValue("udpForwarding", true);
 | 
					 | 
				
			||||||
																		}
 | 
					 | 
				
			||||||
																	}}
 | 
					 | 
				
			||||||
																/>
 | 
					 | 
				
			||||||
																<span className="form-check-label">
 | 
					 | 
				
			||||||
																	{intl.formatMessage({
 | 
																						{intl.formatMessage({
 | 
				
			||||||
																		id: "streams.tcp",
 | 
																							id: "streams.tcp",
 | 
				
			||||||
																	})}
 | 
																						})}
 | 
				
			||||||
																</span>
 | 
																					</span>
 | 
				
			||||||
															</label>
 | 
																					<span className="col-auto">
 | 
				
			||||||
														)}
 | 
																						<Field name="tcpForwarding" type="checkbox">
 | 
				
			||||||
													</Field>
 | 
					 | 
				
			||||||
													<Field name="udpForwarding">
 | 
					 | 
				
			||||||
																		{({ field }: any) => (
 | 
																							{({ field }: any) => (
 | 
				
			||||||
															<label className="form-check form-switch">
 | 
																								<label className="form-check form-check-single form-switch">
 | 
				
			||||||
																				<input
 | 
																									<input
 | 
				
			||||||
 | 
																										id="tcpForwarding"
 | 
				
			||||||
																					className="form-check-input"
 | 
																										className="form-check-input"
 | 
				
			||||||
																					type="checkbox"
 | 
																										type="checkbox"
 | 
				
			||||||
																					name={field.name}
 | 
																										name={field.name}
 | 
				
			||||||
																					checked={field.value}
 | 
																										checked={field.value}
 | 
				
			||||||
																					onChange={(e: any) => {
 | 
																										onChange={(e: any) => {
 | 
				
			||||||
																		setFieldValue(field.name, e.target.checked);
 | 
																											setFieldValue(
 | 
				
			||||||
 | 
																												field.name,
 | 
				
			||||||
 | 
																												e.target.checked,
 | 
				
			||||||
 | 
																											);
 | 
				
			||||||
																						if (!e.target.checked) {
 | 
																											if (!e.target.checked) {
 | 
				
			||||||
																			setFieldValue("tcpForwarding", true);
 | 
																												setFieldValue(
 | 
				
			||||||
 | 
																													"udpForwarding",
 | 
				
			||||||
 | 
																													true,
 | 
				
			||||||
 | 
																												);
 | 
				
			||||||
																						}
 | 
																											}
 | 
				
			||||||
																					}}
 | 
																										}}
 | 
				
			||||||
																				/>
 | 
																									/>
 | 
				
			||||||
																<span className="form-check-label">
 | 
																								</label>
 | 
				
			||||||
 | 
																							)}
 | 
				
			||||||
 | 
																						</Field>
 | 
				
			||||||
 | 
																					</span>
 | 
				
			||||||
 | 
																				</label>
 | 
				
			||||||
 | 
																			</div>
 | 
				
			||||||
 | 
																			<div>
 | 
				
			||||||
 | 
																				<label className="row" htmlFor="udpForwarding">
 | 
				
			||||||
 | 
																					<span className="col">
 | 
				
			||||||
																	{intl.formatMessage({
 | 
																						{intl.formatMessage({
 | 
				
			||||||
																		id: "streams.udp",
 | 
																							id: "streams.udp",
 | 
				
			||||||
																	})}
 | 
																						})}
 | 
				
			||||||
																</span>
 | 
																					</span>
 | 
				
			||||||
 | 
																					<span className="col-auto">
 | 
				
			||||||
 | 
																						<Field name="udpForwarding" type="checkbox">
 | 
				
			||||||
 | 
																							{({ field }: any) => (
 | 
				
			||||||
 | 
																								<label className="form-check form-check-single form-switch">
 | 
				
			||||||
 | 
																									<input
 | 
				
			||||||
 | 
																										id="udpForwarding"
 | 
				
			||||||
 | 
																										className="form-check-input"
 | 
				
			||||||
 | 
																										type="checkbox"
 | 
				
			||||||
 | 
																										name={field.name}
 | 
				
			||||||
 | 
																										checked={field.value}
 | 
				
			||||||
 | 
																										onChange={(e: any) => {
 | 
				
			||||||
 | 
																											setFieldValue(
 | 
				
			||||||
 | 
																												field.name,
 | 
				
			||||||
 | 
																												e.target.checked,
 | 
				
			||||||
 | 
																											);
 | 
				
			||||||
 | 
																											if (!e.target.checked) {
 | 
				
			||||||
 | 
																												setFieldValue(
 | 
				
			||||||
 | 
																													"tcpForwarding",
 | 
				
			||||||
 | 
																													true,
 | 
				
			||||||
 | 
																												);
 | 
				
			||||||
 | 
																											}
 | 
				
			||||||
 | 
																										}}
 | 
				
			||||||
 | 
																									/>
 | 
				
			||||||
																			</label>
 | 
																								</label>
 | 
				
			||||||
																		)}
 | 
																							)}
 | 
				
			||||||
																	</Field>
 | 
																						</Field>
 | 
				
			||||||
 | 
																					</span>
 | 
				
			||||||
 | 
																				</label>
 | 
				
			||||||
 | 
																			</div>
 | 
				
			||||||
 | 
																		</div>
 | 
				
			||||||
												</div>
 | 
																	</div>
 | 
				
			||||||
											</div>
 | 
																</div>
 | 
				
			||||||
											<div className="tab-pane" id="tab-ssl" role="tabpanel">
 | 
																<div className="tab-pane" id="tab-ssl" role="tabpanel">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -167,7 +167,7 @@ export function UserModal({ userId, onClose }: Props) {
 | 
				
			|||||||
								</div>
 | 
													</div>
 | 
				
			||||||
								{currentUser && data && currentUser?.id !== data?.id ? (
 | 
													{currentUser && data && currentUser?.id !== data?.id ? (
 | 
				
			||||||
									<div className="my-3">
 | 
														<div className="my-3">
 | 
				
			||||||
										<h3 className="py-2">{intl.formatMessage({ id: "user.flags.title" })}</h3>
 | 
															<h4 className="py-2">{intl.formatMessage({ id: "user.flags.title" })}</h4>
 | 
				
			||||||
										<div className="divide-y">
 | 
															<div className="divide-y">
 | 
				
			||||||
											<div>
 | 
																<div>
 | 
				
			||||||
												<label className="row" htmlFor="isAdmin">
 | 
																	<label className="row" htmlFor="isAdmin">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ export * from "./DeadHostModal";
 | 
				
			|||||||
export * from "./DeleteConfirmModal";
 | 
					export * from "./DeleteConfirmModal";
 | 
				
			||||||
export * from "./EventDetailsModal";
 | 
					export * from "./EventDetailsModal";
 | 
				
			||||||
export * from "./PermissionsModal";
 | 
					export * from "./PermissionsModal";
 | 
				
			||||||
 | 
					export * from "./RedirectionHostModal";
 | 
				
			||||||
export * from "./SetPasswordModal";
 | 
					export * from "./SetPasswordModal";
 | 
				
			||||||
export * from "./StreamModal";
 | 
					export * from "./StreamModal";
 | 
				
			||||||
export * from "./UserModal";
 | 
					export * from "./UserModal";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -122,14 +122,9 @@ const Dashboard = () => {
 | 
				
			|||||||
			<pre>
 | 
								<pre>
 | 
				
			||||||
				<code>{`Todo:
 | 
									<code>{`Todo:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Users: permissions modal and trigger after adding user
 | 
					 | 
				
			||||||
- modal dialgs for everything
 | 
					 | 
				
			||||||
- Tables
 | 
					 | 
				
			||||||
- check mobile
 | 
					- check mobile
 | 
				
			||||||
- fix bad jwt not refreshing entire page
 | 
					 | 
				
			||||||
- add help docs for host types
 | 
					- add help docs for host types
 | 
				
			||||||
- REDO SCREENSHOTS in docs folder
 | 
					- REDO SCREENSHOTS in docs folder
 | 
				
			||||||
- Remove letsEncryptEmail field from new certificate requests, use current user email server side
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
More for api, then implement here:
 | 
					More for api, then implement here:
 | 
				
			||||||
- Properly implement refresh tokens
 | 
					- Properly implement refresh tokens
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,15 +4,25 @@ import { intl } from "src/locale";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
	tableInstance: ReactTable<any>;
 | 
						tableInstance: ReactTable<any>;
 | 
				
			||||||
 | 
						onNew?: () => void;
 | 
				
			||||||
 | 
						isFiltered?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export default function Empty({ tableInstance }: Props) {
 | 
					export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<tr>
 | 
							<tr>
 | 
				
			||||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
								<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
				
			||||||
				<div className="text-center my-4">
 | 
									<div className="text-center my-4">
 | 
				
			||||||
 | 
										{isFiltered ? (
 | 
				
			||||||
 | 
											<h2>{intl.formatMessage({ id: "empty-search" })}</h2>
 | 
				
			||||||
 | 
										) : (
 | 
				
			||||||
 | 
											<>
 | 
				
			||||||
							<h2>{intl.formatMessage({ id: "redirection-hosts.empty" })}</h2>
 | 
												<h2>{intl.formatMessage({ id: "redirection-hosts.empty" })}</h2>
 | 
				
			||||||
							<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
 | 
												<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
 | 
				
			||||||
					<Button className="btn-yellow my-3">{intl.formatMessage({ id: "redirection-hosts.add" })}</Button>
 | 
												<Button className="btn-yellow my-3" onClick={onNew}>
 | 
				
			||||||
 | 
													{intl.formatMessage({ id: "redirection-hosts.add" })}
 | 
				
			||||||
 | 
												</Button>
 | 
				
			||||||
 | 
											</>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</td>
 | 
								</td>
 | 
				
			||||||
		</tr>
 | 
							</tr>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,9 +9,14 @@ import Empty from "./Empty";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
	data: RedirectionHost[];
 | 
						data: RedirectionHost[];
 | 
				
			||||||
 | 
						isFiltered?: boolean;
 | 
				
			||||||
	isFetching?: boolean;
 | 
						isFetching?: boolean;
 | 
				
			||||||
 | 
						onEdit?: (id: number) => void;
 | 
				
			||||||
 | 
						onDelete?: (id: number) => void;
 | 
				
			||||||
 | 
						onDisableToggle?: (id: number, enabled: boolean) => void;
 | 
				
			||||||
 | 
						onNew?: () => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export default function Table({ data, isFetching }: Props) {
 | 
					export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
 | 
				
			||||||
	const columnHelper = createColumnHelper<RedirectionHost>();
 | 
						const columnHelper = createColumnHelper<RedirectionHost>();
 | 
				
			||||||
	const columns = useMemo(
 | 
						const columns = useMemo(
 | 
				
			||||||
		() => [
 | 
							() => [
 | 
				
			||||||
@@ -90,16 +95,39 @@ export default function Table({ data, isFetching }: Props) {
 | 
				
			|||||||
										{ id: info.row.original.id },
 | 
															{ id: info.row.original.id },
 | 
				
			||||||
									)}
 | 
														)}
 | 
				
			||||||
								</span>
 | 
													</span>
 | 
				
			||||||
								<a className="dropdown-item" href="#">
 | 
													<a
 | 
				
			||||||
 | 
														className="dropdown-item"
 | 
				
			||||||
 | 
														href="#"
 | 
				
			||||||
 | 
														onClick={(e) => {
 | 
				
			||||||
 | 
															e.preventDefault();
 | 
				
			||||||
 | 
															onEdit?.(info.row.original.id);
 | 
				
			||||||
 | 
														}}
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
									<IconEdit size={16} />
 | 
														<IconEdit size={16} />
 | 
				
			||||||
									{intl.formatMessage({ id: "action.edit" })}
 | 
														{intl.formatMessage({ id: "action.edit" })}
 | 
				
			||||||
								</a>
 | 
													</a>
 | 
				
			||||||
								<a className="dropdown-item" href="#">
 | 
													<a
 | 
				
			||||||
 | 
														className="dropdown-item"
 | 
				
			||||||
 | 
														href="#"
 | 
				
			||||||
 | 
														onClick={(e) => {
 | 
				
			||||||
 | 
															e.preventDefault();
 | 
				
			||||||
 | 
															onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
 | 
				
			||||||
 | 
														}}
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
									<IconPower size={16} />
 | 
														<IconPower size={16} />
 | 
				
			||||||
									{intl.formatMessage({ id: "action.disable" })}
 | 
														{intl.formatMessage({
 | 
				
			||||||
 | 
															id: info.row.original.enabled ? "action.disable" : "action.enable",
 | 
				
			||||||
 | 
														})}
 | 
				
			||||||
								</a>
 | 
													</a>
 | 
				
			||||||
								<div className="dropdown-divider" />
 | 
													<div className="dropdown-divider" />
 | 
				
			||||||
								<a className="dropdown-item" href="#">
 | 
													<a
 | 
				
			||||||
 | 
														className="dropdown-item"
 | 
				
			||||||
 | 
														href="#"
 | 
				
			||||||
 | 
														onClick={(e) => {
 | 
				
			||||||
 | 
															e.preventDefault();
 | 
				
			||||||
 | 
															onDelete?.(info.row.original.id);
 | 
				
			||||||
 | 
														}}
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
									<IconTrash size={16} />
 | 
														<IconTrash size={16} />
 | 
				
			||||||
									{intl.formatMessage({ id: "action.delete" })}
 | 
														{intl.formatMessage({ id: "action.delete" })}
 | 
				
			||||||
								</a>
 | 
													</a>
 | 
				
			||||||
@@ -112,7 +140,7 @@ export default function Table({ data, isFetching }: Props) {
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
			}),
 | 
								}),
 | 
				
			||||||
		],
 | 
							],
 | 
				
			||||||
		[columnHelper],
 | 
							[columnHelper, onEdit, onDisableToggle, onDelete],
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const tableInstance = useReactTable<RedirectionHost>({
 | 
						const tableInstance = useReactTable<RedirectionHost>({
 | 
				
			||||||
@@ -126,5 +154,10 @@ export default function Table({ data, isFetching }: Props) {
 | 
				
			|||||||
		enableSortingRemoval: false,
 | 
							enableSortingRemoval: false,
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return <TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} />} />;
 | 
						return (
 | 
				
			||||||
 | 
							<TableLayout
 | 
				
			||||||
 | 
								tableInstance={tableInstance}
 | 
				
			||||||
 | 
								emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,20 @@
 | 
				
			|||||||
import { IconSearch } from "@tabler/icons-react";
 | 
					import { IconSearch } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import { useQueryClient } from "@tanstack/react-query";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
import Alert from "react-bootstrap/Alert";
 | 
					import Alert from "react-bootstrap/Alert";
 | 
				
			||||||
 | 
					import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend";
 | 
				
			||||||
import { Button, LoadingPage } from "src/components";
 | 
					import { Button, LoadingPage } from "src/components";
 | 
				
			||||||
import { useRedirectionHosts } from "src/hooks";
 | 
					import { useRedirectionHosts } from "src/hooks";
 | 
				
			||||||
import { intl } from "src/locale";
 | 
					import { intl } from "src/locale";
 | 
				
			||||||
 | 
					import { DeleteConfirmModal, RedirectionHostModal } from "src/modals";
 | 
				
			||||||
 | 
					import { showSuccess } from "src/notifications";
 | 
				
			||||||
import Table from "./Table";
 | 
					import Table from "./Table";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function TableWrapper() {
 | 
					export default function TableWrapper() {
 | 
				
			||||||
 | 
						const queryClient = useQueryClient();
 | 
				
			||||||
 | 
						const [search, setSearch] = useState("");
 | 
				
			||||||
 | 
						const [deleteId, setDeleteId] = useState(0);
 | 
				
			||||||
 | 
						const [editId, setEditId] = useState(0 as number | "new");
 | 
				
			||||||
	const { isFetching, isLoading, isError, error, data } = useRedirectionHosts(["owner", "certificate"]);
 | 
						const { isFetching, isLoading, isError, error, data } = useRedirectionHosts(["owner", "certificate"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (isLoading) {
 | 
						if (isLoading) {
 | 
				
			||||||
@@ -16,6 +25,31 @@ export default function TableWrapper() {
 | 
				
			|||||||
		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
 | 
							return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleDelete = async () => {
 | 
				
			||||||
 | 
							await deleteRedirectionHost(deleteId);
 | 
				
			||||||
 | 
							showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleDisableToggle = async (id: number, enabled: boolean) => {
 | 
				
			||||||
 | 
							await toggleRedirectionHost(id, enabled);
 | 
				
			||||||
 | 
							queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] });
 | 
				
			||||||
 | 
							queryClient.invalidateQueries({ queryKey: ["redirection-host", id] });
 | 
				
			||||||
 | 
							showSuccess(intl.formatMessage({ id: enabled ? "notification.host-enabled" : "notification.host-disabled" }));
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let filtered = null;
 | 
				
			||||||
 | 
						if (search && data) {
 | 
				
			||||||
 | 
							filtered = data?.filter((item) => {
 | 
				
			||||||
 | 
								return (
 | 
				
			||||||
 | 
									item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||
 | 
				
			||||||
 | 
									item.forwardDomainName.toLowerCase().includes(search)
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						} else if (search !== "") {
 | 
				
			||||||
 | 
							// this can happen if someone deletes the last item while searching
 | 
				
			||||||
 | 
							setSearch("");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<div className="card mt-4">
 | 
							<div className="card mt-4">
 | 
				
			||||||
			<div className="card-status-top bg-yellow" />
 | 
								<div className="card-status-top bg-yellow" />
 | 
				
			||||||
@@ -27,6 +61,7 @@ export default function TableWrapper() {
 | 
				
			|||||||
						</div>
 | 
											</div>
 | 
				
			||||||
						<div className="col-md-auto col-sm-12">
 | 
											<div className="col-md-auto col-sm-12">
 | 
				
			||||||
							<div className="ms-auto d-flex flex-wrap btn-list">
 | 
												<div className="ms-auto d-flex flex-wrap btn-list">
 | 
				
			||||||
 | 
													{data?.length ? (
 | 
				
			||||||
									<div className="input-group input-group-flat w-auto">
 | 
														<div className="input-group input-group-flat w-auto">
 | 
				
			||||||
										<span className="input-group-text input-group-text-sm">
 | 
															<span className="input-group-text input-group-text-sm">
 | 
				
			||||||
											<IconSearch size={16} />
 | 
																<IconSearch size={16} />
 | 
				
			||||||
@@ -36,16 +71,37 @@ export default function TableWrapper() {
 | 
				
			|||||||
											type="text"
 | 
																type="text"
 | 
				
			||||||
											className="form-control form-control-sm"
 | 
																className="form-control form-control-sm"
 | 
				
			||||||
											autoComplete="off"
 | 
																autoComplete="off"
 | 
				
			||||||
 | 
																onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
 | 
				
			||||||
										/>
 | 
															/>
 | 
				
			||||||
									</div>
 | 
														</div>
 | 
				
			||||||
								<Button size="sm" className="btn-yellow">
 | 
													) : null}
 | 
				
			||||||
 | 
													<Button size="sm" className="btn-yellow" onClick={() => setEditId("new")}>
 | 
				
			||||||
									{intl.formatMessage({ id: "redirection-hosts.add" })}
 | 
														{intl.formatMessage({ id: "redirection-hosts.add" })}
 | 
				
			||||||
								</Button>
 | 
													</Button>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<Table data={data ?? []} isFetching={isFetching} />
 | 
									<Table
 | 
				
			||||||
 | 
										data={filtered ?? data ?? []}
 | 
				
			||||||
 | 
										isFiltered={!!search}
 | 
				
			||||||
 | 
										isFetching={isFetching}
 | 
				
			||||||
 | 
										onEdit={(id: number) => setEditId(id)}
 | 
				
			||||||
 | 
										onDelete={(id: number) => setDeleteId(id)}
 | 
				
			||||||
 | 
										onDisableToggle={handleDisableToggle}
 | 
				
			||||||
 | 
										onNew={() => setEditId("new")}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
									{editId ? <RedirectionHostModal id={editId} onClose={() => setEditId(0)} /> : null}
 | 
				
			||||||
 | 
									{deleteId ? (
 | 
				
			||||||
 | 
										<DeleteConfirmModal
 | 
				
			||||||
 | 
											title={intl.formatMessage({ id: "redirection-host.delete.title" })}
 | 
				
			||||||
 | 
											onConfirm={handleDelete}
 | 
				
			||||||
 | 
											onClose={() => setDeleteId(0)}
 | 
				
			||||||
 | 
											invalidations={[["redirection-hosts"], ["redirection-host", deleteId]]}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											{intl.formatMessage({ id: "redirection-host.delete.content" })}
 | 
				
			||||||
 | 
										</DeleteConfirmModal>
 | 
				
			||||||
 | 
									) : null}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user