ソースを参照

Moved certrbot plugin list to backend

frontend doesn't include when building in react version
adds swagger for existing dns-providers endpoint
Jamie Curnow 1 ヶ月 前
コミット
5b7013b8d5

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 .DS_Store
 .DS_Store
 .idea
 .idea
+.qodo
 ._*
 ._*
 .vscode
 .vscode
 certbot-help.txt
 certbot-help.txt

+ 1 - 1
global/README.md → backend/certbot/README.md

@@ -1,4 +1,4 @@
-# certbot-dns-plugins
+# Certbot dns-plugins
 
 
 This file contains info about available Certbot DNS plugins.
 This file contains info about available Certbot DNS plugins.
 This only works for plugins which use the standard argument structure, so:
 This only works for plugins which use the standard argument structure, so:

+ 0 - 0
global/certbot-dns-plugins.json → backend/certbot/dns-plugins.json


+ 334 - 195
backend/internal/certificate.js

@@ -1,11 +1,11 @@
 import fs from "node:fs";
 import fs from "node:fs";
 import https from "node:https";
 import https from "node:https";
-import path from "path";
 import archiver from "archiver";
 import archiver from "archiver";
 import _ from "lodash";
 import _ from "lodash";
 import moment from "moment";
 import moment from "moment";
+import path from "path";
 import tempWrite from "temp-write";
 import tempWrite from "temp-write";
-import dnsPlugins from "../global/certbot-dns-plugins.json" with { type: "json" };
+import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
 import { installPlugin } from "../lib/certbot.js";
 import { installPlugin } from "../lib/certbot.js";
 import { useLetsencryptServer, useLetsencryptStaging } from "../lib/config.js";
 import { useLetsencryptServer, useLetsencryptStaging } from "../lib/config.js";
 import error from "../lib/error.js";
 import error from "../lib/error.js";
@@ -26,7 +26,11 @@ const omissions = () => {
 };
 };
 
 
 const internalCertificate = {
 const internalCertificate = {
-	allowedSslFiles: ["certificate", "certificate_key", "intermediate_certificate"],
+	allowedSslFiles: [
+		"certificate",
+		"certificate_key",
+		"intermediate_certificate",
+	],
 	intervalTimeout: 1000 * 60 * 60, // 1 hour
 	intervalTimeout: 1000 * 60 * 60, // 1 hour
 	interval: null,
 	interval: null,
 	intervalProcessing: false,
 	intervalProcessing: false,
@@ -53,7 +57,10 @@ const internalCertificate = {
 			);
 			);
 
 
 			const expirationThreshold = moment()
 			const expirationThreshold = moment()
-				.add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1])
+				.add(
+					internalCertificate.renewBeforeExpirationBy[0],
+					internalCertificate.renewBeforeExpirationBy[1],
+				)
 				.format("YYYY-MM-DD HH:mm:ss");
 				.format("YYYY-MM-DD HH:mm:ss");
 
 
 			// Fetch all the letsencrypt certs from the db that will expire within the configured threshold
 			// Fetch all the letsencrypt certs from the db that will expire within the configured threshold
@@ -119,91 +126,115 @@ const internalCertificate = {
 			data.nice_name = data.domain_names.join(", ");
 			data.nice_name = data.domain_names.join(", ");
 		}
 		}
 
 
