mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-23 15:00:34 +00:00
DNS Provider configuration
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import express from "express";
|
||||
import dnsPlugins from "../../global/certbot-dns-plugins.json" with { type: "json" };
|
||||
import internalCertificate from "../../internal/certificate.js";
|
||||
import errs from "../../lib/error.js";
|
||||
import jwtdecode from "../../lib/express/jwt-decode.js";
|
||||
@@ -72,6 +73,38 @@ router
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* /api/nginx/certificates/dns-providers
|
||||
*/
|
||||
router
|
||||
.route("/dns-providers")
|
||||
.options((_, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
|
||||
/**
|
||||
* GET /api/nginx/certificates/dns-providers
|
||||
*
|
||||
* Get list of all supported DNS providers
|
||||
*/
|
||||
.get(async (req, res, next) => {
|
||||
try {
|
||||
if (!res.locals.access.token.getUserId()) {
|
||||
throw new errs.PermissionError("Login required");
|
||||
}
|
||||
const clean = Object.keys(dnsPlugins).map((key) => ({
|
||||
id: key,
|
||||
name: dnsPlugins[key].name,
|
||||
credentials: dnsPlugins[key].credentials,
|
||||
}));
|
||||
res.status(200).send(clean);
|
||||
} catch (err) {
|
||||
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Test HTTP challenge for domains
|
||||
*
|
||||
@@ -107,6 +140,41 @@ router
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Validate Certs before saving
|
||||
*
|
||||
* /api/nginx/certificates/validate
|
||||
*/
|
||||
router
|
||||
.route("/validate")
|
||||
.options((_, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
|
||||
/**
|
||||
* POST /api/nginx/certificates/validate
|
||||
*
|
||||
* Validate certificates
|
||||
*/
|
||||
.post(async (req, res, next) => {
|
||||
if (!req.files) {
|
||||
res.status(400).send({ error: "No files were uploaded" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await internalCertificate.validate({
|
||||
files: req.files,
|
||||
});
|
||||
res.status(200).send(result);
|
||||
} catch (err) {
|
||||
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Specific certificate
|
||||
*
|
||||
@@ -266,38 +334,4 @@ router
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Validate Certs before saving
|
||||
*
|
||||
* /api/nginx/certificates/validate
|
||||
*/
|
||||
router
|
||||
.route("/validate")
|
||||
.options((_, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
|
||||
/**
|
||||
* POST /api/nginx/certificates/validate
|
||||
*
|
||||
* Validate certificates
|
||||
*/
|
||||
.post(async (req, res, next) => {
|
||||
if (!req.files) {
|
||||
res.status(400).send({ error: "No files were uploaded" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await internalCertificate.validate({
|
||||
files: req.files,
|
||||
});
|
||||
res.status(200).send(result);
|
||||
} catch (err) {
|
||||
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@@ -14,11 +14,12 @@ router
|
||||
.options((_, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
|
||||
/**
|
||||
* GET /reports/hosts
|
||||
*/
|
||||
.get(jwtdecode(), async (req, res, next) => {
|
||||
.get(async (req, res, next) => {
|
||||
try {
|
||||
const data = await internalReport.getHostsReport(res.locals.access);
|
||||
res.status(200).send(data);
|
||||
|
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