Skip to content

Commit 94375bb

Browse files
committed
DNS Provider configuration
1 parent 54e0362 commit 94375bb

File tree

13 files changed

+387
-42
lines changed

13 files changed

+387
-42
lines changed

backend/routes/nginx/certificates.js

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import express from "express";
2+
import dnsPlugins from "../../global/certbot-dns-plugins.json" with { type: "json" };
23
import internalCertificate from "../../internal/certificate.js";
34
import errs from "../../lib/error.js";
45
import jwtdecode from "../../lib/express/jwt-decode.js";
@@ -72,6 +73,38 @@ router
7273
}
7374
});
7475

76+
/**
77+
* /api/nginx/certificates/dns-providers
78+
*/
79+
router
80+
.route("/dns-providers")
81+
.options((_, res) => {
82+
res.sendStatus(204);
83+
})
84+
.all(jwtdecode())
85+
86+
/**
87+
* GET /api/nginx/certificates/dns-providers
88+
*
89+
* Get list of all supported DNS providers
90+
*/
91+
.get(async (req, res, next) => {
92+
try {
93+
if (!res.locals.access.token.getUserId()) {
94+
throw new errs.PermissionError("Login required");
95+
}
96+
const clean = Object.keys(dnsPlugins).map((key) => ({
97+
id: key,
98+
name: dnsPlugins[key].name,
99+
credentials: dnsPlugins[key].credentials,
100+
}));
101+
res.status(200).send(clean);
102+
} catch (err) {
103+
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
104+
next(err);
105+
}
106+
});
107+
75108
/**
76109
* Test HTTP challenge for domains
77110
*
@@ -107,6 +140,41 @@ router
107140
}
108141
});
109142

143+
144+
/**
145+
* Validate Certs before saving
146+
*
147+
* /api/nginx/certificates/validate
148+
*/
149+
router
150+
.route("/validate")
151+
.options((_, res) => {
152+
res.sendStatus(204);
153+
})
154+
.all(jwtdecode())
155+
156+
/**
157+
* POST /api/nginx/certificates/validate
158+
*
159+
* Validate certificates
160+
*/
161+
.post(async (req, res, next) => {
162+
if (!req.files) {
163+
res.status(400).send({ error: "No files were uploaded" });
164+
return;
165+
}
166+
167+
try {
168+
const result = await internalCertificate.validate({
169+
files: req.files,
170+
});
171+
res.status(200).send(result);
172+
} catch (err) {
173+
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
174+
next(err);
175+
}
176+
});
177+
110178
/**
111179
* Specific certificate
112180
*
@@ -266,38 +334,4 @@ router
266334
}
267335
});
268336

269-
/**
270-
* Validate Certs before saving
271-
*
272-
* /api/nginx/certificates/validate
273-
*/
274-
router
275-
.route("/validate")
276-
.options((_, res) => {
277-
res.sendStatus(204);
278-
})
279-
.all(jwtdecode())
280-
281-
/**
282-
* POST /api/nginx/certificates/validate
283-
*
284-
* Validate certificates
285-
*/
286-
.post(async (req, res, next) => {
287-
if (!req.files) {
288-
res.status(400).send({ error: "No files were uploaded" });
289-
return;
290-
}
291-
292-
try {
293-
const result = await internalCertificate.validate({
294-
files: req.files,
295-
});
296-
res.status(200).send(result);
297-
} catch (err) {
298-
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
299-
next(err);
300-
}
301-
});
302-
303337
export default router;

backend/routes/reports.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ router
1414
.options((_, res) => {
1515
res.sendStatus(204);
1616
})
17+
.all(jwtdecode())
1718