-		const certificate = await certificateModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
+		// this command really should clean up and delete the cert if it can't fully succeed
+		const certificate = await certificateModel
+			.query()
+			.insertAndFetch(data)
+			.then(utils.omitRow(omissions()));
 
 
-		if (certificate.provider === "letsencrypt") {
-			// Request a new Cert from LE. Let the fun begin.
+		try {
+			if (certificate.provider === "letsencrypt") {
+				// Request a new Cert from LE. Let the fun begin.
 
 
-			// 1. Find out any hosts that are using any of the hostnames in this cert
-			// 2. Disable them in nginx temporarily
-			// 3. Generate the LE config
-			// 4. Request cert
-			// 5. Remove LE config
-			// 6. Re-instate previously disabled hosts
+				// 1. Find out any hosts that are using any of the hostnames in this cert
+				// 2. Disable them in nginx temporarily
+				// 3. Generate the LE config
+				// 4. Request cert
+				// 5. Remove LE config
+				// 6. Re-instate previously disabled hosts
 
 
-			// 1. Find out any hosts that are using any of the hostnames in this cert
-			const inUseResult = await internalHost.getHostsWithDomains(certificate.domain_names);
+				// 1. Find out any hosts that are using any of the hostnames in this cert
+				const inUseResult = await internalHost.getHostsWithDomains(
+					certificate.domain_names,
+				);
 
 
-			// 2. Disable them in nginx temporarily
-			await internalCertificate.disableInUseHosts(inUseResult);
+				// 2. Disable them in nginx temporarily
+				await internalCertificate.disableInUseHosts(inUseResult);
 
 
-			const user = await userModel.query().where("is_deleted", 0).andWhere("id", data.owner_user_id).first();
-			if (!user || !user.email) {
-				throw new error.ValidationError(
-					"A valid email address must be set on your user account to use Let's Encrypt",
-				);
-			}
+				const user = await userModel
+					.query()
+					.where("is_deleted", 0)
+					.andWhere("id", data.owner_user_id)
+					.first();
+				if (!user || !user.email) {
+					throw new error.ValidationError(
+						"A valid email address must be set on your user account to use Let's Encrypt",
+					);
+				}
 
 
-			// With DNS challenge no config is needed, so skip 3 and 5.
-			if (certificate.meta?.dns_challenge) {
-				try {
-					await internalNginx.reload();
-					// 4. Request cert
-					await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email);
-					await internalNginx.reload();
-					// 6. Re-instate previously disabled hosts
-					await internalCertificate.enableInUseHosts(inUseResult);
-				} catch (err) {
-					// In the event of failure, revert things and throw err back
-					await internalCertificate.enableInUseHosts(inUseResult);
-					await internalNginx.reload();
-					throw err;
+				// With DNS challenge no config is needed, so skip 3 and 5.
+				if (certificate.meta?.dns_challenge) {
+					try {
+						await internalNginx.reload();
+						// 4. Request cert
+						await internalCertificate.requestLetsEncryptSslWithDnsChallenge(
+							certificate,
+							user.email,
+						);
+						await internalNginx.reload();
+						// 6. Re-instate previously disabled hosts
+						await internalCertificate.enableInUseHosts(inUseResult);
+					} catch (err) {
+						// In the event of failure, revert things and throw err back
+						await internalCertificate.enableInUseHosts(inUseResult);
+						await internalNginx.reload();
+						throw err;
+					}
+				} else {
+					// 3. Generate the LE config
+					try {
+						await internalNginx.generateLetsEncryptRequestConfig(certificate);
+						await internalNginx.reload();
+						setTimeout(() => {}, 5000);
+						// 4. Request cert
+						await internalCertificate.requestLetsEncryptSsl(
+							certificate,
+							user.email,
+						);
+						// 5. Remove LE config
+						await internalNginx.deleteLetsEncryptRequestConfig(certificate);
+						await internalNginx.reload();
+						// 6. Re-instate previously disabled hosts
+						await internalCertificate.enableInUseHosts(inUseResult);
+					} catch (err) {
+						// In the event of failure, revert things and throw err back
+						await internalNginx.deleteLetsEncryptRequestConfig(certificate);
+						await internalCertificate.enableInUseHosts(inUseResult);
+						await internalNginx.reload();
+						throw err;
+					}
 				}
 				}
-			} else {
-				// 3. Generate the LE config
+
+				// At this point, the letsencrypt cert should exist on disk.
+				// Lets get the expiry date from the file and update the row silently
 				try {
 				try {
-					await internalNginx.generateLetsEncryptRequestConfig(certificate);
-					await internalNginx.reload();
-					setTimeout(() => {}, 5000);
-					// 4. Request cert
-					await internalCertificate.requestLetsEncryptSsl(certificate, user.email);
-					// 5. Remove LE config
-					await internalNginx.deleteLetsEncryptRequestConfig(certificate);
-					await internalNginx.reload();
-					// 6. Re-instate previously disabled hosts
-					await internalCertificate.enableInUseHosts(inUseResult);
+					const certInfo = await internalCertificate.getCertificateInfoFromFile(
+						`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
+					);
+					const savedRow = await certificateModel
+						.query()
+						.patchAndFetchById(certificate.id, {
+							expires_on: moment(certInfo.dates.to, "X").format(
+								"YYYY-MM-DD HH:mm:ss",
+							),
+						})
+						.then(utils.omitRow(omissions()));
+
+					// Add cert data for audit log
+					savedRow.meta = _.assign({}, savedRow.meta, {
+						letsencrypt_certificate: certInfo,
+					});
+					return savedRow;
 				} catch (err) {
 				} catch (err) {
-					// In the event of failure, revert things and throw err back
-					await internalNginx.deleteLetsEncryptRequestConfig(certificate);
-					await internalCertificate.enableInUseHosts(inUseResult);
-					await internalNginx.reload();
+					// Delete the certificate from the database if it was not created successfully
+					await certificateModel.query().deleteById(certificate.id);
 					throw err;
 					throw err;
 				}
 				}
 			}
 			}
-
-			// At this point, the letsencrypt cert should exist on disk.
-			// Lets get the expiry date from the file and update the row silently
-			try {
-				const certInfo = await internalCertificate.getCertificateInfoFromFile(
-					`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
-				);
-				const savedRow = await certificateModel
-					.query()
-					.patchAndFetchById(certificate.id, {
-						expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
-					})
-					.then(utils.omitRow(omissions()));
-
-				// Add cert data for audit log
-				savedRow.meta = _.assign({}, savedRow.meta, {
-					letsencrypt_certificate: certInfo,
-				});
-				return savedRow;
-			} catch (err) {
-				// Delete the certificate from the database if it was not created successfully
-				await certificateModel.query().deleteById(certificate.id);
-				throw err;
-			}
+		} catch (err) {
+			// Delete the certificate here. This is a hard delete, since it never existed properly
+			await certificateModel.query().deleteById(certificate.id);
+			throw err;
 		}
 		}
 
 
 		data.meta = _.assign({}, data.meta || {}, certificate.meta);
 		data.meta = _.assign({}, data.meta || {}, certificate.meta);
@@ -313,7 +344,9 @@ const internalCertificate = {
 		if (certificate.provider === "letsencrypt") {
 		if (certificate.provider === "letsencrypt") {
 			const zipDirectory = internalCertificate.getLiveCertPath(data.id);
 			const zipDirectory = internalCertificate.getLiveCertPath(data.id);
 			if (!fs.existsSync(zipDirectory)) {
 			if (!fs.existsSync(zipDirectory)) {
-				throw new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`);
+				throw new error.ItemNotFoundError(
+					`Certificate ${certificate.nice_name} does not exists`,
+				);
 			}
 			}
 
 
 			const certFiles = fs
 			const certFiles = fs
@@ -330,7 +363,9 @@ const internalCertificate = {
 				fileName: opName,
 				fileName: opName,
 			};
 			};
 		}
 		}
-		throw new error.ValidationError("Only Let'sEncrypt certificates can be downloaded");
+		throw new error.ValidationError(
+			"Only Let'sEncrypt certificates can be downloaded",
+		);
 	},
 	},
 
 
 	/**
 	/**
@@ -435,7 +470,10 @@ const internalCertificate = {
 	 * @returns {Promise}
 	 * @returns {Promise}
 	 */
 	 */
 	getCount: async (userId, visibility) => {
 	getCount: async (userId, visibility) => {
-		const query = certificateModel.query().count("id as count").where("is_deleted", 0);
+		const query = certificateModel
+			.query()
+			.count("id as count")
+			.where("is_deleted", 0);
 
 
 		if (visibility !== "all") {
 		if (visibility !== "all") {
 			query.andWhere("owner_user_id", userId);
 			query.andWhere("owner_user_id", userId);
@@ -483,13 +521,17 @@ const internalCertificate = {
 			});
 			});
 		}).then(() => {
 		}).then(() => {
 			return new Promise((resolve, reject) => {
 			return new Promise((resolve, reject) => {
-				fs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => {
-					if (err) {
-						reject(err);
-					} else {
-						resolve();
-					}
-				});
+				fs.writeFile(
+					`${dir}/privkey.pem`,
+					certificate.meta.certificate_key,
+					(err) => {
+						if (err) {
+							reject(err);
+						} else {
+							resolve();
+						}
+					},
+				);
 			});
 			});
 		});
 		});
 	},
 	},
@@ -562,7 +604,9 @@ const internalCertificate = {
 	upload: async (access, data) => {
 	upload: async (access, data) => {
 		const row = await internalCertificate.get(access, { id: data.id });
 		const row = await internalCertificate.get(access, { id: data.id });
 		if (row.provider !== "other") {
 		if (row.provider !== "other") {
-			throw new error.ValidationError("Cannot upload certificates for this type of provider");
+			throw new error.ValidationError(
+				"Cannot upload certificates for this type of provider",
+			);
 		}
 		}
 
 
 		const validations = await internalCertificate.validate(data);
 		const validations = await internalCertificate.validate(data);
@@ -578,7 +622,9 @@ const internalCertificate = {
 
 
 		const certificate = await internalCertificate.update(access, {
 		const certificate = await internalCertificate.update(access, {
 			id: data.id,
 			id: data.id,
-			expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
+			expires_on: moment(validations.certificate.dates.to, "X").format(
+				"YYYY-MM-DD HH:mm:ss",
+			),
 			domain_names: [validations.certificate.cn],
 			domain_names: [validations.certificate.cn],
 			meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
 			meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
 		});
 		});
@@ -603,7 +649,9 @@ const internalCertificate = {
 		}, 10000);
 		}, 10000);
 
 
 		try {
 		try {
-			const result = await utils.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `);
+			const result = await utils.exec(
+				`openssl pkey -in ${filepath} -check -noout 2>&1 `,
+			);
 			clearTimeout(failTimeout);
 			clearTimeout(failTimeout);
 			if (!result.toLowerCase().includes("key is valid")) {
 			if (!result.toLowerCase().includes("key is valid")) {
 				throw new error.ValidationError(`Result Validation Error: ${result}`);
 				throw new error.ValidationError(`Result Validation Error: ${result}`);
@@ -613,7 +661,10 @@ const internalCertificate = {
 		} catch (err) {
 		} catch (err) {
 			clearTimeout(failTimeout);
 			clearTimeout(failTimeout);
 			fs.unlinkSync(filepath);
 			fs.unlinkSync(filepath);
-			throw new error.ValidationError(`Certificate Key is not valid (${err.message})`, err);
+			throw new error.ValidationError(
+				`Certificate Key is not valid (${err.message})`,
+				err,
+			);
 		}
 		}
 	},
 	},
 
 
@@ -627,7 +678,10 @@ const internalCertificate = {
 	getCertificateInfo: async (certificate, throwExpired) => {
 	getCertificateInfo: async (certificate, throwExpired) => {
 		try {
 		try {
 			const filepath = await tempWrite(certificate, "/tmp");
 			const filepath = await tempWrite(certificate, "/tmp");
-			const certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired);
+			const certData = await internalCertificate.getCertificateInfoFromFile(
+				filepath,
+				throwExpired,
+			);
 			fs.unlinkSync(filepath);
 			fs.unlinkSync(filepath);
 			return certData;
 			return certData;
 		} catch (err) {
 		} catch (err) {
@@ -647,7 +701,13 @@ const internalCertificate = {
 		const certData = {};
 		const certData = {};
 
 
 		try {
 		try {
-			const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]);
+			const result = await utils.execFile("openssl", [
+				"x509",
+				"-in",
+				certificateFile,
+				"-subject",
+				"-noout",
+			]);
 			// Examples:
 			// Examples:
 			// subject=CN = *.jc21.com
 			// subject=CN = *.jc21.com
 			// subject=CN = something.example.com
 			// subject=CN = something.example.com
@@ -657,7 +717,13 @@ const internalCertificate = {
 				certData.cn = match[1];
 				certData.cn = match[1];
 			}
 			}
 
 
-			const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]);
+			const result2 = await utils.execFile("openssl", [
+				"x509",
+				"-in",
+				certificateFile,
+				"-issuer",
+				"-noout",
+			]);
 			// Examples:
 			// Examples:
 			// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
 			// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
 			// issuer=C = US, O = Let's Encrypt, CN = E5
 			// issuer=C = US, O = Let's Encrypt, CN = E5
@@ -668,7 +734,13 @@ const internalCertificate = {
 				certData.issuer = match2[1];
 				certData.issuer = match2[1];
 			}
 			}
 
 
-			const result3 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-dates", "-noout"]);
+			const result3 = await utils.execFile("openssl", [
+				"x509",
+				"-in",
+				certificateFile,
+				"-dates",
+				"-noout",
+			]);
 			// notBefore=Jul 14 04:04:29 2018 GMT
 			// notBefore=Jul 14 04:04:29 2018 GMT
 			// notAfter=Oct 12 04:04:29 2018 GMT
 			// notAfter=Oct 12 04:04:29 2018 GMT
 			let validFrom = null;
 			let validFrom = null;
@@ -680,7 +752,10 @@ const internalCertificate = {
 				const match = regex.exec(str.trim());
 				const match = regex.exec(str.trim());
 
 
 				if (match && typeof match[2] !== "undefined") {
 				if (match && typeof match[2] !== "undefined") {
-					const date = Number.parseInt(moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"), 10);
+					const date = Number.parseInt(
+						moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"),
+						10,
+					);
 
 
 					if (match[1].toLowerCase() === "notbefore") {
 					if (match[1].toLowerCase() === "notbefore") {
 						validFrom = date;
 						validFrom = date;
@@ -692,10 +767,15 @@ const internalCertificate = {
 			});
 			});
 
 
 			if (!validFrom || !validTo) {
 			if (!validFrom || !validTo) {
-				throw new error.ValidationError(`Could not determine dates from certificate: ${result}`);
+				throw new error.ValidationError(
+					`Could not determine dates from certificate: ${result}`,
+				);
 			}
 			}
 
 
-			if (throw_expired && validTo < Number.parseInt(moment().format("X"), 10)) {
+			if (
+				throw_expired &&
+				validTo < Number.parseInt(moment().format("X"), 10)
+			) {
 				throw new error.ValidationError("Certificate has expired");
 				throw new error.ValidationError("Certificate has expired");
 			}
 			}
 
 
@@ -706,7 +786,10 @@ const internalCertificate = {
 
 
 			return certData;
 			return certData;
 		} catch (err) {
 		} catch (err) {
-			throw new error.ValidationError(`Certificate is not valid (${err.message})`, err);
+			throw new error.ValidationError(
+				`Certificate is not valid (${err.message})`,
+				err,
+			);
 		}
 		}
 	},
 	},
 
 
@@ -787,7 +870,11 @@ const internalCertificate = {
 
 
 		const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
 		const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
 		fs.mkdirSync("/etc/letsencrypt/credentials", { recursive: true });
 		fs.mkdirSync("/etc/letsencrypt/credentials", { recursive: true });
-		fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, { mode: 0o600 });
+		fs.writeFileSync(
+			credentialsLocation,
+			certificate.meta.dns_provider_credentials,
+			{ mode: 0o600 },
+		);
 
 
 		// Whether the plugin has a --<name>-credentials argument
 		// Whether the plugin has a --<name>-credentials argument
 		const hasConfigArg = certificate.meta.dns_provider !== "route53";
 		const hasConfigArg = certificate.meta.dns_provider !== "route53";
@@ -812,7 +899,10 @@ const internalCertificate = {
 		];
 		];
 
 
 		if (hasConfigArg) {
 		if (hasConfigArg) {
-			args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation);
+			args.push(
+				`--${dnsPlugin.full_plugin_name}-credentials`,
+				credentialsLocation,
+			);
 		}
 		}
 		if (certificate.meta.propagation_seconds !== undefined) {
 		if (certificate.meta.propagation_seconds !== undefined) {
 			args.push(
 			args.push(
@@ -821,7 +911,10 @@ const internalCertificate = {
 			);
 			);
 		}
 		}
 
 
-		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
+		const adds = internalCertificate.getAdditionalCertbotArgs(
+			certificate.id,
+			certificate.meta.dns_provider,
+		);
 		args.push(...adds.args);
 		args.push(...adds.args);
 
 
 		logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
 		logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
@@ -857,9 +950,13 @@ const internalCertificate = {
 				`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
 				`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
 			);
 			);
 
 
-			const updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, {
-				expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
-			});
+			const updatedCertificate = await certificateModel
+				.query()
+				.patchAndFetchById(certificate.id, {
+					expires_on: moment(certInfo.dates.to, "X").format(
+						"YYYY-MM-DD HH:mm:ss",
+					),
+				});
 
 
 			// Add to audit log
 			// Add to audit log
 			await internalAuditLog.add(access, {
 			await internalAuditLog.add(access, {
@@ -869,7 +966,9 @@ const internalCertificate = {
 				meta: updatedCertificate,
 				meta: updatedCertificate,
 			});
 			});
 		} else {
 		} else {
-			throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed");
+			throw new error.ValidationError(
+				"Only Let'sEncrypt certificates can be renewed",
+			);
 		}
 		}
 	},
 	},
 
 
