You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 18:05:34 +03:00 
			
		
		
		
	DNS Provider configuration
This commit is contained in:
		
							
								
								
									
										9
									
								
								frontend/src/api/backend/getCertificateDNSProviders.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/api/backend/getCertificateDNSProviders.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import * as api from "./base"; | ||||
| import type { DNSProvider } from "./models"; | ||||
|  | ||||
| export async function getCertificateDNSProviders(params = {}): Promise<DNSProvider[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/certificates/dns-providers", | ||||
| 		params, | ||||
| 	}); | ||||
| } | ||||
| @@ -19,6 +19,7 @@ export * from "./getAccessLists"; | ||||
| export * from "./getAuditLog"; | ||||
| export * from "./getAuditLogs"; | ||||
| export * from "./getCertificate"; | ||||
| export * from "./getCertificateDNSProviders"; | ||||
| export * from "./getCertificates"; | ||||
| export * from "./getDeadHost"; | ||||
| export * from "./getDeadHosts"; | ||||
|   | ||||
| @@ -193,3 +193,9 @@ export interface Setting { | ||||
| 	value: string; | ||||
| 	meta: Record<string, any>; | ||||
| } | ||||
|  | ||||
| export interface DNSProvider { | ||||
| 	id: string; | ||||
| 	name: string; | ||||
| 	credentials: string; | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								frontend/src/components/Form/DNSProviderFields.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/components/Form/DNSProviderFields.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| .dnsChallengeWarning { | ||||
| 	border: 1px solid #fecaca; /* Tailwind's red-300 */ | ||||
| 	padding: 1rem; | ||||
| 	border-radius: 0.375rem; /* Tailwind's rounded-md */ | ||||
| 	margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| .textareaMono { | ||||
| 	font-family: 'Courier New', Courier, monospace !important; | ||||
| 	/* background-color: #f9fafb; | ||||
| 	border: 1px solid #d1d5db; | ||||
| 	padding: 0.5rem; | ||||
| 	border-radius: 0.375rem; | ||||
| 	width: 100%; */ | ||||
| 	resize: vertical; | ||||
| } | ||||
							
								
								
									
										114
									
								
								frontend/src/components/Form/DNSProviderFields.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								frontend/src/components/Form/DNSProviderFields.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| import cn from "classnames"; | ||||
| import { Field, useFormikContext } from "formik"; | ||||
| import { useState } from "react"; | ||||
| import Select, { type ActionMeta } from "react-select"; | ||||
| import type { DNSProvider } from "src/api/backend"; | ||||
| import { useDnsProviders } from "src/hooks"; | ||||
| import styles from "./DNSProviderFields.module.css"; | ||||
|  | ||||
| interface DNSProviderOption { | ||||
| 	readonly value: string; | ||||
| 	readonly label: string; | ||||
| 	readonly credentials: string; | ||||
| } | ||||
|  | ||||
| export function DNSProviderFields() { | ||||
| 	const { values, setFieldValue } = useFormikContext(); | ||||
| 	const { data: dnsProviders, isLoading } = useDnsProviders(); | ||||
| 	const [dnsProviderId, setDnsProviderId] = useState<string | null>(null); | ||||
|  | ||||
| 	const v: any = values || {}; | ||||
|  | ||||
| 	const handleChange = (newValue: any, _actionMeta: ActionMeta<DNSProviderOption>) => { | ||||
| 		setFieldValue("dnsProvider", newValue?.value); | ||||
| 		setFieldValue("dnsProviderCredentials", newValue?.credentials); | ||||
| 		setDnsProviderId(newValue?.value); | ||||
| 	}; | ||||
|  | ||||
| 	const options: DNSProviderOption[] = | ||||
| 		dnsProviders?.map((p: DNSProvider) => ({ | ||||
| 			value: p.id, | ||||
| 			label: p.name, | ||||
| 			credentials: p.credentials, | ||||
| 		})) || []; | ||||
|  | ||||
| 	return ( | ||||
| 		<div className={styles.dnsChallengeWarning}> | ||||
| 			<p className="text-danger"> | ||||
| 				This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective | ||||
| 				plugins documentation. | ||||
| 			</p> | ||||
|  | ||||
| 			<Field name="dnsProvider"> | ||||
| 				{({ field }: any) => ( | ||||
| 					<div className="row"> | ||||
| 						<label htmlFor="dnsProvider" className="form-label"> | ||||
| 							DNS Provider | ||||
| 						</label> | ||||
| 						<Select | ||||
| 							name={field.name} | ||||
| 							id="dnsProvider" | ||||
| 							closeMenuOnSelect={true} | ||||
| 							isClearable={false} | ||||
| 							placeholder="Select a Provider..." | ||||
| 							isLoading={isLoading} | ||||
| 							isSearchable | ||||
| 							onChange={handleChange} | ||||
| 							options={options} | ||||
| 						/> | ||||
| 					</div> | ||||
| 				)} | ||||
| 			</Field> | ||||
|  | ||||
| 			{dnsProviderId ? ( | ||||
| 				<> | ||||
| 					<Field name="dnsProviderCredentials"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<div className="row mt-3"> | ||||
| 								<label htmlFor="dnsProviderCredentials" className="form-label"> | ||||
| 									Credentials File Content | ||||
| 								</label> | ||||
| 								<textarea | ||||
| 									id="dnsProviderCredentials" | ||||
| 									className={cn("form-control", styles.textareaMono)} | ||||
| 									rows={3} | ||||
| 									spellCheck={false} | ||||
| 									value={v.dnsProviderCredentials || ""} | ||||
| 									{...field} | ||||
| 								/> | ||||
| 								<small className="text-muted"> | ||||
| 									This plugin requires a configuration file containing an API token or other | ||||
| 									credentials to your provider | ||||
| 								</small> | ||||
| 								<small className="text-danger"> | ||||
| 									This data will be stored as plaintext in the database and in a file! | ||||
| 								</small> | ||||
| 							</div> | ||||
| 						)} | ||||
| 					</Field> | ||||
| 					<Field name="propagationSeconds"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<div className="row mt-3"> | ||||
| 								<label htmlFor="propagationSeconds" className="form-label"> | ||||
| 									Propagation Seconds | ||||
| 								</label> | ||||
| 								<input | ||||
| 									id="propagationSeconds" | ||||
| 									type="number" | ||||
| 									className="form-control" | ||||
| 									min={0} | ||||
| 									max={600} | ||||
| 									{...field} | ||||
| 								/> | ||||
| 								<small className="text-muted"> | ||||
| 									Leave empty to use the plugins default value. Number of seconds to wait for DNS | ||||
| 									propagation. | ||||
| 								</small> | ||||
| 							</div> | ||||
| 						)} | ||||
| 					</Field> | ||||
| 				</> | ||||
| 			) : null} | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -2,7 +2,7 @@ import { IconShield } from "@tabler/icons-react"; | ||||
| import { Field, useFormikContext } from "formik"; | ||||
| import Select, { type ActionMeta, components, type OptionProps } from "react-select"; | ||||
| import type { Certificate } from "src/api/backend"; | ||||
| import { useCertificates } from "src/hooks"; | ||||
| import { useCertificates, useUser } from "src/hooks"; | ||||
| import { DateTimeFormat, intl } from "src/locale"; | ||||
|  | ||||
| interface CertOption { | ||||
| @@ -39,12 +39,27 @@ export function SSLCertificateField({ | ||||
| 	required, | ||||
| 	allowNew, | ||||
| }: Props) { | ||||
| 	const { data: currentUser } = useUser("me"); | ||||
| 	const { isLoading, isError, error, data } = useCertificates(); | ||||
| 	const { values, setFieldValue } = useFormikContext(); | ||||
| 	const v: any = values || {}; | ||||
|  | ||||
| 	const { setFieldValue } = useFormikContext(); | ||||
|  | ||||
| 	const handleChange = (v: any, _actionMeta: ActionMeta<CertOption>) => { | ||||
| 		setFieldValue(name, v?.value); | ||||
| 	const handleChange = (newValue: any, _actionMeta: ActionMeta<CertOption>) => { | ||||
| 		setFieldValue(name, newValue?.value); | ||||
| 		const { sslForced, http2Support, hstsEnabled, hstsSubdomains, dnsChallenge, letsencryptEmail } = v; | ||||
| 		if (!newValue?.value) { | ||||
| 			sslForced && setFieldValue("sslForced", false); | ||||
| 			http2Support && setFieldValue("http2Support", false); | ||||
| 			hstsEnabled && setFieldValue("hstsEnabled", false); | ||||
| 			hstsSubdomains && setFieldValue("hstsSubdomains", false); | ||||
| 		} | ||||
| 		if (newValue?.value === "new") { | ||||
| 			if (!letsencryptEmail) { | ||||
| 				setFieldValue("letsencryptEmail", currentUser?.email); | ||||
| 			} | ||||
| 		} else { | ||||
| 			dnsChallenge && setFieldValue("dnsChallenge", false); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const options: CertOption[] = | ||||
| @@ -61,7 +76,7 @@ export function SSLCertificateField({ | ||||
| 	if (allowNew) { | ||||
| 		options?.unshift({ | ||||
| 			value: "new", | ||||
| 			label: "Request a new HTTP certificate", | ||||
| 			label: "Request a new Certificate", | ||||
| 			subLabel: "with Let's Encrypt", | ||||
| 			icon: <IconShield size={14} className="text-lime" />, | ||||
| 		}); | ||||
|   | ||||
							
								
								
									
										128
									
								
								frontend/src/components/Form/SSLOptionsFields.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								frontend/src/components/Form/SSLOptionsFields.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| import cn from "classnames"; | ||||
| import { Field, useFormikContext } from "formik"; | ||||
| import { DNSProviderFields } from "src/components"; | ||||
|  | ||||
| export function SSLOptionsFields() { | ||||
| 	const { values, setFieldValue } = useFormikContext(); | ||||
| 	const v: any = values || {}; | ||||
|  | ||||
| 	const newCertificate = v?.certificateId === "new"; | ||||
| 	const hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0); | ||||
| 	const { sslForced, http2Support, hstsEnabled, hstsSubdomains, dnsChallenge } = v; | ||||
|  | ||||
| 	const handleToggleChange = (e: any, fieldName: string) => { | ||||
| 		setFieldValue(fieldName, e.target.checked); | ||||
| 	}; | ||||
|  | ||||
| 	const toggleClasses = "form-check-input"; | ||||
| 	const toggleEnabled = cn(toggleClasses, "bg-cyan"); | ||||
|  | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<div className="row"> | ||||
| 				<div className="col-6"> | ||||
| 					<Field name="sslForced"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<label className="form-check form-switch mt-1"> | ||||
| 								<input | ||||
| 									className={sslForced ? toggleEnabled : toggleClasses} | ||||
| 									type="checkbox" | ||||
| 									checked={!!sslForced} | ||||
| 									onChange={(e) => handleToggleChange(e, field.name)} | ||||
| 									disabled={!hasCertificate} | ||||
| 								/> | ||||
| 								<span className="form-check-label">Force SSL</span> | ||||
| 							</label> | ||||
| 						)} | ||||
| 					</Field> | ||||
| 				</div> | ||||
| 				<div className="col-6"> | ||||
| 					<Field name="http2Support"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<label className="form-check form-switch mt-1"> | ||||
| 								<input | ||||
| 									className={http2Support ? toggleEnabled : toggleClasses} | ||||
| 									type="checkbox" | ||||
| 									checked={!!http2Support} | ||||
| 									onChange={(e) => handleToggleChange(e, field.name)} | ||||
| 									disabled={!hasCertificate} | ||||
| 								/> | ||||
| 								<span className="form-check-label">HTTP/2 Support</span> | ||||
| 							</label> | ||||
| 						)} | ||||
| 					</Field> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div className="row"> | ||||
| 				<div className="col-6"> | ||||
| 					<Field name="hstsEnabled"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<label className="form-check form-switch mt-1"> | ||||
| 								<input | ||||
| 									className={hstsEnabled ? toggleEnabled : toggleClasses} | ||||
| 									type="checkbox" | ||||
| 									checked={!!hstsEnabled} | ||||
| 									onChange={(e) => handleToggleChange(e, field.name)} | ||||
| 									disabled={!hasCertificate || !sslForced} | ||||
| 								/> | ||||
| 								<span className="form-check-label">HSTS Enabled</span> | ||||
| 							</label> | ||||
| 						)} | ||||
| 					</Field> | ||||
| 				</div> | ||||
| 				<div className="col-6"> | ||||
| 					<Field name="hstsSubdomains"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<label className="form-check form-switch mt-1"> | ||||
| 								<input | ||||
| 									className={hstsSubdomains ? toggleEnabled : toggleClasses} | ||||
| 									type="checkbox" | ||||
| 									checked={!!hstsSubdomains} | ||||
| 									onChange={(e) => handleToggleChange(e, field.name)} | ||||
| 									disabled={!hasCertificate || !hstsEnabled} | ||||
| 								/> | ||||
| 								<span className="form-check-label">HSTS Enabled</span> | ||||
| 							</label> | ||||
| 						)} | ||||
| 					</Field> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			{newCertificate ? ( | ||||
| 				<> | ||||
| 					<Field name="dnsChallenge"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<label className="form-check form-switch mt-1"> | ||||
| 								<input | ||||
| 									className={dnsChallenge ? toggleEnabled : toggleClasses} | ||||
| 									type="checkbox" | ||||
| 									checked={!!dnsChallenge} | ||||
| 									onChange={(e) => handleToggleChange(e, field.name)} | ||||
| 								/> | ||||
| 								<span className="form-check-label">Use a DNS Challenge</span> | ||||
| 							</label> | ||||
| 						)} | ||||
| 					</Field> | ||||
|  | ||||
| 					{dnsChallenge ? <DNSProviderFields /> : null} | ||||
|  | ||||
| 					<Field name="letsencryptEmail"> | ||||
| 						{({ field }: any) => ( | ||||
| 							<div className="row mt-5"> | ||||
| 								<label htmlFor="letsencryptEmail" className="form-label"> | ||||
| 									Email Address for Let's Encrypt | ||||
| 								</label> | ||||
| 								<input | ||||
| 									id="letsencryptEmail" | ||||
| 									type="email" | ||||
| 									className="form-control" | ||||
| 									required | ||||
| 									{...field} | ||||
| 								/> | ||||
| 							</div> | ||||
| 						)} | ||||
| 					</Field> | ||||
| 				</> | ||||
| 			) : null} | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
| @@ -1,2 +1,4 @@ | ||||
| export * from "./DNSProviderFields"; | ||||
| export * from "./DomainNamesField"; | ||||
| export * from "./SSLCertificateField"; | ||||
| export * from "./SSLOptionsFields"; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ export * from "./useAuditLogs"; | ||||
| export * from "./useCertificates"; | ||||
| export * from "./useDeadHost"; | ||||
| export * from "./useDeadHosts"; | ||||
| export * from "./useDnsProviders"; | ||||
| export * from "./useHealth"; | ||||
| export * from "./useHostReport"; | ||||
| export * from "./useProxyHosts"; | ||||
|   | ||||
							
								
								
									
										17
									
								
								frontend/src/hooks/useDnsProviders.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/hooks/useDnsProviders.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type DNSProvider, getCertificateDNSProviders } from "src/api/backend"; | ||||