1819
/**
1920
* GET /reports/hosts
2021
*/
21-
.get(jwtdecode(), async (req, res, next) => {
22+
.get(async (req, res, next) => {
2223
try {
2324
const data = await internalReport.getHostsReport(res.locals.access);
2425
res.status(200).send(data);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as api from "./base";
2+
import type { DNSProvider } from "./models";
3+
4+
export async function getCertificateDNSProviders(params = {}): Promise<DNSProvider[]> {
5+
return await api.get({
6+
url: "/nginx/certificates/dns-providers",
7+
params,
8+
});
9+
}

frontend/src/api/backend/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from "./getAccessLists";
1919
export * from "./getAuditLog";
2020
export * from "./getAuditLogs";
2121
export * from "./getCertificate";
22+
export * from "./getCertificateDNSProviders";
2223
export * from "./getCertificates";
2324
export * from "./getDeadHost";
2425
export * from "./getDeadHosts";

frontend/src/api/backend/models.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,9 @@ export interface Setting {
193193
value: string;
194194
meta: Record<string, any>;
195195
}
196+
197+
export interface DNSProvider {
198+
id: string;
199+
name: string;
200+
credentials: string;
201+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.dnsChallengeWarning {
2+
border: 1px solid #fecaca; /* Tailwind's red-300 */
3+
padding: 1rem;
4+
border-radius: 0.375rem; /* Tailwind's rounded-md */
5+
margin-top: 1rem;
6+
}
7+
8+
.textareaMono {
9+
font-family: 'Courier New', Courier, monospace !important;
10+
/* background-color: #f9fafb;
11+
border: 1px solid #d1d5db;
12+
padding: 0.5rem;
13+
border-radius: 0.375rem;
14+
width: 100%; */
15+
resize: vertical;
16+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import cn from "classnames";
2+
import { Field, useFormikContext } from "formik";
3+
import { useState } from "react";
4+
import Select, { type ActionMeta } from "react-select";
5+
import type { DNSProvider } from "src/api/backend";
6+
import { useDnsProviders } from "src/hooks";
7+
import styles from "./DNSProviderFields.module.css";
8+
9+
interface DNSProviderOption {
10+
readonly value: string;
11+
readonly label: string;
12+
readonly credentials: string;
13+
}
14+
15+
export function DNSProviderFields() {
16+
const { values, setFieldValue } = useFormikContext();
17+
const { data: dnsProviders, isLoading } = useDnsProviders();
18+
const [dnsProviderId, setDnsProviderId] = useState<string | null>(null);
19+
20+
const v: any = values || {};
21+
22+
const handleChange = (newValue: any, _actionMeta: ActionMeta<DNSProviderOption>) => {
23+
setFieldValue("dnsProvider", newValue?.value);
24+
setFieldValue("dnsProviderCredentials", newValue?.credentials);
25+
setDnsProviderId(newValue?.value);
26+
};
27+
28+
const options: DNSProviderOption[] =
29+
dnsProviders?.map((p: DNSProvider) => ({
30+
value: p.id,
31+
label: p.name,
32+
credentials: p.credentials,
33+
})) || [];
34+
35+
return (
36+
<div className={styles.dnsChallengeWarning}>
37+
<p className="text-danger">
38+
This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective
39+
plugins documentation.
40+
</p>
41+
42+
<Field name="dnsProvider">
43+
{({ field }: any) => (
44+
<div className="row">
45+
<label htmlFor="dnsProvider" className="form-label">
46+
DNS Provider
47+
</label>
48+
<Select
49+
name={field.name}
50+
id="dnsProvider"
51+
closeMenuOnSelect={true}
52+
isClearable={false}
53+
placeholder="Select a Provider..."
54+
isLoading={isLoading}
55+
isSearchable
56+
onChange={handleChange}
57+
options={options}
58+
/>
59+
</div>
60+
)}
61+
</Field>
62+
63+
{dnsProviderId ? (
64+
<>
65+
<Field name="dnsProviderCredentials">
66+
{({ field }: any) => (
67+
<div className="row mt-3">
68+
<label htmlFor="dnsProviderCredentials" className="form-label">
69+
Credentials File Content
70+
</label>
71+
<textarea
72+
id="dnsProviderCredentials"
73+
className={cn("form-control", styles.textareaMono)}
74+
rows={3}
75+
spellCheck={false}
76+
value={v.dnsProviderCredentials || ""}
77+
{...field}
78+
/>
79+
<small className="text-muted">
80+
This plugin requires a configuration file containing an API token or other
81+
credentials to your provider
82+
</small>
83+
<small className="text-danger">
84+
This data will be stored as plaintext in the database and in a file!
85+
</small>
86+
</div>
87+
)}
88+
</Field>
89+
<Field name="propagationSeconds">
90+
{({ field }: any) => (
91+
<div className="row mt-3">
92+
<label htmlFor="propagationSeconds" className="form-label">
93+
Propagation Seconds
94+
</label>
95+
<input
96+
id="propagationSeconds"
97+
type="number"
98+
className="form-control"
99+
min={0}
100+
max={600}
101+
{...field}
102+
/>
103+
<small className="text-muted">
104+
Leave empty to use the plugins default value. Number of seconds to wait for DNS
105+
propagation.
106+
</small>
107+
</div>
108+
)}
109+
</Field>
110+
</>
111+
) : null}
112+
</div>
113+
);
114+
}

frontend/src/components/Form/SSLCertificateField.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { IconShield } from "@tabler/icons-react";
22
import { Field, useFormikContext } from "formik";
33
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
44
import type { Certificate } from "src/api/backend";
5-
import { useCertificates } from "src/hooks";
5+
import { useCertificates, useUser } from "src/hooks";
66
import { DateTimeFormat, intl } from "src/locale";
77

88
interface CertOption {
@@ -39,12 +39,27 @@ export function SSLCertificateField({
3939
required,
4040
allowNew,
4141
}: Props) {
42+
const { data: currentUser } = useUser("me");
4243
const { isLoading, isError, error, data } = useCertificates();
44+
const { values, setFieldValue } = useFormikContext();
45+
const v: any = values || {};
4346

44-
const { setFieldValue } = useFormikContext();
45-
46-
const handleChange = (v: any, _actionMeta: ActionMeta<CertOption>) => {
47-
setFieldValue(name, v?.value);
47+
const handleChange = (newValue: any, _actionMeta: ActionMeta<CertOption>) => {
48+
setFieldValue(name, newValue?.value);
49+
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, dnsChallenge, letsencryptEmail } = v;
50+
if (!newValue?.value) {
51+
sslForced && setFieldValue("sslForced", false);
52+
http2Support && setFieldValue("http2Support", false);
53+
hstsEnabled && setFieldValue("hstsEnabled", false);
54+
hstsSubdomains && setFieldValue("hstsSubdomains", false);
55+
}
56+
if (newValue?.value === "new") {
57+
if (!letsencryptEmail) {
58+
setFieldValue("letsencryptEmail", currentUser?.email);
59+
}
60+
} else {
61+
dnsChallenge && setFieldValue("dnsChallenge", false);
62+
}
4863
};
4964

5065
const options: CertOption[] =
@@ -61,7 +76,7 @@ export function SSLCertificateField({
6176
if (allowNew) {
6277
options?.unshift({
6378
value: "new",
64-
label: "Request a new HTTP certificate",
79+
label: "Request a new Certificate",
6580
subLabel: "with Let's Encrypt",
6681
icon: <IconShield size={14} className="text-lime" />,
6782
});

0 commit comments

Comments
 (0)