@@ -899,7 +998,10 @@ const internalCertificate = {
 			"--disable-hook-validation",
 			"--disable-hook-validation",
 		];
 		];
 
 
-		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
+		const adds = internalCertificate.getAdditionalCertbotArgs(
+			certificate.id,
+			certificate.meta.dns_provider,
+		);
 		args.push(...adds.args);
 		args.push(...adds.args);
 
 
 		logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
 		logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
@@ -938,7 +1040,10 @@ const internalCertificate = {
 			"--no-random-sleep-on-renew",
 			"--no-random-sleep-on-renew",
 		];
 		];
 
 
-		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
+		const adds = internalCertificate.getAdditionalCertbotArgs(
+			certificate.id,
+			certificate.meta.dns_provider,
+		);
 		args.push(...adds.args);
 		args.push(...adds.args);
 
 
 		logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
 		logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
@@ -978,7 +1083,9 @@ const internalCertificate = {
 
 
 		try {
 		try {
 			const result = await utils.execFile(certbotCommand, args, adds.opts);
 			const result = await utils.execFile(certbotCommand, args, adds.opts);
-			await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`);
+			await utils.exec(
+				`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`,
+			);
 			logger.info(result);
 			logger.info(result);
 			return result;
 			return result;
 		} catch (err) {
 		} catch (err) {
@@ -995,7 +1102,10 @@ const internalCertificate = {
 	 */
 	 */
 	hasLetsEncryptSslCerts: (certificate) => {
 	hasLetsEncryptSslCerts: (certificate) => {
 		const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id);
 		const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id);
-		return fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`);
+		return (
+			fs.existsSync(`${letsencryptPath}/fullchain.pem`) &&
+			fs.existsSync(`${letsencryptPath}/privkey.pem`)
+		);
 	},
 	},
 
 
 	/**
 	/**
@@ -1009,15 +1119,24 @@ const internalCertificate = {
 	disableInUseHosts: async (inUseResult) => {
 	disableInUseHosts: async (inUseResult) => {
 		if (inUseResult?.total_count) {
 		if (inUseResult?.total_count) {
 			if (inUseResult?.proxy_hosts.length) {
 			if (inUseResult?.proxy_hosts.length) {
-				await internalNginx.bulkDeleteConfigs("proxy_host", inUseResult.proxy_hosts);
+				await internalNginx.bulkDeleteConfigs(
+					"proxy_host",
+					inUseResult.proxy_hosts,
+				);
 			}
 			}
 
 
 			if (inUseResult?.redirection_hosts.length) {
 			if (inUseResult?.redirection_hosts.length) {
-				await internalNginx.bulkDeleteConfigs("redirection_host", inUseResult.redirection_hosts);
+				await internalNginx.bulkDeleteConfigs(
+					"redirection_host",
+					inUseResult.redirection_hosts,
+				);
 			}
 			}
 
 
 			if (inUseResult?.dead_hosts.length) {
 			if (inUseResult?.dead_hosts.length) {
-				await internalNginx.bulkDeleteConfigs("dead_host", inUseResult.dead_hosts);
+				await internalNginx.bulkDeleteConfigs(
+					"dead_host",
+					inUseResult.dead_hosts,
+				);
 			}
 			}
 		}
 		}
 	},
 	},
@@ -1033,50 +1152,73 @@ const internalCertificate = {
 	enableInUseHosts: async (inUseResult) => {
 	enableInUseHosts: async (inUseResult) => {
 		if (inUseResult.total_count) {
 		if (inUseResult.total_count) {
 			if (inUseResult.proxy_hosts.length) {
 			if (inUseResult.proxy_hosts.length) {
-				await internalNginx.bulkGenerateConfigs("proxy_host", inUseResult.proxy_hosts);
+				await internalNginx.bulkGenerateConfigs(
+					"proxy_host",
+					inUseResult.proxy_hosts,
+				);
 			}
 			}
 
 
 			if (inUseResult.redirection_hosts.length) {
 			if (inUseResult.redirection_hosts.length) {
-				await internalNginx.bulkGenerateConfigs("redirection_host", inUseResult.redirection_hosts);
+				await internalNginx.bulkGenerateConfigs(
+					"redirection_host",
+					inUseResult.redirection_hosts,
+				);
 			}
 			}
 
 
 			if (inUseResult.dead_hosts.length) {
 			if (inUseResult.dead_hosts.length) {
-				await internalNginx.bulkGenerateConfigs("dead_host", inUseResult.dead_hosts);
+				await internalNginx.bulkGenerateConfigs(
+					"dead_host",
+					inUseResult.dead_hosts,
+				);
 			}
 			}
 		}
 		}
 	},
 	},
 
 
-	testHttpsChallenge: async (access, domains) => {
+	/**
+	 *
+	 * @param   {Object}    payload
+	 * @param   {string[]}  payload.domains
+	 * @returns
+	 */
+	testHttpsChallenge: async (access, payload) => {
 		await access.can("certificates:list");
 		await access.can("certificates:list");
 
 
-		if (!isArray(domains)) {
-			throw new error.InternalValidationError("Domains must be an array of strings");
-		}
-		if (domains.length === 0) {
-			throw new error.InternalValidationError("No domains provided");
-		}
-
 		// Create a test challenge file
 		// Create a test challenge file
-		const testChallengeDir = "/data/letsencrypt-acme-challenge/.well-known/acme-challenge";
+		const testChallengeDir =
+			"/data/letsencrypt-acme-challenge/.well-known/acme-challenge";
 		const testChallengeFile = `${testChallengeDir}/test-challenge`;
 		const testChallengeFile = `${testChallengeDir}/test-challenge`;
 		fs.mkdirSync(testChallengeDir, { recursive: true });
 		fs.mkdirSync(testChallengeDir, { recursive: true });
 		fs.writeFileSync(testChallengeFile, "Success", { encoding: "utf8" });
 		fs.writeFileSync(testChallengeFile, "Success", { encoding: "utf8" });
 
 
-		async function performTestForDomain(domain) {
-			logger.info(`Testing http challenge for ${domain}`);
-			const url = `http://${domain}/.well-known/acme-challenge/test-challenge`;
-			const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;
-			const options = {
-				method: "POST",
-				headers: {
-					"User-Agent": "Mozilla/5.0",
-					"Content-Type": "application/x-www-form-urlencoded",
-					"Content-Length": Buffer.byteLength(formBody),
-				},
-			};
+		const results = {};
+		for (const domain of payload.domains) {
+			results[domain] = await internalCertificate.performTestForDomain(domain);
+		}
+
+		// Remove the test challenge file
+		fs.unlinkSync(testChallengeFile);
+
+		return results;
+	},
 
 
-			const result = await new Promise((resolve) => {
-				const req = https.request("https://www.site24x7.com/tools/restapi-tester", options, (res) => {
+	performTestForDomain: async (domain) => {
+		logger.info(`Testing http challenge for ${domain}`);
+		const url = `http://${domain}/.well-known/acme-challenge/test-challenge`;
+		const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;
+		const options = {
+			method: "POST",
+			headers: {
+				"User-Agent": "Mozilla/5.0",
+				"Content-Type": "application/x-www-form-urlencoded",
+				"Content-Length": Buffer.byteLength(formBody),
+			},
+		};
+
+		const result = await new Promise((resolve) => {
+			const req = https.request(
+				"https://www.site24x7.com/tools/restapi-tester",
+				options,
+				(res) => {
 					let responseBody = "";
 					let responseBody = "";
 
 
 					res.on("data", (chunk) => {
 					res.on("data", (chunk) => {
@@ -1107,69 +1249,66 @@ const internalCertificate = {
 							resolve(undefined);
 							resolve(undefined);
 						}
 						}
 					});
 					});
-				});
+				},
+			);
 
 
-				// Make sure to write the request body.
-				req.write(formBody);
-				req.end();
-				req.on("error", (e) => {
-					logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e);
-					resolve(undefined);
-				});
+			// Make sure to write the request body.
+			req.write(formBody);
+			req.end();
+			req.on("error", (e) => {
+				logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e);
+				resolve(undefined);
 			});
 			});
+		});
 
 
-			if (!result) {
-				// Some error occurred while trying to get the data
-				return "failed";
-			}
-			if (result.error) {
-				logger.info(
-					`HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`,
-				);
-				return `other:${result.error.msg}`;
-			}
-			if (`${result.responsecode}` === "200" && result.htmlresponse === "Success") {
-				// Server exists and has responded with the correct data
-				return "ok";
-			}
-			if (`${result.responsecode}` === "200") {
-				// Server exists but has responded with wrong data
-				logger.info(
-					`HTTP challenge test failed for domain ${domain} because of invalid returned data:`,
-					result.htmlresponse,
-				);
-				return "wrong-data";
-			}
-			if (`${result.responsecode}` === "404") {
-				// Server exists but responded with a 404
-				logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`);
-				return "404";
-			}
-			if (
-				`${result.responsecode}` === "0" ||
-				(typeof result.reason === "string" && result.reason.toLowerCase() === "host unavailable")
-			) {
-				// Server does not exist at domain
-				logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`);
-				return "no-host";
-			}
-			// Other errors
+		if (!result) {
+			// Some error occurred while trying to get the data
+			return "failed";
+		}
+		if (result.error) {
 			logger.info(
 			logger.info(
-				`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`,
+				`HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`,
 			);
 			);
-			return `other:${result.responsecode}`;
+			return `other:${result.error.msg}`;
 		}
 		}