|  | ||||
| const fetchDnsProviders = () => { | ||||
| 	return getCertificateDNSProviders(); | ||||
| }; | ||||
|  | ||||
| const useDnsProviders = (options = {}) => { | ||||
| 	return useQuery<DNSProvider[], Error>({ | ||||
| 		queryKey: ["dns-providers"], | ||||
| 		queryFn: () => fetchDnsProviders(), | ||||
| 		staleTime: 300 * 1000, | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { fetchDnsProviders, useDnsProviders }; | ||||
| @@ -3,7 +3,7 @@ import { Form, Formik } from "formik"; | ||||
| import { useState } from "react"; | ||||
| import { Alert } from "react-bootstrap"; | ||||
| import Modal from "react-bootstrap/Modal"; | ||||
| import { Button, DomainNamesField, Loading, SSLCertificateField } from "src/components"; | ||||
| import { Button, DomainNamesField, Loading, SSLCertificateField, SSLOptionsFields } from "src/components"; | ||||
| import { useDeadHost } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| @@ -124,6 +124,7 @@ export function DeadHostModal({ id, onClose }: Props) { | ||||
| 													label="ssl-certificate" | ||||
| 													allowNew | ||||
| 												/> | ||||
| 												<SSLOptionsFields /> | ||||
| 											</div> | ||||
| 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||
| 												<h4>Advanced</h4> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user