From 53507f88b3927bee9be45eb7de25425764dc2019 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 22 Sep 2025 22:19:18 +1000 Subject: [PATCH] DNS Provider configuration --- backend/routes/nginx/certificates.js | 102 +++++++++----- backend/routes/reports.js | 3 +- .../api/backend/getCertificateDNSProviders.ts | 9 ++ frontend/src/api/backend/index.ts | 1 + frontend/src/api/backend/models.ts | 6 + .../Form/DNSProviderFields.module.css | 16 +++ .../src/components/Form/DNSProviderFields.tsx | 114 ++++++++++++++++ .../components/Form/SSLCertificateField.tsx | 27 +++- .../src/components/Form/SSLOptionsFields.tsx | 128 ++++++++++++++++++ frontend/src/components/Form/index.ts | 2 + frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useDnsProviders.ts | 17 +++ frontend/src/modals/DeadHostModal.tsx | 3 +- 13 files changed, 387 insertions(+), 42 deletions(-) create mode 100644 frontend/src/api/backend/getCertificateDNSProviders.ts create mode 100644 frontend/src/components/Form/DNSProviderFields.module.css create mode 100644 frontend/src/components/Form/DNSProviderFields.tsx create mode 100644 frontend/src/components/Form/SSLOptionsFields.tsx create mode 100644 frontend/src/hooks/useDnsProviders.ts diff --git a/backend/routes/nginx/certificates.js b/backend/routes/nginx/certificates.js index 16d04f38..47a827b0 100644 --- a/backend/routes/nginx/certificates.js +++ b/backend/routes/nginx/certificates.js @@ -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; diff --git a/backend/routes/reports.js b/backend/routes/reports.js index a5ac3de7..bd3a91fe 100644 --- a/backend/routes/reports.js +++ b/backend/routes/reports.js @@ -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); diff --git a/frontend/src/api/backend/getCertificateDNSProviders.ts b/frontend/src/api/backend/getCertificateDNSProviders.ts new file mode 100644 index 00000000..03e3afa2 --- /dev/null +++ b/frontend/src/api/backend/getCertificateDNSProviders.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { DNSProvider } from "./models"; + +export async function getCertificateDNSProviders(params = {}): Promise { + return await api.get({ + url: "/nginx/certificates/dns-providers", + params, + }); +} diff --git a/frontend/src/api/backend/index.ts b/frontend/src/api/backend/index.ts index cb771c04..65210cb8 100644 --- a/frontend/src/api/backend/index.ts +++ b/frontend/src/api/backend/index.ts @@ -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"; diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts index 7a8037a7..6526fcc4 100644 --- a/frontend/src/api/backend/models.ts +++ b/frontend/src/api/backend/models.ts @@ -193,3 +193,9 @@ export interface Setting { value: string; meta: Record; } + +export interface DNSProvider { + id: string; + name: string; + credentials: string; +} diff --git a/frontend/src/components/Form/DNSProviderFields.module.css b/frontend/src/components/Form/DNSProviderFields.module.css new file mode 100644 index 00000000..ba4ac629 --- /dev/null +++ b/frontend/src/components/Form/DNSProviderFields.module.css @@ -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; +} diff --git a/frontend/src/components/Form/DNSProviderFields.tsx b/frontend/src/components/Form/DNSProviderFields.tsx new file mode 100644 index 00000000..3ac36eaf --- /dev/null +++ b/frontend/src/components/Form/DNSProviderFields.tsx @@ -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(null); + + const v: any = values || {}; + + const handleChange = (newValue: any, _actionMeta: ActionMeta) => { + 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 ( +
+

+ This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective + plugins documentation. +

+ + + {({ field }: any) => ( +
+ +