-
-		const results = {};
-
-		for (const domain of domains) {
-			results[domain] = await performTestForDomain(domain);
+		if (
+			`${result.responsecode}` === "200" &&
+			result.htmlresponse === "Success"
+		) {
+			// Server exists and has responded with the correct data
+			return "ok";
 		}
 		}
-
-		// Remove the test challenge file
-		fs.unlinkSync(testChallengeFile);
-
-		return results;
+		if (`${result.responsecode}` === "200") {
+			// Server exists but has responded with wrong data
+			logger.info(
+				`HTTP challenge test failed for domain ${domain} because of invalid returned data:`,
+				result.htmlresponse,
+			);
+			return "wrong-data";
+		}
+		if (`${result.responsecode}` === "404") {
+			// Server exists but responded with a 404
+			logger.info(
+				`HTTP challenge test failed for domain ${domain} because code 404 was returned`,
+			);
+			return "404";
+		}
+		if (
+			`${result.responsecode}` === "0" ||
+			(typeof result.reason === "string" &&
+				result.reason.toLowerCase() === "host unavailable")
+		) {
+			// Server does not exist at domain
+			logger.info(
+				`HTTP challenge test failed for domain ${domain} the host was not found`,
+			);
+			return "no-host";
+		}
+		// Other errors
+		logger.info(
+			`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`,
+		);
+		return `other:${result.responsecode}`;
 	},
 	},
 
 
 	getAdditionalCertbotArgs: (certificate_id, dns_provider) => {
 	getAdditionalCertbotArgs: (certificate_id, dns_provider) => {

+ 2 - 2
backend/lib/certbot.js

@@ -1,5 +1,5 @@
 import batchflow from "batchflow";
 import batchflow from "batchflow";
-import dnsPlugins from "../global/certbot-dns-plugins.json" with { type: "json" };
+import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
 import { certbot as logger } from "../logger.js";
 import { certbot as logger } from "../logger.js";
 import errs from "./error.js";
 import errs from "./error.js";
 import utils from "./utils.js";
 import utils from "./utils.js";
@@ -8,7 +8,7 @@ const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0
 
 
 /**
 /**
  * Installs a cerbot plugin given the key for the object from
  * Installs a cerbot plugin given the key for the object from
- * ../global/certbot-dns-plugins.json
+ * ../certbot/dns-plugins.json
  *
  *
  * @param   {string}  pluginKey
  * @param   {string}  pluginKey
  * @returns {Object}
  * @returns {Object}

+ 6 - 1
backend/lib/validator/api.js

@@ -24,16 +24,21 @@ const apiValidator = async (schema, payload /*, description*/) => {
 		throw new errs.ValidationError("Payload is undefined");
 		throw new errs.ValidationError("Payload is undefined");
 	}
 	}
 
 
+
 	const validate = ajv.compile(schema);
 	const validate = ajv.compile(schema);
+
 	const valid = validate(payload);
 	const valid = validate(payload);
 
 
+
 	if (valid && !validate.errors) {
 	if (valid && !validate.errors) {
 		return payload;
 		return payload;
 	}
 	}
 
 
+
+
 	const message = ajv.errorsText(validate.errors);
 	const message = ajv.errorsText(validate.errors);
 	const err = new errs.ValidationError(message);
 	const err = new errs.ValidationError(message);
-	err.debug = [validate.errors, payload];
+	err.debug = {validationErrors: validate.errors, payload};
 	throw err;
 	throw err;
 };
 };
 
 

+ 31 - 15
backend/routes/nginx/certificates.js

@@ -1,5 +1,5 @@
 import express from "express";
 import express from "express";
-import dnsPlugins from "../../global/certbot-dns-plugins.json" with { type: "json" };
+import dnsPlugins from "../../certbot/dns-plugins.json" with { type: "json" };
 import internalCertificate from "../../internal/certificate.js";
 import internalCertificate from "../../internal/certificate.js";
 import errs from "../../lib/error.js";
 import errs from "../../lib/error.js";
 import jwtdecode from "../../lib/express/jwt-decode.js";
 import jwtdecode from "../../lib/express/jwt-decode.js";
@@ -44,11 +44,18 @@ router
 					},
 					},
 				},
 				},
 				{
 				{
-					expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
+					expand:
+						typeof req.query.expand === "string"
+							? req.query.expand.split(",")
+							: null,
 					query: typeof req.query.query === "string" ? req.query.query : null,
 					query: typeof req.query.query === "string" ? req.query.query : null,
 				},
 				},
 			);
 			);
-			const rows = await internalCertificate.getAll(res.locals.access, data.expand, data.query);
+			const rows = await internalCertificate.getAll(
+				res.locals.access,
+				data.expand,
+				data.query,
+			);
 			res.status(200).send(rows);
 			res.status(200).send(rows);
 		} catch (err) {
 		} catch (err) {
 			logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
 			logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -63,9 +70,15 @@ router
 	 */
 	 */
 	.post(async (req, res, next) => {
 	.post(async (req, res, next) => {
 		try {
 		try {
-			const payload = await apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body);
+			const payload = await apiValidator(
+				getValidationSchema("/nginx/certificates", "post"),
+				req.body,
+			);
 			req.setTimeout(900000); // 15 minutes timeout
 			req.setTimeout(900000); // 15 minutes timeout
-			const result = await internalCertificate.create(res.locals.access, payload);
+			const result = await internalCertificate.create(
+				res.locals.access,
+				payload,
+			);
 			res.status(201).send(result);
 			res.status(201).send(result);
 		} catch (err) {
 		} catch (err) {
 			logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
 			logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -120,20 +133,21 @@ router
 	.all(jwtdecode())
 	.all(jwtdecode())
 
 
 	/**
 	/**
-	 * GET /api/nginx/certificates/test-http
+	 * POST /api/nginx/certificates/test-http
 	 *
 	 *
 	 * Test HTTP challenge for domains
 	 * Test HTTP challenge for domains
 	 */
 	 */
-	.get(async (req, res, next) => {
-		if (req.query.domains === undefined) {
-			next(new errs.ValidationError("Domains are required as query parameters"));
-			return;
-		}
-
+	.post(async (req, res, next) => {
 		try {
 		try {
+			const payload = await apiValidator(
+				getValidationSchema("/nginx/certificates/test-http", "post"),
+				req.body,
+			);
+			req.setTimeout(60000); // 1 minute timeout
+
 			const result = await internalCertificate.testHttpsChallenge(
 			const result = await internalCertificate.testHttpsChallenge(
 				res.locals.access,
 				res.locals.access,
-				JSON.parse(req.query.domains),
+				payload,
 			);
 			);
 			res.status(200).send(result);
 			res.status(200).send(result);
 		} catch (err) {
 		} catch (err) {
@@ -142,7 +156,6 @@ router
 		}
 		}
 	});
 	});
 
 
-
 /**
 /**
  * Validate Certs before saving
  * Validate Certs before saving
  *
  *
@@ -211,7 +224,10 @@ router
 				},
 				},
 				{
 				{
 					certificate_id: req.params.certificate_id,
 					certificate_id: req.params.certificate_id,
-					expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
+					expand:
+						typeof req.query.expand === "string"
+							? req.query.expand.split(",")
+							: null,
 				},
 				},
 			);
 			);
 			const row = await internalCertificate.get(res.locals.access, {
 			const row = await internalCertificate.get(res.locals.access, {

+ 1 - 1
backend/routes/nginx/proxy_hosts.js

@@ -65,7 +65,7 @@ router
 			const result = await internalProxyHost.create(res.locals.access, payload);
 			const result = await internalProxyHost.create(res.locals.access, payload);
 			res.status(201).send(result);
 			res.status(201).send(result);
 		} catch (err) {
 		} catch (err) {
-			logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
+			logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err} ${JSON.stringify(err.debug, null, 2)}`);
 			next(err);
 			next(err);
 		}
 		}
 	});
 	});

+ 23 - 0
backend/schema/components/dns-providers-list.json

@@ -0,0 +1,23 @@
+{
+	"type": "array",
+	"description": "DNS Providers list",
+	"items": {
+		"type": "object",
+		"required": ["id", "name", "credentials"],
+		"additionalProperties": false,
+		"properties": {
+			"id": {
+				"type": "string",
+				"description": "Unique identifier for the DNS provider, matching the python package"
+			},
+			"name": {
+				"type": "string",
+				"description": "Human-readable name of the DNS provider"
+			},
+			"credentials": {
+				"type": "string",
+				"description": "Instructions on how to format the credentials for this DNS provider"
+			}
+		}
+	}
+}

+ 52 - 0
backend/schema/paths/nginx/certificates/dns-providers/get.json

@@ -0,0 +1,52 @@
+{
+	"operationId": "getDNSProviders",
+	"summary": "Get DNS Providers for Certificates",
+	"tags": [
+		"Certificates"
+	],
+	"security": [
+		{
+			"BearerAuth": [
+				"certificates"
+			]
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": "vultr",
+									"name": "Vultr",
+									"credentials": "dns_vultr_key = YOUR_VULTR_API_KEY"
+								},
+								{
+									"id": "websupport",
+									"name": "Websupport.sk",
+									"credentials": "dns_websupport_identifier = <api_key>\ndns_websupport_secret_key = <secret>"
+								},
+								{
+									"id": "wedos",
+									"name": "Wedos",
+									"credentials": "dns_wedos_user = <wedos_registration>\ndns_wedos_auth = <wapi_password>"
+								},
+								{
+									"id": "zoneedit",
+									"name": "ZoneEdit",
+									"credentials": "dns_zoneedit_user = <login-user-id>\ndns_zoneedit_token = <dyn-authentication-token>"
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/dns-providers-list.json"
+					}
+				}
+			}
+		}
+	}
+}

+ 16 - 10
backend/schema/paths/nginx/certificates/test-http/get.json → backend/schema/paths/nginx/certificates/test-http/post.json

@@ -7,18 +7,24 @@
 			"BearerAuth": ["certificates"]
 			"BearerAuth": ["certificates"]
 		}
 		}
 	],
 	],
-	"parameters": [
-		{
-			"in": "query",
-			"name": "domains",
-			"description": "Expansions",
-			"required": true,
-			"schema": {
-				"type": "string",
-				"example": "[\"test.example.ord\",\"test.example.com\",\"nonexistent.example.com\"]"
+	"requestBody": {
+		"description": "Test Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["domains"],
+					"properties": {
+						"domains": {
+							"$ref": "../../../../common.json#/properties/domain_names"
+						}
+					}
+				}
 			}
 			}
 		}
 		}
-	],
+	},
 	"responses": {
 	"responses": {
 		"200": {
 		"200": {
 			"description": "200 response",
 			"description": "200 response",

+ 7 - 2
backend/schema/swagger.json

@@ -61,14 +61,19 @@
 				"$ref": "./paths/nginx/certificates/post.json"
 				"$ref": "./paths/nginx/certificates/post.json"
 			}
 			}
 		},
 		},
+		"/nginx/certificates/dns-providers": {
+			"get": {
+				"$ref": "./paths/nginx/certificates/dns-providers/get.json"
+			}
+		},
 		"/nginx/certificates/validate": {
 		"/nginx/certificates/validate": {
 			"post": {
 			"post": {
 				"$ref": "./paths/nginx/certificates/validate/post.json"
 				"$ref": "./paths/nginx/certificates/validate/post.json"
 			}
 			}
 		},
 		},
 		"/nginx/certificates/test-http": {
 		"/nginx/certificates/test-http": {
-			"get": {
-				"$ref": "./paths/nginx/certificates/test-http/get.json"
+			"post": {
+				"$ref": "./paths/nginx/certificates/test-http/post.json"
 			}
 			}
 		},
 		},
 		"/nginx/certificates/{certID}": {
 		"/nginx/certificates/{certID}": {

+ 13 - 8
backend/scripts/install-certbot-plugins

@@ -1,7 +1,7 @@
 #!/usr/bin/node
 #!/usr/bin/node
 
 
 // Usage:
 // Usage:
-//   Install all plugins defined in `certbot-dns-plugins.json`:
+//   Install all plugins defined in `../certbot/dns-plugins.json`:
 //    ./install-certbot-plugins
 //    ./install-certbot-plugins
 //   Install one or more specific plugins:
 //   Install one or more specific plugins:
 //    ./install-certbot-plugins route53 cloudflare
 //    ./install-certbot-plugins route53 cloudflare
@@ -10,20 +10,21 @@
 //    docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c "/app/scripts/install-certbot-plugins"
 //    docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c "/app/scripts/install-certbot-plugins"
 //
 //
 
 
-import dnsPlugins from "../global/certbot-dns-plugins.json" with { type: "json" };
+import batchflow from "batchflow";
+import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
 import { installPlugin } from "../lib/certbot.js";
 import { installPlugin } from "../lib/certbot.js";
 import { certbot as logger } from "../logger.js";
 import { certbot as logger } from "../logger.js";
-import batchflow from "batchflow";
 
 
-let hasErrors      = false;
-let failingPlugins = [];
+let hasErrors = false;
+const failingPlugins = [];
 
 
 let pluginKeys = Object.keys(dnsPlugins);
 let pluginKeys = Object.keys(dnsPlugins);
 if (process.argv.length > 2) {
 if (process.argv.length > 2) {
 	pluginKeys = process.argv.slice(2);
 	pluginKeys = process.argv.slice(2);
 }
 }
 
 
-batchflow(pluginKeys).sequential()
+batchflow(pluginKeys)
+	.sequential()
 	.each((i, pluginKey, next) => {
 	.each((i, pluginKey, next) => {
 		installPlugin(pluginKey)
 		installPlugin(pluginKey)
 			.then(() => {
 			.then(() => {
@@ -40,10 +41,14 @@ batchflow(pluginKeys).sequential()
 	})
 	})
 	.end(() => {
 	.end(() => {
 		if (hasErrors) {
 		if (hasErrors) {
-			logger.error('Some plugins failed to install. Please check the logs above. Failing plugins: ' + '\n - ' + failingPlugins.join('\n - '));
+			logger.error(
+				"Some plugins failed to install. Please check the logs above. Failing plugins: " +
+					"\n - " +
+					failingPlugins.join("\n - "),
+			);
 			process.exit(1);
 			process.exit(1);
 		} else {
 		} else {
-			logger.complete('Plugins installed successfully');
+			logger.complete("Plugins installed successfully");
 			process.exit(0);
 			process.exit(0);
 		}
 		}
 	});
 	});

+ 0 - 1
docker/Dockerfile

@@ -39,7 +39,6 @@ EXPOSE 80 81 443
 
 
 COPY backend       /app
 COPY backend       /app
 COPY frontend/dist /app/frontend
 COPY frontend/dist /app/frontend
-COPY global        /app/global
 
 
 WORKDIR /app
 WORKDIR /app
 RUN yarn install \
 RUN yarn install \

+ 55 - 57
docker/docker-compose.dev.yml

@@ -1,6 +1,5 @@
 # WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.
 # WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.
 services:
 services:
-
   fullstack:
   fullstack:
     image: npm2dev:core
     image: npm2dev:core
     container_name: npm2dev.core
     container_name: npm2dev.core
@@ -23,9 +22,9 @@ services:
       PGID: 1000
       PGID: 1000
       FORCE_COLOR: 1
       FORCE_COLOR: 1
       # specifically for dev:
       # specifically for dev:
-      DEBUG: 'true'
-      DEVELOPMENT: 'true'
-      LE_STAGING: 'true'
+      DEBUG: "true"
+      DEVELOPMENT: "true"
+      LE_STAGING: "true"
       # db:
       # db:
       # DB_MYSQL_HOST: 'db'
       # DB_MYSQL_HOST: 'db'
       # DB_MYSQL_PORT: '3306'
       # DB_MYSQL_PORT: '3306'
@@ -33,26 +32,25 @@ services:
       # DB_MYSQL_PASSWORD: 'npm'
       # DB_MYSQL_PASSWORD: 'npm'
       # DB_MYSQL_NAME: 'npm'
       # DB_MYSQL_NAME: 'npm'
       # db-postgres:
       # db-postgres:
-      DB_POSTGRES_HOST: 'db-postgres'
-      DB_POSTGRES_PORT: '5432'
-      DB_POSTGRES_USER: 'npm'
-      DB_POSTGRES_PASSWORD: 'npmpass'
-      DB_POSTGRES_NAME: 'npm'
+      DB_POSTGRES_HOST: "db-postgres"
+      DB_POSTGRES_PORT: "5432"
+      DB_POSTGRES_USER: "npm"
+      DB_POSTGRES_PASSWORD: "npmpass"
+      DB_POSTGRES_NAME: "npm"
       # DB_SQLITE_FILE: "/data/database.sqlite"
       # DB_SQLITE_FILE: "/data/database.sqlite"
       # DISABLE_IPV6: "true"
       # DISABLE_IPV6: "true"
       # Required for DNS Certificate provisioning testing:
       # Required for DNS Certificate provisioning testing:
-      LE_SERVER: 'https://ca.internal/acme/acme/directory'
-      REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
+      LE_SERVER: "https://ca.internal/acme/acme/directory"
+      REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt"
     volumes:
     volumes:
       - npm_data:/data
       - npm_data:/data
       - le_data:/etc/letsencrypt
       - le_data:/etc/letsencrypt
-      - './dev/resolv.conf:/etc/resolv.conf:ro'
+      - "./dev/resolv.conf:/etc/resolv.conf:ro"
       - ../backend:/app
       - ../backend:/app
-      - ../frontend:/app/frontend
-      - ../global:/app/global
-      - '/etc/localtime:/etc/localtime:ro'
+      - ../frontend:/frontend
+      - "/etc/localtime:/etc/localtime:ro"
     healthcheck:
     healthcheck:
-      test: [ "CMD", "/usr/bin/check-health" ]
+      test: ["CMD", "/usr/bin/check-health"]
       interval: 10s
       interval: 10s
       timeout: 3s
       timeout: 3s
     depends_on:
     depends_on:
@@ -72,13 +70,13 @@ services:
       - nginx_proxy_manager
       - nginx_proxy_manager
     environment:
     environment:
       TZ: "${TZ:-Australia/Brisbane}"
       TZ: "${TZ:-Australia/Brisbane}"
-      MYSQL_ROOT_PASSWORD: 'npm'
-      MYSQL_DATABASE: 'npm'
-      MYSQL_USER: 'npm'
-      MYSQL_PASSWORD: 'npm'
+      MYSQL_ROOT_PASSWORD: "npm"
+      MYSQL_DATABASE: "npm"
+      MYSQL_USER: "npm"
+      MYSQL_PASSWORD: "npm"
     volumes:
     volumes:
       - db_data:/var/lib/mysql
       - db_data:/var/lib/mysql
-      - '/etc/localtime:/etc/localtime:ro'
+      - "/etc/localtime:/etc/localtime:ro"
 
 
   db-postgres:
   db-postgres:
     image: postgres:latest
     image: postgres:latest
@@ -86,9 +84,9 @@ services:
     networks:
     networks:
       - nginx_proxy_manager
       - nginx_proxy_manager
     environment:
     environment:
-      POSTGRES_USER: 'npm'
-      POSTGRES_PASSWORD: 'npmpass'
-      POSTGRES_DB: 'npm'
+      POSTGRES_USER: "npm"
+      POSTGRES_PASSWORD: "npmpass"
+      POSTGRES_DB: "npm"
     volumes:
     volumes:
       - psql_data:/var/lib/postgresql/data
       - psql_data:/var/lib/postgresql/data
       - ./ci/postgres:/docker-entrypoint-initdb.d
       - ./ci/postgres:/docker-entrypoint-initdb.d
@@ -97,8 +95,8 @@ services:
     image: jc21/testca
     image: jc21/testca
     container_name: npm2dev.stepca
     container_name: npm2dev.stepca
     volumes:
     volumes:
-      - './dev/resolv.conf:/etc/resolv.conf:ro'
-      - '/etc/localtime:/etc/localtime:ro'
+      - "./dev/resolv.conf:/etc/resolv.conf:ro"
+      - "/etc/localtime:/etc/localtime:ro"
     networks:
     networks:
       nginx_proxy_manager:
       nginx_proxy_manager:
         aliases:
         aliases:
@@ -119,7 +117,7 @@ services:
       - 3082:80
       - 3082:80
     environment:
     environment:
       URL: "http://npm:81/api/schema"
       URL: "http://npm:81/api/schema"
-      PORT: '80'
+      PORT: "80"
     depends_on:
     depends_on:
       - fullstack
       - fullstack
 
 
@@ -127,9 +125,9 @@ services:
     image: ubuntu/squid
     image: ubuntu/squid
     container_name: npm2dev.squid
     container_name: npm2dev.squid
     volumes:
     volumes:
-      - './dev/squid.conf:/etc/squid/squid.conf:ro'
-      - './dev/resolv.conf:/etc/resolv.conf:ro'
-      - '/etc/localtime:/etc/localtime:ro'
+      - "./dev/squid.conf:/etc/squid/squid.conf:ro"
+      - "./dev/resolv.conf:/etc/resolv.conf:ro"
+      - "/etc/localtime:/etc/localtime:ro"
     networks:
     networks:
       - nginx_proxy_manager
       - nginx_proxy_manager
     ports:
     ports:
@@ -139,18 +137,18 @@ services:
     image: pschiffe/pdns-mysql:4.8
     image: pschiffe/pdns-mysql:4.8
     container_name: npm2dev.pdns
     container_name: npm2dev.pdns
     volumes:
     volumes:
-      - '/etc/localtime:/etc/localtime:ro'
+      - "/etc/localtime:/etc/localtime:ro"
     environment:
     environment:
-      PDNS_master: 'yes'
-      PDNS_api: 'yes'
-      PDNS_api_key: 'npm'
-      PDNS_webserver: 'yes'
-      PDNS_webserver_address: '0.0.0.0'
-      PDNS_webserver_password: 'npm'
-      PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
-      PDNS_version_string: 'anonymous'
+      PDNS_master: "yes"
+      PDNS_api: "yes"
+      PDNS_api_key: "npm"
+      PDNS_webserver: "yes"
+      PDNS_webserver_address: "0.0.0.0"
+      PDNS_webserver_password: "npm"
+      PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
+      PDNS_version_string: "anonymous"
       PDNS_default_ttl: 1500
       PDNS_default_ttl: 1500
-      PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
+      PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
       PDNS_gmysql_host: pdns-db
       PDNS_gmysql_host: pdns-db
       PDNS_gmysql_port: 3306
       PDNS_gmysql_port: 3306
       PDNS_gmysql_user: pdns
       PDNS_gmysql_user: pdns
@@ -168,14 +166,14 @@ services:
     image: mariadb
     image: mariadb
     container_name: npm2dev.pdns-db
     container_name: npm2dev.pdns-db
     environment:
     environment:
-      MYSQL_ROOT_PASSWORD: 'pdns'
-      MYSQL_DATABASE: 'pdns'
-      MYSQL_USER: 'pdns'
-      MYSQL_PASSWORD: 'pdns'
+      MYSQL_ROOT_PASSWORD: "pdns"
+      MYSQL_DATABASE: "pdns"
+      MYSQL_USER: "pdns"
+      MYSQL_PASSWORD: "pdns"
     volumes:
     volumes:
-      - 'pdns_mysql:/var/lib/mysql'
-      - '/etc/localtime:/etc/localtime:ro'
-      - './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro'
+      - "pdns_mysql:/var/lib/mysql"
+      - "/etc/localtime:/etc/localtime:ro"
+      - "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro"
     networks:
     networks:
       - nginx_proxy_manager
       - nginx_proxy_manager
 
 
@@ -186,25 +184,25 @@ services:
       context: ../
       context: ../
       dockerfile: test/cypress/Dockerfile
       dockerfile: test/cypress/Dockerfile
     environment:
     environment:
-      HTTP_PROXY: 'squid:3128'
-      HTTPS_PROXY: 'squid:3128'
+      HTTP_PROXY: "squid:3128"
+      HTTPS_PROXY: "squid:3128"
     volumes:
     volumes:
-      - '../test/results:/results'
-      - './dev/resolv.conf:/etc/resolv.conf:ro'
-      - '/etc/localtime:/etc/localtime:ro'
+      - "../test/results:/results"
+      - "./dev/resolv.conf:/etc/resolv.conf:ro"
+      - "/etc/localtime:/etc/localtime:ro"
     command: cypress run --browser chrome --config-file=cypress/config/ci.js
     command: cypress run --browser chrome --config-file=cypress/config/ci.js
     networks:
     networks:
       - nginx_proxy_manager
       - nginx_proxy_manager
 
 
   authentik-redis:
   authentik-redis:
-    image: 'redis:alpine'
+    image: "redis:alpine"
     container_name: npm2dev.authentik-redis
     container_name: npm2dev.authentik-redis
     command: --save 60 1 --loglevel warning
     command: --save 60 1 --loglevel warning
     networks:
     networks:
       - nginx_proxy_manager
       - nginx_proxy_manager
     restart: unless-stopped
     restart: unless-stopped
     healthcheck:
     healthcheck:
-      test: [ 'CMD-SHELL', 'redis-cli ping | grep PONG' ]
+      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
       start_period: 20s
       start_period: 20s
       interval: 30s
       interval: 30s
       retries: 5
       retries: 5
@@ -246,9 +244,9 @@ services:
     networks:
     networks:
       - nginx_proxy_manager
       - nginx_proxy_manager
     environment:
     environment:
-      AUTHENTIK_HOST: 'http://authentik:9000'
-      AUTHENTIK_INSECURE: 'true'
-      AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp'
+      AUTHENTIK_HOST: "http://authentik:9000"
+      AUTHENTIK_INSECURE: "true"
+      AUTHENTIK_TOKEN: "wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp"
     restart: unless-stopped
     restart: unless-stopped
     depends_on:
     depends_on:
       - authentik
       - authentik

+ 3 - 3
docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run

@@ -7,11 +7,11 @@ set -e
 
 
 if [ "$DEVELOPMENT" = 'true' ]; then
 if [ "$DEVELOPMENT" = 'true' ]; then
 	. /usr/bin/common.sh
 	. /usr/bin/common.sh
-	cd /app/frontend || exit 1
+	cd /frontend || exit 1
 	HOME=$NPMHOME
 	HOME=$NPMHOME
 	export HOME
 	export HOME
-	mkdir -p /app/frontend/dist
-	chown -R "$PUID:$PGID" /app/frontend/dist
+	mkdir -p /frontend/dist
+	chown -R "$PUID:$PGID" /frontend/dist
 
 
 	log_info 'Starting frontend ...'
 	log_info 'Starting frontend ...'
 	s6-setuidgid "$PUID:$PGID" yarn install
 	s6-setuidgid "$PUID:$PGID" yarn install

+ 0 - 1
scripts/ci/frontend-build

@@ -15,7 +15,6 @@ if hash docker 2>/dev/null; then
 		-e CI=true \
 		-e CI=true \
 		-e NODE_OPTIONS=--openssl-legacy-provider \
 		-e NODE_OPTIONS=--openssl-legacy-provider \
 		-v "$(pwd)/frontend:/app/frontend" \
 		-v "$(pwd)/frontend:/app/frontend" \
-		-v "$(pwd)/global:/app/global" \
 		-w /app/frontend "${DOCKER_IMAGE}" \
 		-w /app/frontend "${DOCKER_IMAGE}" \
 		sh -c "yarn install && yarn lint && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
 		sh -c "yarn install && yarn lint && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
 
 

+ 0 - 1
scripts/ci/test-and-build

@@ -10,7 +10,6 @@ docker pull "${TESTING_IMAGE}"
 echo -e "${BLUE}❯ ${CYAN}Testing backend ...${RESET}"
 echo -e "${BLUE}❯ ${CYAN}Testing backend ...${RESET}"
 docker run --rm \
 docker run --rm \
 	-v "$(pwd)/backend:/app" \
 	-v "$(pwd)/backend:/app" \
-	-v "$(pwd)/global:/app/global" \
 	-w /app \
 	-w /app \
 	"${TESTING_IMAGE}" \
 	"${TESTING_IMAGE}" \
 	sh -c 'yarn install && yarn lint . && rm -rf node_modules'
 	sh -c 'yarn install && yarn lint . && rm -rf node_modules'