404 hosts add update complete, fix certbot renewals

and remove the need for email and agreement on cert requests
This commit is contained in:
Jamie Curnow
2025-09-23 18:02:00 +10:00
parent f39efb3e63
commit 4240e00a46
34 changed files with 936 additions and 448 deletions

View File

@@ -13,6 +13,7 @@ import utils from "../lib/utils.js";
import { ssl as logger } from "../logger.js"; import { ssl as logger } from "../logger.js";
import certificateModel from "../models/certificate.js"; import certificateModel from "../models/certificate.js";
import tokenModel from "../models/token.js"; import tokenModel from "../models/token.js";
import userModel from "../models/user.js";
import internalAuditLog from "./audit-log.js"; import internalAuditLog from "./audit-log.js";
import internalHost from "./host.js"; import internalHost from "./host.js";
import internalNginx from "./nginx.js"; import internalNginx from "./nginx.js";
@@ -81,7 +82,7 @@ const internalCertificate = {
Promise.resolve({ Promise.resolve({
permission_visibility: "all", permission_visibility: "all",
}), }),
token: new tokenModel(), token: tokenModel(),
}, },
{ id: certificate.id }, { id: certificate.id },
) )
@@ -118,10 +119,7 @@ const internalCertificate = {
data.nice_name = data.domain_names.join(", "); data.nice_name = data.domain_names.join(", ");
} }
const certificate = await certificateModel const certificate = await certificateModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
.query()
.insertAndFetch(data)
.then(utils.omitRow(omissions()));
if (certificate.provider === "letsencrypt") { if (certificate.provider === "letsencrypt") {
// Request a new Cert from LE. Let the fun begin. // Request a new Cert from LE. Let the fun begin.
@@ -139,12 +137,19 @@ const internalCertificate = {
// 2. Disable them in nginx temporarily // 2. Disable them in nginx temporarily
await internalCertificate.disableInUseHosts(inUseResult); 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",
);
}
// With DNS challenge no config is needed, so skip 3 and 5. // With DNS challenge no config is needed, so skip 3 and 5.
if (certificate.meta?.dns_challenge) { if (certificate.meta?.dns_challenge) {
try { try {
await internalNginx.reload(); await internalNginx.reload();
// 4. Request cert // 4. Request cert
await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate); await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email);
await internalNginx.reload(); await internalNginx.reload();
// 6. Re-instate previously disabled hosts // 6. Re-instate previously disabled hosts
await internalCertificate.enableInUseHosts(inUseResult); await internalCertificate.enableInUseHosts(inUseResult);
@@ -159,9 +164,9 @@ const internalCertificate = {
try { try {
await internalNginx.generateLetsEncryptRequestConfig(certificate); await internalNginx.generateLetsEncryptRequestConfig(certificate);
await internalNginx.reload(); await internalNginx.reload();
setTimeout(() => {}, 5000) setTimeout(() => {}, 5000);
// 4. Request cert // 4. Request cert
await internalCertificate.requestLetsEncryptSsl(certificate); await internalCertificate.requestLetsEncryptSsl(certificate, user.email);
// 5. Remove LE config // 5. Remove LE config
await internalNginx.deleteLetsEncryptRequestConfig(certificate); await internalNginx.deleteLetsEncryptRequestConfig(certificate);
await internalNginx.reload(); await internalNginx.reload();
@@ -204,13 +209,12 @@ const internalCertificate = {
data.meta = _.assign({}, data.meta || {}, certificate.meta); data.meta = _.assign({}, data.meta || {}, certificate.meta);
// Add to audit log // Add to audit log
await internalAuditLog await internalAuditLog.add(access, {
.add(access, { action: "created",
action: "created", object_type: "certificate",
object_type: "certificate", object_id: certificate.id,
object_id: certificate.id, meta: data,
meta: data, });
});
return certificate; return certificate;
}, },
@@ -248,13 +252,12 @@ const internalCertificate = {
} }
// Add to audit log // Add to audit log
await internalAuditLog await internalAuditLog.add(access, {
.add(access, { action: "updated",
action: "updated", object_type: "certificate",
object_type: "certificate", object_id: row.id,
object_id: row.id, meta: _.omit(data, ["expires_on"]), // this prevents json circular reference because expires_on might be raw
meta: _.omit(data, ["expires_on"]), // this prevents json circular reference because expires_on might be raw });
});
return savedRow; return savedRow;
}, },
@@ -268,7 +271,7 @@ const internalCertificate = {
* @return {Promise} * @return {Promise}
*/ */
get: async (access, data) => { get: async (access, data) => {
const accessData = await access.can("certificates:get", data.id) const accessData = await access.can("certificates:get", data.id);
const query = certificateModel const query = certificateModel
.query() .query()
.where("is_deleted", 0) .where("is_deleted", 0)
@@ -367,12 +370,9 @@ const internalCertificate = {
throw new error.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} }
await certificateModel await certificateModel.query().where("id", row.id).patch({
.query() is_deleted: 1,
.where("id", row.id) });
.patch({
is_deleted: 1,
});
// Add to audit log // Add to audit log
row.meta = internalCertificate.cleanMeta(row.meta); row.meta = internalCertificate.cleanMeta(row.meta);
@@ -435,10 +435,7 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: async (userId, visibility) => { getCount: async (userId, visibility) => {
const query = certificateModel const query = certificateModel.query().count("id as count").where("is_deleted", 0);
.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);
@@ -501,12 +498,10 @@ const internalCertificate = {
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @param {Array} data.domain_names * @param {Array} data.domain_names
* @param {String} data.meta.letsencrypt_email
* @param {Boolean} data.meta.letsencrypt_agree
* @returns {Promise} * @returns {Promise}
*/ */
createQuickCertificate: async (access, data) => { createQuickCertificate: async (access, data) => {
return internalCertificate.create(access, { return await internalCertificate.create(access, {
provider: "letsencrypt", provider: "letsencrypt",
domain_names: data.domain_names, domain_names: data.domain_names,
meta: data.meta, meta: data.meta,
@@ -652,7 +647,7 @@ 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
@@ -739,9 +734,10 @@ const internalCertificate = {
/** /**
* Request a certificate using the http challenge * Request a certificate using the http challenge
* @param {Object} certificate the certificate row * @param {Object} certificate the certificate row
* @param {String} email the email address to use for registration
* @returns {Promise} * @returns {Promise}
*/ */
requestLetsEncryptSsl: async (certificate) => { requestLetsEncryptSsl: async (certificate, email) => {
logger.info( logger.info(
`Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`, `Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
); );
@@ -760,7 +756,7 @@ const internalCertificate = {
"--authenticator", "--authenticator",
"webroot", "webroot",
"--email", "--email",
certificate.meta.letsencrypt_email, email,
"--preferred-challenges", "--preferred-challenges",
"dns,http", "dns,http",
"--domains", "--domains",
@@ -779,9 +775,10 @@ const internalCertificate = {
/** /**
* @param {Object} certificate the certificate row * @param {Object} certificate the certificate row
* @param {String} email the email address to use for registration
* @returns {Promise} * @returns {Promise}
*/ */
requestLetsEncryptSslWithDnsChallenge: async (certificate) => { requestLetsEncryptSslWithDnsChallenge: async (certificate, email) => {
await installPlugin(certificate.meta.dns_provider); await installPlugin(certificate.meta.dns_provider);
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
logger.info( logger.info(
@@ -807,7 +804,7 @@ const internalCertificate = {
`npm-${certificate.id}`, `npm-${certificate.id}`,
"--agree-tos", "--agree-tos",
"--email", "--email",
certificate.meta.letsencrypt_email, email,
"--domains", "--domains",
certificate.domain_names.join(","), certificate.domain_names.join(","),
"--authenticator", "--authenticator",
@@ -847,7 +844,7 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
renew: async (access, data) => { renew: async (access, data) => {
await access.can("certificates:update", data) await access.can("certificates:update", data);
const certificate = await internalCertificate.get(access, data); const certificate = await internalCertificate.get(access, data);
if (certificate.provider === "letsencrypt") { if (certificate.provider === "letsencrypt") {
@@ -860,11 +857,9 @@ const internalCertificate = {
`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
); );
const updatedCertificate = await certificateModel const updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, {
.query() expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
.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, {
@@ -1159,7 +1154,9 @@ const internalCertificate = {
return "no-host"; return "no-host";
} }
// Other errors // Other errors
logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`); logger.info(
`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`,
);
return `other:${result.responsecode}`; return `other:${result.responsecode}`;
} }
@@ -1201,7 +1198,7 @@ const internalCertificate = {
getLiveCertPath: (certificateId) => { getLiveCertPath: (certificateId) => {
return `/etc/letsencrypt/live/npm-${certificateId}`; return `/etc/letsencrypt/live/npm-${certificateId}`;
} },
}; };
export default internalCertificate; export default internalCertificate;

View File

@@ -54,10 +54,21 @@ const internalDeadHost = {
thisData.advanced_config = ""; thisData.advanced_config = "";
} }
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions())); const row = await deadHostModel.query()
.insertAndFetch(thisData)
.then(utils.omitRow(omissions()));
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "dead-host",
object_id: row.id,
meta: _.assign({}, data.meta || {}, row.meta),
});
if (createCertificate) { if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, data); const cert = await internalCertificate.createQuickCertificate(access, data);
// update host with cert id // update host with cert id
await internalDeadHost.update(access, { await internalDeadHost.update(access, {
id: row.id, id: row.id,
@@ -71,17 +82,13 @@ const internalDeadHost = {
expand: ["certificate", "owner"], expand: ["certificate", "owner"],
}); });
// Sanity check
if (createCertificate && !freshRow.certificate_id) {
throw new errs.InternalValidationError("The host was created but the Certificate creation failed.");
}
// Configure nginx // Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", freshRow); await internalNginx.configure(deadHostModel, "dead_host", freshRow);
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "dead-host",
object_id: freshRow.id,
meta: data,
});
return freshRow; return freshRow;
}, },
@@ -94,7 +101,6 @@ const internalDeadHost = {
*/ */
update: async (access, data) => { update: async (access, data) => {
const createCertificate = data.certificate_id === "new"; const createCertificate = data.certificate_id === "new";
if (createCertificate) { if (createCertificate) {
delete data.certificate_id; delete data.certificate_id;
} }
@@ -147,6 +153,13 @@ const internalDeadHost = {
thisData = internalHost.cleanSslHstsData(thisData, row); thisData = internalHost.cleanSslHstsData(thisData, row);
// do the row update
await deadHostModel
.query()
.where({id: data.id})
.patch(data);
// Add to audit log // Add to audit log
await internalAuditLog.add(access, { await internalAuditLog.add(access, {
action: "updated", action: "updated",

View File

@@ -6,46 +6,6 @@ import utils from "./utils.js";
const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')"; const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')";
/**
* @param {array} pluginKeys
*/
const installPlugins = async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
batchflow(pluginKeys)
.sequential()
.each((_i, pluginKey, next) => {
certbot
.installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
);
} else {
resolve();
}
});
});
};
/** /**
* 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 * ../global/certbot-dns-plugins.json
@@ -84,4 +44,43 @@ const installPlugin = async (pluginKey) => {
}); });
}; };
/**
* @param {array} pluginKeys
*/
const installPlugins = async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
batchflow(pluginKeys)
.sequential()
.each((_i, pluginKey, next) => {
installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
);
} else {
resolve();
}
});
});
};
export { installPlugins, installPlugin }; export { installPlugins, installPlugin };

View File

@@ -98,6 +98,8 @@ router
name: dnsPlugins[key].name, name: dnsPlugins[key].name,
credentials: dnsPlugins[key].credentials, credentials: dnsPlugins[key].credentials,
})); }));
clean.sort((a, b) => a.name.localeCompare(b.name));
res.status(200).send(clean); res.status(200).send(clean);
} catch (err) { } catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);

View File

@@ -62,15 +62,9 @@
"dns_provider_credentials": { "dns_provider_credentials": {
"type": "string" "type": "string"
}, },
"letsencrypt_agree": {
"type": "boolean"
},
"letsencrypt_certificate": { "letsencrypt_certificate": {
"type": "object" "type": "object"
}, },
"letsencrypt_email": {
"$ref": "../common.json#/properties/email"
},
"propagation_seconds": { "propagation_seconds": {
"type": "integer", "type": "integer",
"minimum": 0 "minimum": 0

View File

@@ -36,8 +36,6 @@
"domain_names": ["test.example.com"], "domain_names": ["test.example.com"],
"expires_on": "2025-01-07T04:34:18.000Z", "expires_on": "2025-01-07T04:34:18.000Z",
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false "dns_challenge": false
} }
} }

View File

@@ -37,8 +37,6 @@
"nice_name": "My Test Cert", "nice_name": "My Test Cert",
"domain_names": ["test.jc21.supernerd.pro"], "domain_names": ["test.jc21.supernerd.pro"],
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false "dns_challenge": false
} }
} }

View File

@@ -36,8 +36,6 @@
"domain_names": ["test.example.com"], "domain_names": ["test.example.com"],
"expires_on": "2025-01-07T04:34:18.000Z", "expires_on": "2025-01-07T04:34:18.000Z",
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false "dns_challenge": false
} }
} }

View File

@@ -52,8 +52,6 @@
"nice_name": "test.example.com", "nice_name": "test.example.com",
"domain_names": ["test.example.com"], "domain_names": ["test.example.com"],
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false, "dns_challenge": false,
"letsencrypt_certificate": { "letsencrypt_certificate": {
"cn": "test.example.com", "cn": "test.example.com",

View File

@@ -121,11 +121,13 @@ const setupCertbotPlugins = async () => {
// Make sure credentials file exists // Make sure credentials file exists
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
// Escape single quotes and backslashes // Escape single quotes and backslashes
const escapedCredentials = certificate.meta.dns_provider_credentials if (typeof certificate.meta.dns_provider_credentials === "string") {
.replaceAll("'", "\\'") const escapedCredentials = certificate.meta.dns_provider_credentials
.replaceAll("\\", "\\\\"); .replaceAll("'", "\\'")
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`; .replaceAll("\\", "\\\\");
promises.push(utils.exec(credentials_cmd)); const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;
promises.push(utils.exec(credentials_cmd));
}
} }
return true; return true;
}); });

View File

@@ -19,6 +19,7 @@
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.89.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@uiw/react-textarea-code-editor": "^3.1.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"country-flag-icons": "^1.5.20", "country-flag-icons": "^1.5.20",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",

View File

@@ -65,3 +65,8 @@
} }
} }
} }
.textareaMono {
font-family: 'Courier New', Courier, monospace !important;
resize: vertical;
}

View File

@@ -1,16 +1,8 @@
.dnsChallengeWarning { .dnsChallengeWarning {
border: 1px solid #fecaca; /* Tailwind's red-300 */ border: 1px solid var(--tblr-orange-lt);
padding: 1rem; padding: 1rem;
border-radius: 0.375rem; /* Tailwind's rounded-md */ border-radius: 0.375rem;
margin-top: 1rem; margin-top: 1rem;
background-color: var(--tblr-cyan-lt);
} }
.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;
}

View File

@@ -1,4 +1,3 @@
import cn from "classnames";
import { Field, useFormikContext } from "formik"; import { Field, useFormikContext } from "formik";
import { useState } from "react"; import { useState } from "react";
import Select, { type ActionMeta } from "react-select"; import Select, { type ActionMeta } from "react-select";
@@ -20,8 +19,8 @@ export function DNSProviderFields() {
const v: any = values || {}; const v: any = values || {};
const handleChange = (newValue: any, _actionMeta: ActionMeta<DNSProviderOption>) => { const handleChange = (newValue: any, _actionMeta: ActionMeta<DNSProviderOption>) => {
setFieldValue("dnsProvider", newValue?.value); setFieldValue("meta.dnsProvider", newValue?.value);
setFieldValue("dnsProviderCredentials", newValue?.credentials); setFieldValue("meta.dnsProviderCredentials", newValue?.credentials);
setDnsProviderId(newValue?.value); setDnsProviderId(newValue?.value);
}; };
@@ -34,12 +33,12 @@ export function DNSProviderFields() {
return ( return (
<div className={styles.dnsChallengeWarning}> <div className={styles.dnsChallengeWarning}>
<p className="text-danger"> <p className="text-info">
This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective This section requires some knowledge about Certbot and DNS plugins. Please consult the respective
plugins documentation. plugins documentation.
</p> </p>
<Field name="dnsProvider"> <Field name="meta.dnsProvider">
{({ field }: any) => ( {({ field }: any) => (
<div className="row"> <div className="row">
<label htmlFor="dnsProvider" className="form-label"> <label htmlFor="dnsProvider" className="form-label">
@@ -64,33 +63,37 @@ export function DNSProviderFields() {
{dnsProviderId ? ( {dnsProviderId ? (
<> <>
<Field name="dnsProviderCredentials"> <Field name="meta.dnsProviderCredentials">
{({ field }: any) => ( {({ field }: any) => (
<div className="row mt-3"> <div className="mt-3">
<label htmlFor="dnsProviderCredentials" className="form-label"> <label htmlFor="dnsProviderCredentials" className="form-label">
Credentials File Content Credentials File Content
</label> </label>
<textarea <textarea
id="dnsProviderCredentials" id="dnsProviderCredentials"
className={cn("form-control", styles.textareaMono)} className="form-control textareaMono"
rows={3} rows={3}
spellCheck={false} spellCheck={false}
value={v.dnsProviderCredentials || ""} value={v.meta.dnsProviderCredentials || ""}
{...field} {...field}
/> />
<small className="text-muted"> <div>
This plugin requires a configuration file containing an API token or other <small className="text-muted">
credentials to your provider This plugin requires a configuration file containing an API token or other
</small> credentials to your provider
<small className="text-danger"> </small>
This data will be stored as plaintext in the database and in a file! </div>
</small> <div>
<small className="text-danger">
This data will be stored as plaintext in the database and in a file!
</small>
</div>
</div> </div>
)} )}
</Field> </Field>
<Field name="propagationSeconds"> <Field name="meta.propagationSeconds">
{({ field }: any) => ( {({ field }: any) => (
<div className="row mt-3"> <div className="mt-3">
<label htmlFor="propagationSeconds" className="form-label"> <label htmlFor="propagationSeconds" className="form-label">
Propagation Seconds Propagation Seconds
</label> </label>

View File

@@ -2,6 +2,7 @@ import { Field, useFormikContext } from "formik";
import type { ActionMeta, MultiValue } from "react-select"; import type { ActionMeta, MultiValue } from "react-select";
import CreatableSelect from "react-select/creatable"; import CreatableSelect from "react-select/creatable";
import { intl } from "src/locale"; import { intl } from "src/locale";
import { validateDomain, validateDomains } from "src/modules/Validations";
export type SelectOption = { export type SelectOption = {
label: string; label: string;
@@ -22,17 +23,10 @@ export function DomainNamesField({
label = "domain-names", label = "domain-names",
id = "domainNames", id = "domainNames",
maxDomains, maxDomains,
isWildcardPermitted, isWildcardPermitted = true,
dnsProviderWildcardSupported, dnsProviderWildcardSupported = true,
}: Props) { }: Props) {
const { values, setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
const getDomainCount = (v: string[] | undefined): number => {
if (v?.length) {
return v.length;
}
return 0;
};
const handleChange = (v: MultiValue<SelectOption>, _actionMeta: ActionMeta<SelectOption>) => { const handleChange = (v: MultiValue<SelectOption>, _actionMeta: ActionMeta<SelectOption>) => {
const doms = v?.map((i: SelectOption) => { const doms = v?.map((i: SelectOption) => {
@@ -41,50 +35,18 @@ export function DomainNamesField({
setFieldValue(name, doms); setFieldValue(name, doms);
}; };
const isDomainValid = (d: string): boolean => {
const dom = d.trim().toLowerCase();
const v: any = values;
// Deny if the list of domains is hit
if (maxDomains && getDomainCount(v?.[name]) >= maxDomains) {
return false;
}
if (dom.length < 3) {
return false;
}
// Prevent wildcards
if ((!isWildcardPermitted || !dnsProviderWildcardSupported) && dom.indexOf("*") !== -1) {
return false;
}
// Prevent duplicate * in domain
if ((dom.match(/\*/g) || []).length > 1) {
return false;
}
// Prevent some invalid characters
if ((dom.match(/(@|,|!|&|\$|#|%|\^|\(|\))/g) || []).length > 0) {
return false;
}
// This will match *.com type domains,
return dom.match(/\*\.[^.]+$/m) === null;
};
const helperTexts: string[] = []; const helperTexts: string[] = [];
if (maxDomains) { if (maxDomains) {
helperTexts.push(intl.formatMessage({ id: "domain_names.max" }, { count: maxDomains })); helperTexts.push(intl.formatMessage({ id: "domain-names.max" }, { count: maxDomains }));
} }
if (!isWildcardPermitted) { if (!isWildcardPermitted) {
helperTexts.push(intl.formatMessage({ id: "wildcards-not-permitted" })); helperTexts.push(intl.formatMessage({ id: "domain-names.wildcards-not-permitted" }));
} else if (!dnsProviderWildcardSupported) { } else if (!dnsProviderWildcardSupported) {
helperTexts.push(intl.formatMessage({ id: "wildcards-not-supported" })); helperTexts.push(intl.formatMessage({ id: "domain-names.wildcards-not-supported" }));
} }
return ( return (
<Field name={name}> <Field name={name} validate={validateDomains(isWildcardPermitted && dnsProviderWildcardSupported, maxDomains)}>
{({ field, form }: any) => ( {({ field, form }: any) => (
<div className="mb-3"> <div className="mb-3">
<label className="form-label" htmlFor={id}> <label className="form-label" htmlFor={id}>
@@ -97,21 +59,19 @@ export function DomainNamesField({
id={id} id={id}
closeMenuOnSelect={true} closeMenuOnSelect={true}
isClearable={false} isClearable={false}
isValidNewOption={isDomainValid} isValidNewOption={validateDomain(isWildcardPermitted && dnsProviderWildcardSupported)}
isMulti isMulti
placeholder="Start typing to add domain..." placeholder={intl.formatMessage({ id: "domain-names.placeholder" })}
onChange={handleChange} onChange={handleChange}
value={field.value?.map((d: string) => ({ label: d, value: d }))} value={field.value?.map((d: string) => ({ label: d, value: d }))}
/> />
{form.errors[field.name] ? ( {form.errors[field.name] && form.touched[field.name] ? (
<div className="invalid-feedback"> <small className="text-danger">{form.errors[field.name]}</small>
{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null}
</div>
) : helperTexts.length ? ( ) : helperTexts.length ? (
helperTexts.map((i) => ( helperTexts.map((i) => (
<div key={i} className="invalid-feedback text-info"> <small key={i} className="text-info">
{i} {i}
</div> </small>
)) ))
) : null} ) : null}
</div> </div>

View File

@@ -0,0 +1,40 @@
import CodeEditor from "@uiw/react-textarea-code-editor";
import { Field } from "formik";
import { intl } from "src/locale";
interface Props {
id?: string;
name?: string;
label?: string;
}
export function NginxConfigField({
name = "advancedConfig",
label = "nginx-config.label",
id = "advancedConfig",
}: Props) {
return (
<Field name={name}>
{({ field }: any) => (
<div className="mt-3">
<label htmlFor={id} className="form-label">
{intl.formatMessage({ id: label })}
</label>
<CodeEditor
language="nginx"
placeholder={intl.formatMessage({ id: "nginx-config.placeholder" })}
padding={15}
data-color-mode="dark"
minHeight={200}
indentWidth={2}
style={{
fontFamily: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
borderRadius: "0.3rem",
minHeight: "200px",
}}
{...field}
/>
</div>
)}
</Field>
);
}

View File

@@ -2,7 +2,7 @@ import { IconShield } from "@tabler/icons-react";
import { Field, useFormikContext } from "formik"; import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend"; import type { Certificate } from "src/api/backend";
import { useCertificates, useUser } from "src/hooks"; import { useCertificates } from "src/hooks";
import { DateTimeFormat, intl } from "src/locale"; import { DateTimeFormat, intl } from "src/locale";
interface CertOption { interface CertOption {
@@ -39,26 +39,33 @@ export function SSLCertificateField({
required, required,
allowNew, allowNew,
}: Props) { }: Props) {
const { data: currentUser } = useUser("me");
const { isLoading, isError, error, data } = useCertificates(); const { isLoading, isError, error, data } = useCertificates();
const { values, setFieldValue } = useFormikContext(); const { values, setFieldValue } = useFormikContext();
const v: any = values || {}; const v: any = values || {};
const handleChange = (newValue: any, _actionMeta: ActionMeta<CertOption>) => { const handleChange = (newValue: any, _actionMeta: ActionMeta<CertOption>) => {
setFieldValue(name, newValue?.value); setFieldValue(name, newValue?.value);
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, dnsChallenge, letsencryptEmail } = v; const {
sslForced,
http2Support,
hstsEnabled,
hstsSubdomains,
dnsChallenge,
dnsProvider,
dnsProviderCredentials,
propagationSeconds,
} = v;
if (!newValue?.value) { if (!newValue?.value) {
sslForced && setFieldValue("sslForced", false); sslForced && setFieldValue("sslForced", false);
http2Support && setFieldValue("http2Support", false); http2Support && setFieldValue("http2Support", false);
hstsEnabled && setFieldValue("hstsEnabled", false); hstsEnabled && setFieldValue("hstsEnabled", false);
hstsSubdomains && setFieldValue("hstsSubdomains", false); hstsSubdomains && setFieldValue("hstsSubdomains", false);
} }
if (newValue?.value === "new") { if (newValue?.value !== "new") {
if (!letsencryptEmail) { dnsChallenge && setFieldValue("dnsChallenge", undefined);
setFieldValue("letsencryptEmail", currentUser?.email); dnsProvider && setFieldValue("dnsProvider", undefined);
} dnsProviderCredentials && setFieldValue("dnsProviderCredentials", undefined);
} else { propagationSeconds && setFieldValue("propagationSeconds", undefined);
dnsChallenge && setFieldValue("dnsChallenge", false);
} }
}; };
@@ -105,7 +112,7 @@ export function SSLCertificateField({
<Select <Select
className="react-select-container" className="react-select-container"
classNamePrefix="react-select" classNamePrefix="react-select"
defaultValue={options[0]} defaultValue={options.find((o) => o.value === field.value) || options[0]}
options={options} options={options}
components={{ Option }} components={{ Option }}
styles={{ styles={{

View File

@@ -1,6 +1,7 @@
import cn from "classnames"; import cn from "classnames";
import { Field, useFormikContext } from "formik"; import { Field, useFormikContext } from "formik";
import { DNSProviderFields } from "src/components"; import { DNSProviderFields } from "src/components";
import { intl } from "src/locale";
export function SSLOptionsFields() { export function SSLOptionsFields() {
const { values, setFieldValue } = useFormikContext(); const { values, setFieldValue } = useFormikContext();
@@ -8,10 +9,16 @@ export function SSLOptionsFields() {
const newCertificate = v?.certificateId === "new"; const newCertificate = v?.certificateId === "new";
const hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0); const hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0);
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, dnsChallenge } = v; const { sslForced, http2Support, hstsEnabled, hstsSubdomains, meta } = v;
const { dnsChallenge } = meta || {};
const handleToggleChange = (e: any, fieldName: string) => { const handleToggleChange = (e: any, fieldName: string) => {
setFieldValue(fieldName, e.target.checked); setFieldValue(fieldName, e.target.checked);
if (fieldName === "meta.dnsChallenge" && !e.target.checked) {
setFieldValue("meta.dnsProvider", undefined);
setFieldValue("meta.dnsProviderCredentials", undefined);
setFieldValue("meta.propagationSeconds", undefined);
}
}; };
const toggleClasses = "form-check-input"; const toggleClasses = "form-check-input";
@@ -31,7 +38,9 @@ export function SSLOptionsFields() {
onChange={(e) => handleToggleChange(e, field.name)} onChange={(e) => handleToggleChange(e, field.name)}
disabled={!hasCertificate} disabled={!hasCertificate}
/> />
<span className="form-check-label">Force SSL</span> <span className="form-check-label">
{intl.formatMessage({ id: "domains.force-ssl" })}
</span>
</label> </label>
)} )}
</Field> </Field>
@@ -47,7 +56,9 @@ export function SSLOptionsFields() {
onChange={(e) => handleToggleChange(e, field.name)} onChange={(e) => handleToggleChange(e, field.name)}
disabled={!hasCertificate} disabled={!hasCertificate}
/> />
<span className="form-check-label">HTTP/2 Support</span> <span className="form-check-label">
{intl.formatMessage({ id: "domains.http2-support" })}
</span>
</label> </label>
)} )}
</Field> </Field>
@@ -65,7 +76,9 @@ export function SSLOptionsFields() {
onChange={(e) => handleToggleChange(e, field.name)} onChange={(e) => handleToggleChange(e, field.name)}
disabled={!hasCertificate || !sslForced} disabled={!hasCertificate || !sslForced}
/> />
<span className="form-check-label">HSTS Enabled</span> <span className="form-check-label">
{intl.formatMessage({ id: "domains.hsts-enabled" })}
</span>
</label> </label>
)} )}
</Field> </Field>
@@ -81,7 +94,9 @@ export function SSLOptionsFields() {
onChange={(e) => handleToggleChange(e, field.name)} onChange={(e) => handleToggleChange(e, field.name)}
disabled={!hasCertificate || !hstsEnabled} disabled={!hasCertificate || !hstsEnabled}
/> />
<span className="form-check-label">HSTS Enabled</span> <span className="form-check-label">
{intl.formatMessage({ id: "domains.hsts-subdomains" })}
</span>
</label> </label>
)} )}
</Field> </Field>
@@ -89,7 +104,7 @@ export function SSLOptionsFields() {
</div> </div>
{newCertificate ? ( {newCertificate ? (
<> <>
<Field name="dnsChallenge"> <Field name="meta.dnsChallenge">
{({ field }: any) => ( {({ field }: any) => (
<label className="form-check form-switch mt-1"> <label className="form-check form-switch mt-1">
<input <input
@@ -98,29 +113,14 @@ export function SSLOptionsFields() {
checked={!!dnsChallenge} checked={!!dnsChallenge}
onChange={(e) => handleToggleChange(e, field.name)} onChange={(e) => handleToggleChange(e, field.name)}
/> />
<span className="form-check-label">Use a DNS Challenge</span> <span className="form-check-label">
{intl.formatMessage({ id: "domains.use-dns" })}
</span>
</label> </label>
)} )}
</Field> </Field>
{dnsChallenge ? <DNSProviderFields /> : null} {dnsChallenge ? <DNSProviderFields /> : null}
<Field name="letsencryptEmail">
{({ field }: any) => (
<div className="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} ) : null}
</> </>

View File

@@ -1,4 +1,5 @@
export * from "./DNSProviderFields"; export * from "./DNSProviderFields";
export * from "./DomainNamesField"; export * from "./DomainNamesField";
export * from "./NginxConfigField";
export * from "./SSLCertificateField"; export * from "./SSLCertificateField";
export * from "./SSLOptionsFields"; export * from "./SSLOptionsFields";

View File

@@ -50,10 +50,23 @@
"dead-hosts.title": "404 Hosts", "dead-hosts.title": "404 Hosts",
"disabled": "Disabled", "disabled": "Disabled",
"domain-names": "Domain Names", "domain-names": "Domain Names",
"domain-names.max": "{count} domain names maximum",
"domain-names.placeholder": "Start typing to add domain...",
"domain-names.wildcards-not-permitted": "Wildcards not permitted for this type",
"domain-names.wildcards-not-supported": "Wildcards not supported for this CA",
"domains.force-ssl": "Force SSL",
"domains.hsts-enabled": "HSTS Enabled",
"domains.hsts-subdomains": "HSTS Sub-domains",
"domains.http2-support": "HTTP/2 Support",
"domains.use-dns": "Use DNS Challenge",
"email-address": "Email address", "email-address": "Email address",
"empty-subtitle": "Why don't you create one?", "empty-subtitle": "Why don't you create one?",
"error.invalid-auth": "Invalid email or password", "error.invalid-auth": "Invalid email or password",
"error.invalid-domain": "Invalid domain: {domain}",
"error.invalid-email": "Invalid email address",
"error.max-domains": "Too many domains, max is {max}",
"error.passwords-must-match": "Passwords must match", "error.passwords-must-match": "Passwords must match",
"error.required": "This is required",
"event.created-user": "Created User", "event.created-user": "Created User",
"event.deleted-user": "Deleted User", "event.deleted-user": "Deleted User",
"event.updated-user": "Updated User", "event.updated-user": "Updated User",
@@ -63,10 +76,13 @@
"lets-encrypt": "Let's Encrypt", "lets-encrypt": "Let's Encrypt",
"loading": "Loading…", "loading": "Loading…",
"login.title": "Login to your account", "login.title": "Login to your account",
"nginx-config.label": "Custom Nginx Configuration",
"nginx-config.placeholder": "# Enter your custom Nginx configuration here at your own risk!",
"no-permission-error": "You do not have access to view this.", "no-permission-error": "You do not have access to view this.",
"notfound.action": "Take me home", "notfound.action": "Take me home",
"notfound.text": "We are sorry but the page you are looking for was not found", "notfound.text": "We are sorry but the page you are looking for was not found",
"notfound.title": "Oops… You just found an error page", "notfound.title": "Oops… You just found an error page",
"notification.dead-host-saved": "404 Host has been saved",
"notification.error": "Error", "notification.error": "Error",
"notification.success": "Success", "notification.success": "Success",
"notification.user-deleted": "User has been deleted", "notification.user-deleted": "User has been deleted",
@@ -127,7 +143,5 @@
"user.switch-light": "Switch to Light mode", "user.switch-light": "Switch to Light mode",
"users.actions-title": "User #{id}", "users.actions-title": "User #{id}",
"users.add": "Add User", "users.add": "Add User",
"users.title": "Users", "users.title": "Users"
"wildcards-not-permitted": "Wildcards not permitted for this type",
"wildcards-not-supported": "Wildcards not supported for this CA"
} }

View File

@@ -152,6 +152,33 @@
"domain-names": { "domain-names": {
"defaultMessage": "Domain Names" "defaultMessage": "Domain Names"
}, },
"domain-names.max": {
"defaultMessage": "{count} domain names maximum"
},
"domain-names.placeholder": {
"defaultMessage": "Start typing to add domain..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards not permitted for this type"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards not supported for this CA"
},
"domains.force-ssl": {
"defaultMessage": "Force SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Enabled"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Sub-domains"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Support"
},
"domains.use-dns": {
"defaultMessage": "Use DNS Challenge"
},
"email-address": { "email-address": {
"defaultMessage": "Email address" "defaultMessage": "Email address"
}, },
@@ -161,6 +188,18 @@
"error.invalid-auth": { "error.invalid-auth": {
"defaultMessage": "Invalid email or password" "defaultMessage": "Invalid email or password"
}, },
"error.invalid-domain": {
"defaultMessage": "Invalid domain: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Invalid email address"
},
"error.max-domains": {
"defaultMessage": "Too many domains, max is {max}"
},
"error.required": {
"defaultMessage": "This is required"
},
"event.created-user": { "event.created-user": {
"defaultMessage": "Created User" "defaultMessage": "Created User"
}, },
@@ -191,6 +230,12 @@
"login.title": { "login.title": {
"defaultMessage": "Login to your account" "defaultMessage": "Login to your account"
}, },
"nginx-config.label": {
"defaultMessage": "Custom Nginx Configuration"
},
"nginx-config.placeholder": {
"defaultMessage": "# Enter your custom Nginx configuration here at your own risk!"
},
"no-permission-error": { "no-permission-error": {
"defaultMessage": "You do not have access to view this." "defaultMessage": "You do not have access to view this."
}, },
@@ -203,6 +248,9 @@
"notfound.title": { "notfound.title": {
"defaultMessage": "Oops… You just found an error page" "defaultMessage": "Oops… You just found an error page"
}, },
"notification.dead-host-saved": {
"defaultMessage": "404 Host has been saved"
},
"notification.error": { "notification.error": {
"defaultMessage": "Error" "defaultMessage": "Error"
}, },
@@ -385,11 +433,5 @@
}, },
"users.title": { "users.title": {
"defaultMessage": "Users" "defaultMessage": "Users"
},
"wildcards-not-permitted": {
"defaultMessage": "Wildcards not permitted for this type"
},
"wildcards-not-supported": {
"defaultMessage": "Wildcards not supported for this CA"
} }
} }

View File

@@ -13,6 +13,7 @@ interface Props {
} }
export function ChangePasswordModal({ userId, onClose }: Props) { export function ChangePasswordModal({ userId, onClose }: Props) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => { const onSubmit = async (values: any, { setSubmitting }: any) => {
if (values.new !== values.confirm) { if (values.new !== values.confirm) {
@@ -20,13 +21,18 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
setSubmitting(false); setSubmitting(false);
return; return;
} }
if (isSubmitting) return;
setIsSubmitting(true);
setError(null); setError(null);
try { try {
await updateAuth(userId, values.new, values.current); await updateAuth(userId, values.new, values.current);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
setError(intl.formatMessage({ id: err.message })); setError(intl.formatMessage({ id: err.message }));
} }
setIsSubmitting(false);
setSubmitting(false); setSubmitting(false);
}; };
@@ -42,7 +48,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
} }
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ isSubmitting }) => ( {() => (
<Form> <Form>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>{intl.formatMessage({ id: "user.change-password" })}</Modal.Title> <Modal.Title>{intl.formatMessage({ id: "user.change-password" })}</Modal.Title>

View File

@@ -3,9 +3,17 @@ import { Form, Formik } from "formik";
import { useState } from "react"; import { useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal"; import Modal from "react-bootstrap/Modal";
import { Button, DomainNamesField, Loading, SSLCertificateField, SSLOptionsFields } from "src/components"; import {
import { useDeadHost } from "src/hooks"; Button,
DomainNamesField,
Loading,
NginxConfigField,
SSLCertificateField,
SSLOptionsFields,
} from "src/components";
import { useDeadHost, useSetDeadHost } from "src/hooks";
import { intl } from "src/locale"; import { intl } from "src/locale";
import { showSuccess } from "src/notifications";
interface Props { interface Props {
id: number | "new"; id: number | "new";
@@ -13,28 +21,31 @@ interface Props {
} }
export function DeadHostModal({ id, onClose }: Props) { export function DeadHostModal({ id, onClose }: Props) {
const { data, isLoading, error } = useDeadHost(id); const { data, isLoading, error } = useDeadHost(id);
// const { mutate: setDeadHost } = useSetDeadHost(); const { mutate: setDeadHost } = useSetDeadHost();
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => { const onSubmit = async (values: any, { setSubmitting }: any) => {
setSubmitting(true); if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null); setErrorMsg(null);
console.log("SUBMIT:", values);
setSubmitting(false);
// const { ...payload } = {
// id: id === "new" ? undefined : id,
// roles: [],
// ...values,
// };
// setDeadHost(payload, { const { ...payload } = {
// onError: (err: any) => setErrorMsg(err.message), id: id === "new" ? undefined : id,
// onSuccess: () => { ...values,
// showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" })); };
// onClose();
// }, setDeadHost(payload, {
// onSettled: () => setSubmitting(false), onError: (err: any) => setErrorMsg(err.message),
// }); onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" }));
onClose();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
}; };
return ( return (
@@ -56,11 +67,12 @@ export function DeadHostModal({ id, onClose }: Props) {
http2Support: data?.http2Support, http2Support: data?.http2Support,
hstsEnabled: data?.hstsEnabled, hstsEnabled: data?.hstsEnabled,
hstsSubdomains: data?.hstsSubdomains, hstsSubdomains: data?.hstsSubdomains,
meta: data?.meta || {},
} as any } as any
} }
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ isSubmitting }) => ( {() => (
<Form> <Form>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
@@ -127,140 +139,11 @@ export function DeadHostModal({ id, onClose }: Props) {
<SSLOptionsFields /> <SSLOptionsFields />
</div> </div>
<div className="tab-pane" id="tab-advanced" role="tabpanel"> <div className="tab-pane" id="tab-advanced" role="tabpanel">
<h4>Advanced</h4> <NginxConfigField />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* <div className="row">
<div className="col-lg-6">
<div className="mb-3">
<Field name="name" validate={validateString(1, 50)}>
{({ field, form }: any) => (
<div className="form-floating mb-3">
<input
id="name"
className={`form-control ${form.errors.name && form.touched.name ? "is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "user.full-name" })}
{...field}
/>
<label htmlFor="name">
{intl.formatMessage({ id: "user.full-name" })}
</label>
{form.errors.name ? (
<div className="invalid-feedback">
{form.errors.name && form.touched.name
? form.errors.name
: null}
</div>
) : null}
</div>
)}
</Field>
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<Field name="nickname" validate={validateString(1, 30)}>
{({ field, form }: any) => (
<div className="form-floating mb-3">
<input
id="nickname"
className={`form-control ${form.errors.nickname && form.touched.nickname ? "is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "user.nickname" })}
{...field}
/>
<label htmlFor="nickname">
{intl.formatMessage({ id: "user.nickname" })}
</label>
{form.errors.nickname ? (
<div className="invalid-feedback">
{form.errors.nickname && form.touched.nickname
? form.errors.nickname
: null}
</div>
) : null}
</div>
)}
</Field>
</div>
</div>
</div>
<div className="mb-3">
<Field name="email" validate={validateEmail()}>
{({ field, form }: any) => (
<div className="form-floating mb-3">
<input
id="email"
type="email"
className={`form-control ${form.errors.email && form.touched.email ? "is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "email-address" })}
{...field}
/>
<label htmlFor="email">
{intl.formatMessage({ id: "email-address" })}
</label>
{form.errors.email ? (
<div className="invalid-feedback">
{form.errors.email && form.touched.email
? form.errors.email
: null}
</div>
) : null}
</div>
)}
</Field>
</div>
{currentUser && data && currentUser?.id !== data?.id ? (
<div className="my-3">
<h3 className="py-2">{intl.formatMessage({ id: "user.flags.title" })}</h3>
<div className="divide-y">
<div>
<label className="row" htmlFor="isAdmin">
<span className="col">
{intl.formatMessage({ id: "role.admin" })}
</span>
<span className="col-auto">
<Field name="isAdmin" type="checkbox">
{({ field }: any) => (
<label className="form-check form-check-single form-switch">
<input
{...field}
id="isAdmin"
className="form-check-input"
type="checkbox"
/>
</label>
)}
</Field>
</span>
</label>
</div>
<div>
<label className="row" htmlFor="isDisabled">
<span className="col">
{intl.formatMessage({ id: "disabled" })}
</span>
<span className="col-auto">
<Field name="isDisabled" type="checkbox">
{({ field }: any) => (
<label className="form-check form-check-single form-switch">
<input
{...field}
id="isDisabled"
className="form-check-input"
type="checkbox"
/>
</label>
)}
</Field>
</span>
</label>
</div>
</div>
</div>
) : null} */}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>

View File

@@ -15,10 +15,11 @@ interface Props {
export function DeleteConfirmModal({ title, children, onConfirm, onClose, invalidations }: Props) { export function DeleteConfirmModal({ title, children, onConfirm, onClose, invalidations }: Props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async () => { const onSubmit = async () => {
setSubmitting(true); if (isSubmitting) return;
setIsSubmitting(true);
setError(null); setError(null);
try { try {
await onConfirm(); await onConfirm();
@@ -30,7 +31,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
} catch (err: any) { } catch (err: any) {
setError(intl.formatMessage({ id: err.message })); setError(intl.formatMessage({ id: err.message }));
} }
setSubmitting(false); setIsSubmitting(false);
}; };
return ( return (
@@ -45,7 +46,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
{children} {children}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={submitting}> <Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
{intl.formatMessage({ id: "cancel" })} {intl.formatMessage({ id: "cancel" })}
</Button> </Button>
<Button <Button
@@ -53,8 +54,8 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
actionType="primary" actionType="primary"
className="ms-auto btn-red" className="ms-auto btn-red"
data-bs-dismiss="modal" data-bs-dismiss="modal"
isLoading={submitting} isLoading={isSubmitting}
disabled={submitting} disabled={isSubmitting}
onClick={onSubmit} onClick={onSubmit}
> >
{intl.formatMessage({ id: "action.delete" })} {intl.formatMessage({ id: "action.delete" })}

View File

@@ -17,8 +17,11 @@ export function PermissionsModal({ userId, onClose }: Props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorMsg, setErrorMsg] = useState<string | null>(null);
const { data, isLoading, error } = useUser(userId); const { data, isLoading, error } = useUser(userId);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => { const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null); setErrorMsg(null);
try { try {
await setPermissions(userId, values); await setPermissions(userId, values);
@@ -29,6 +32,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
setErrorMsg(intl.formatMessage({ id: err.message })); setErrorMsg(intl.formatMessage({ id: err.message }));
} }
setSubmitting(false); setSubmitting(false);
setIsSubmitting(false);
}; };
const getPermissionButtons = (field: any, form: any) => { const getPermissionButtons = (field: any, form: any) => {
@@ -104,7 +108,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
} }
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ isSubmitting }) => ( {() => (
<Form> <Form>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>

View File

@@ -15,8 +15,10 @@ interface Props {
export function SetPasswordModal({ userId, onClose }: Props) { export function SetPasswordModal({ userId, onClose }: Props) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => { const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setError(null); setError(null);
try { try {
await updateAuth(userId, values.new); await updateAuth(userId, values.new);
@@ -24,6 +26,7 @@ export function SetPasswordModal({ userId, onClose }: Props) {
} catch (err: any) { } catch (err: any) {
setError(intl.formatMessage({ id: err.message })); setError(intl.formatMessage({ id: err.message }));
} }
setIsSubmitting(false);
setSubmitting(false); setSubmitting(false);
}; };
@@ -37,7 +40,7 @@ export function SetPasswordModal({ userId, onClose }: Props) {
} }
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ isSubmitting }) => ( {() => (
<Form> <Form>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>{intl.formatMessage({ id: "user.set-password" })}</Modal.Title> <Modal.Title>{intl.formatMessage({ id: "user.set-password" })}</Modal.Title>

View File

@@ -17,9 +17,13 @@ export function UserModal({ userId, onClose }: Props) {
const { data: currentUser, isLoading: currentIsLoading } = useUser("me"); const { data: currentUser, isLoading: currentIsLoading } = useUser("me");
const { mutate: setUser } = useSetUser(); const { mutate: setUser } = useSetUser();
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => { const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null); setErrorMsg(null);
const { ...payload } = { const { ...payload } = {
id: userId === "new" ? undefined : userId, id: userId === "new" ? undefined : userId,
roles: [], roles: [],
@@ -43,7 +47,10 @@ export function UserModal({ userId, onClose }: Props) {
showSuccess(intl.formatMessage({ id: "notification.user-saved" })); showSuccess(intl.formatMessage({ id: "notification.user-saved" }));
onClose(); onClose();
}, },
onSettled: () => setSubmitting(false), onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
}); });
}; };
@@ -68,7 +75,7 @@ export function UserModal({ userId, onClose }: Props) {
} }
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ isSubmitting }) => ( {() => (
<Form> <Form>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>

View File

@@ -1,3 +1,5 @@
import { intl } from "src/locale";
const validateString = (minLength = 0, maxLength = 0) => { const validateString = (minLength = 0, maxLength = 0) => {
if (minLength <= 0 && maxLength <= 0) { if (minLength <= 0 && maxLength <= 0) {
// this doesn't require translation // this doesn't require translation
@@ -6,12 +8,14 @@ const validateString = (minLength = 0, maxLength = 0) => {
return (value: string): string | undefined => { return (value: string): string | undefined => {
if (minLength && (typeof value === "undefined" || !value.length)) { if (minLength && (typeof value === "undefined" || !value.length)) {
return "This is required"; return intl.formatMessage({ id: "error.required" });
} }
if (minLength && value.length < minLength) { if (minLength && value.length < minLength) {
// TODO: i18n
return `Minimum length is ${minLength} character${minLength === 1 ? "" : "s"}`; return `Minimum length is ${minLength} character${minLength === 1 ? "" : "s"}`;
} }
if (maxLength && (typeof value === "undefined" || value.length > maxLength)) { if (maxLength && (typeof value === "undefined" || value.length > maxLength)) {
// TODO: i18n
return `Maximum length is ${maxLength} character${maxLength === 1 ? "" : "s"}`; return `Maximum length is ${maxLength} character${maxLength === 1 ? "" : "s"}`;
} }
}; };
@@ -26,12 +30,14 @@ const validateNumber = (min = -1, max = -1) => {
return (value: string): string | undefined => { return (value: string): string | undefined => {
const int: number = +value; const int: number = +value;
if (min > -1 && !int) { if (min > -1 && !int) {
return "This is required"; return intl.formatMessage({ id: "error.required" });
} }
if (min > -1 && int < min) { if (min > -1 && int < min) {
// TODO: i18n
return `Minimum is ${min}`; return `Minimum is ${min}`;
} }
if (max > -1 && int > max) { if (max > -1 && int > max) {
// TODO: i18n
return `Maximum is ${max}`; return `Maximum is ${max}`;
} }
}; };
@@ -40,12 +46,62 @@ const validateNumber = (min = -1, max = -1) => {
const validateEmail = () => { const validateEmail = () => {
return (value: string): string | undefined => { return (value: string): string | undefined => {
if (!value.length) { if (!value.length) {
return "This is required"; return intl.formatMessage({ id: "error.required" });
} }
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) { if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
return "Invalid email address"; return intl.formatMessage({ id: "error.invalid-email" });
} }
}; };
}; };
export { validateEmail, validateNumber, validateString }; const validateDomain = (allowWildcards = false) => {
return (d: string): boolean => {
const dom = d.trim().toLowerCase();
if (dom.length < 3) {
return false;
}
// Prevent wildcards
if (!allowWildcards && dom.indexOf("*") !== -1) {
return false;
}
// Prevent duplicate * in domain
if ((dom.match(/\*/g) || []).length > 1) {
return false;
}
// Prevent some invalid characters
if ((dom.match(/(@|,|!|&|\$|#|%|\^|\(|\))/g) || []).length > 0) {
return false;
}
// This will match *.com type domains,
return dom.match(/\*\.[^.]+$/m) === null;
};
};
const validateDomains = (allowWildcards = false, maxDomains?: number) => {
const vDom = validateDomain(allowWildcards);
return (value: string[]): string | undefined => {
if (!value.length) {
return intl.formatMessage({ id: "error.required" });
}
// Deny if the list of domains is hit
if (maxDomains && value.length >= maxDomains) {
return intl.formatMessage({ id: "error.max-domains" }, { max: maxDomains });
}
// validate each domain
for (let i = 0; i < value.length; i++) {
if (!vDom(value[i])) {
return intl.formatMessage({ id: "error.invalid-domain" }, { domain: value[i] });
}
}
};
};
export { validateEmail, validateNumber, validateString, validateDomains, validateDomain };

View File

@@ -129,6 +129,7 @@ const Dashboard = () => {
- fix bad jwt not refreshing entire page - fix bad jwt not refreshing entire page
- add help docs for host types - add help docs for host types
- REDO SCREENSHOTS in docs folder - REDO SCREENSHOTS in docs folder
- Remove letsEncryptEmail field from new certificate requests, use current user email server side
More for api, then implement here: More for api, then implement here:
- Properly implement refresh tokens - Properly implement refresh tokens

View File

@@ -10,10 +10,11 @@ import Empty from "./Empty";
interface Props { interface Props {
data: DeadHost[]; data: DeadHost[];
isFetching?: boolean; isFetching?: boolean;
onEdit?: (id: number) => void;
onDelete?: (id: number) => void; onDelete?: (id: number) => void;
onNew?: () => void; onNew?: () => void;
} }
export default function Table({ data, isFetching, onDelete, onNew }: Props) { export default function Table({ data, isFetching, onEdit, onDelete, onNew }: Props) {
const columnHelper = createColumnHelper<DeadHost>(); const columnHelper = createColumnHelper<DeadHost>();
const columns = useMemo( const columns = useMemo(
() => [ () => [
@@ -71,7 +72,14 @@ export default function Table({ data, isFetching, onDelete, onNew }: Props) {
{ id: info.row.original.id }, { id: info.row.original.id },
)} )}
</span> </span>
<a className="dropdown-item" href="#"> <a
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
onEdit?.(info.row.original.id);
}}
>
<IconEdit size={16} /> <IconEdit size={16} />
{intl.formatMessage({ id: "action.edit" })} {intl.formatMessage({ id: "action.edit" })}
</a> </a>
@@ -100,7 +108,7 @@ export default function Table({ data, isFetching, onDelete, onNew }: Props) {
}, },
}), }),
], ],
[columnHelper, onDelete], [columnHelper, onDelete, onEdit],
); );
const tableInstance = useReactTable<DeadHost>({ const tableInstance = useReactTable<DeadHost>({

View File

@@ -58,6 +58,7 @@ export default function TableWrapper() {
<Table <Table
data={data ?? []} data={data ?? []}
isFetching={isFetching} isFetching={isFetching}
onEdit={(id: number) => setEditId(id)}
onDelete={(id: number) => setDeleteId(id)} onDelete={(id: number) => setDeleteId(id)}
onNew={() => setEditId("new")} onNew={() => setEditId("new")}
/> />

View File

@@ -142,7 +142,7 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1"
"@babel/runtime@^7.12.0", "@babel/runtime@^7.18.3": "@babel/runtime@^7.12.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6":
version "7.28.4" version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
@@ -982,6 +982,20 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/hast@^2.0.0":
version "2.3.10"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643"
integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==
dependencies:
"@types/unist" "^2"
"@types/hast@^3.0.0":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
dependencies:
"@types/unist" "*"
"@types/hoist-non-react-statics@^3.3.1": "@types/hoist-non-react-statics@^3.3.1":
version "3.3.7" version "3.3.7"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c"
@@ -994,6 +1008,13 @@
resolved "https://registry.yarnpkg.com/@types/humps/-/humps-2.0.6.tgz#a358688fe092e40b5f50261e0a55e2fa6d68cabe" resolved "https://registry.yarnpkg.com/@types/humps/-/humps-2.0.6.tgz#a358688fe092e40b5f50261e0a55e2fa6d68cabe"
integrity sha512-Fagm1/a/1J9gDKzGdtlPmmTN5eSw/aaTzHtj740oSfo+MODsSY2WglxMmhTdOglC8nxqUhGGQ+5HfVtBvxo3Kg== integrity sha512-Fagm1/a/1J9gDKzGdtlPmmTN5eSw/aaTzHtj740oSfo+MODsSY2WglxMmhTdOglC8nxqUhGGQ+5HfVtBvxo3Kg==
"@types/mdast@^4.0.0":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
dependencies:
"@types/unist" "*"
"@types/node@^20.0.0": "@types/node@^20.0.0":
version "20.19.11" version "20.19.11"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.11.tgz#728cab53092bd5f143beed7fbba7ba99de3c16c4" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.11.tgz#728cab53092bd5f143beed7fbba7ba99de3c16c4"
@@ -1006,6 +1027,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==
"@types/prismjs@^1.0.0":
version "1.26.5"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6"
integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==
"@types/prop-types@^15.7.12": "@types/prop-types@^15.7.12":
version "15.7.15" version "15.7.15"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
@@ -1042,6 +1068,16 @@
dependencies: dependencies:
csstype "^3.0.2" csstype "^3.0.2"
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
"@types/unist@^2", "@types/unist@^2.0.0":
version "2.0.11"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4"
integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
"@types/warning@^3.0.3": "@types/warning@^3.0.3":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798" resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798"
@@ -1052,6 +1088,20 @@
resolved "https://registry.yarnpkg.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz#e5e06dcd3e92d4e622ef0129637707d66c28d6a4" resolved "https://registry.yarnpkg.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz#e5e06dcd3e92d4e622ef0129637707d66c28d6a4"
integrity sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA== integrity sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==
"@uiw/react-textarea-code-editor@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@uiw/react-textarea-code-editor/-/react-textarea-code-editor-3.1.1.tgz#8ca1b706a3081a51c68bc0df91c9c3cdadd9944e"
integrity sha512-AERRbp/d85vWR+UPgsB5hEgerNXuyszdmhWl2fV2H2jN63jgOobwEnjIpb76Vwy8SaGa/AdehaoJX2XZgNXtJA==
dependencies:
"@babel/runtime" "^7.18.6"
rehype "~13.0.0"
rehype-prism-plus "2.0.0"
"@ungap/structured-clone@^1.0.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
"@vitejs/plugin-react@^5.0.3": "@vitejs/plugin-react@^5.0.3":
version "5.0.3" version "5.0.3"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.0.3.tgz#182ea45406d89e55b4e35c92a4a8c2c8388726c8" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.0.3.tgz#182ea45406d89e55b4e35c92a4a8c2c8388726c8"
@@ -1166,6 +1216,11 @@ babel-plugin-macros@^3.1.0:
cosmiconfig "^7.0.0" cosmiconfig "^7.0.0"
resolve "^1.19.0" resolve "^1.19.0"
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
base64-js@^1.3.1: base64-js@^1.3.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -1216,6 +1271,11 @@ caniuse-lite@^1.0.30001737:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4"
integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA== integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
chai@^5.2.0: chai@^5.2.0:
version "5.3.3" version "5.3.3"
resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06"
@@ -1227,6 +1287,26 @@ chai@^5.2.0:
loupe "^3.1.0" loupe "^3.1.0"
pathval "^2.0.0" pathval "^2.0.0"
character-entities-html4@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==
character-entities-legacy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b"
integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
character-entities@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
character-reference-invalid@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9"
integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
check-error@^2.1.1: check-error@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
@@ -1249,6 +1329,11 @@ clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
comma-separated-tokens@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
convert-source-map@^1.5.0: convert-source-map@^1.5.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
@@ -1307,6 +1392,13 @@ decimal.js@^10.4.3:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
decode-named-character-reference@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz#25c32ae6dd5e21889549d40f676030e9514cc0ed"
integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==
dependencies:
character-entities "^2.0.0"
decode-uri-component@^0.4.1: decode-uri-component@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5"
@@ -1322,7 +1414,7 @@ deepmerge@^2.1.1:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
dequal@^2.0.3: dequal@^2.0.0, dequal@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
@@ -1332,6 +1424,13 @@ detect-libc@^1.0.3:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
devlop@^1.0.0, devlop@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
dependencies:
dequal "^2.0.0"
dom-accessibility-api@^0.5.9: dom-accessibility-api@^0.5.9:
version "0.5.16" version "0.5.16"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
@@ -1355,6 +1454,11 @@ electron-to-chromium@^1.5.211:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz#f434187f227fb7e67bfcf8243b959cf3ce14013e" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz#f434187f227fb7e67bfcf8243b959cf3ce14013e"
integrity sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q== integrity sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==
entities@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694"
integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
error-ex@^1.3.1: error-ex@^1.3.1:
version "1.3.4" version "1.3.4"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414"
@@ -1421,6 +1525,11 @@ expect-type@^1.2.1:
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.2.tgz#c030a329fb61184126c8447585bc75a7ec6fbff3" resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.2.tgz#c030a329fb61184126c8447585bc75a7ec6fbff3"
integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA== integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==
extend@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
fast-deep-equal@^3.1.3: fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -1506,6 +1615,99 @@ hasown@^2.0.2:
dependencies: dependencies:
function-bind "^1.1.2" function-bind "^1.1.2"
hast-util-from-html@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz#485c74785358beb80c4ba6346299311ac4c49c82"
integrity sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==
dependencies:
"@types/hast" "^3.0.0"
devlop "^1.1.0"
hast-util-from-parse5 "^8.0.0"
parse5 "^7.0.0"
vfile "^6.0.0"
vfile-message "^4.0.0"
hast-util-from-parse5@^8.0.0:
version "8.0.3"
resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e"
integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
devlop "^1.0.0"
hastscript "^9.0.0"
property-information "^7.0.0"
vfile "^6.0.0"
vfile-location "^5.0.0"
web-namespaces "^2.0.0"
hast-util-parse-selector@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz#25ab00ae9e75cbc62cf7a901f68a247eade659e2"
integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==
dependencies:
"@types/hast" "^2.0.0"
hast-util-parse-selector@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27"
integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
dependencies:
"@types/hast" "^3.0.0"
hast-util-to-html@^9.0.0:
version "9.0.5"
resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005"
integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
ccount "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-whitespace "^3.0.0"
html-void-elements "^3.0.0"
mdast-util-to-hast "^13.0.0"
property-information "^7.0.0"
space-separated-tokens "^2.0.0"
stringify-entities "^4.0.0"
zwitch "^2.0.4"
hast-util-to-string@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz#a4f15e682849326dd211c97129c94b0c3e76527c"
integrity sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==
dependencies:
"@types/hast" "^3.0.0"
hast-util-whitespace@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==
dependencies:
"@types/hast" "^3.0.0"
hastscript@^7.0.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b"
integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==
dependencies:
"@types/hast" "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-parse-selector "^3.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
hastscript@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff"
integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==
dependencies:
"@types/hast" "^3.0.0"
comma-separated-tokens "^2.0.0"
hast-util-parse-selector "^4.0.0"
property-information "^7.0.0"
space-separated-tokens "^2.0.0"
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -1513,6 +1715,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-
dependencies: dependencies:
react-is "^16.7.0" react-is "^16.7.0"
html-void-elements@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
humps@^2.0.1: humps@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa"
@@ -1558,6 +1765,19 @@ invariant@^2.2.4:
dependencies: dependencies:
loose-envify "^1.0.0" loose-envify "^1.0.0"
is-alphabetical@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b"
integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==
is-alphanumerical@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875"
integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==
dependencies:
is-alphabetical "^2.0.0"
is-decimal "^2.0.0"
is-arrayish@^0.2.1: is-arrayish@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -1570,6 +1790,11 @@ is-core-module@^2.16.0:
dependencies: dependencies:
hasown "^2.0.2" hasown "^2.0.2"
is-decimal@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7"
integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==
is-extglob@^2.1.1: is-extglob@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1582,11 +1807,21 @@ is-glob@^4.0.3:
dependencies: dependencies:
is-extglob "^2.1.1" is-extglob "^2.1.1"
is-hexadecimal@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027"
integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==
is-number@^7.0.0: is-number@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-plain-obj@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -1663,11 +1898,58 @@ magic-string@^0.30.17:
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5" "@jridgewell/sourcemap-codec" "^1.5.5"
mdast-util-to-hast@^13.0.0:
version "13.2.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4"
integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
"@ungap/structured-clone" "^1.0.0"
devlop "^1.0.0"
micromark-util-sanitize-uri "^2.0.0"
trim-lines "^3.0.0"
unist-util-position "^5.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
memoize-one@^6.0.0: memoize-one@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
micromark-util-character@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6"
integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-encode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8"
integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==
micromark-util-sanitize-uri@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7"
integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-encode "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-symbol@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8"
integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==
micromark-util-types@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e"
integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==
micromatch@^4.0.5: micromatch@^4.0.5:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
@@ -1721,6 +2003,19 @@ parent-module@^1.0.0:
dependencies: dependencies:
callsites "^3.0.0" callsites "^3.0.0"
parse-entities@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159"
integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==
dependencies:
"@types/unist" "^2.0.0"
character-entities-legacy "^3.0.0"
character-reference-invalid "^2.0.0"
decode-named-character-reference "^1.0.0"
is-alphanumerical "^2.0.0"
is-decimal "^2.0.0"
is-hexadecimal "^2.0.0"
parse-json@^5.0.0: parse-json@^5.0.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
@@ -1731,6 +2026,18 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0" json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6" lines-and-columns "^1.1.6"
parse-numeric-range@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3"
integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==
parse5@^7.0.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05"
integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
dependencies:
entities "^6.0.0"
path-key@^4.0.0: path-key@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18"
@@ -1816,6 +2123,16 @@ prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.13.1" react-is "^16.13.1"
property-information@^6.0.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec"
integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==
property-information@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d"
integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==
query-string@^9.3.1: query-string@^9.3.1:
version "9.3.1" version "9.3.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.3.1.tgz#d0c93e6c7fb7c17bdf04aa09e382114580ede270" resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.3.1.tgz#d0c93e6c7fb7c17bdf04aa09e382114580ede270"
@@ -1969,6 +2286,56 @@ redent@^3.0.0:
indent-string "^4.0.0" indent-string "^4.0.0"
strip-indent "^3.0.0" strip-indent "^3.0.0"
refractor@^4.8.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-4.9.0.tgz#2e1c7af0157230cdd2f9086660912eadc5f68323"
integrity sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==
dependencies:
"@types/hast" "^2.0.0"
"@types/prismjs" "^1.0.0"
hastscript "^7.0.0"
parse-entities "^4.0.0"
rehype-parse@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-9.0.1.tgz#9993bda129acc64c417a9d3654a7be38b2a94c20"
integrity sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==
dependencies:
"@types/hast" "^3.0.0"
hast-util-from-html "^2.0.0"
unified "^11.0.0"
rehype-prism-plus@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/rehype-prism-plus/-/rehype-prism-plus-2.0.0.tgz#75b1e2d0dd7496125987a1732cb7d560de02a0fd"
integrity sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==
dependencies:
hast-util-to-string "^3.0.0"
parse-numeric-range "^1.3.0"
refractor "^4.8.0"
rehype-parse "^9.0.0"
unist-util-filter "^5.0.0"
unist-util-visit "^5.0.0"
rehype-stringify@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-10.0.1.tgz#2ec1ebc56c6aba07905d3b4470bdf0f684f30b75"
integrity sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==
dependencies:
"@types/hast" "^3.0.0"
hast-util-to-html "^9.0.0"
unified "^11.0.0"
rehype@~13.0.0:
version "13.0.2"
resolved "https://registry.yarnpkg.com/rehype/-/rehype-13.0.2.tgz#ab0b3ac26573d7b265a0099feffad450e4cf1952"
integrity sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==
dependencies:
"@types/hast" "^3.0.0"
rehype-parse "^9.0.0"
rehype-stringify "^10.0.0"
unified "^11.0.0"
resolve-from@^4.0.0: resolve-from@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -2069,6 +2436,11 @@ source-map@^0.5.7:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
split-on-first@^3.0.0: split-on-first@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7"
@@ -2084,6 +2456,14 @@ std-env@^3.9.0:
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
stringify-entities@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3"
integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==
dependencies:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
strip-ansi@^7.1.0: strip-ansi@^7.1.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -2178,6 +2558,16 @@ to-regex-range@^5.0.1:
dependencies: dependencies:
is-number "^7.0.0" is-number "^7.0.0"
trim-lines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
trough@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f"
integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==
tsconfck@^3.0.3: tsconfck@^3.0.3:
version "3.1.6" version "3.1.6"
resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead"
@@ -2218,6 +2608,66 @@ unicorn-magic@^0.3.0:
resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104" resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104"
integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==
unified@^11.0.0:
version "11.0.5"
resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1"
integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==
dependencies:
"@types/unist" "^3.0.0"
bail "^2.0.0"
devlop "^1.0.0"
extend "^3.0.0"
is-plain-obj "^4.0.0"
trough "^2.0.0"
vfile "^6.0.0"
unist-util-filter@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/unist-util-filter/-/unist-util-filter-5.0.1.tgz#f9f3a0bdee007e040964c274dda27bac663d0a39"
integrity sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
unist-util-is@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-position@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4"
integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
dependencies:
"@types/unist" "^3.0.0"
unist-util-visit-parents@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
update-browserslist-db@^1.1.3: update-browserslist-db@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
@@ -2236,6 +2686,30 @@ use-sync-external-store@^1.4.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
vfile-location@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3"
integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==
dependencies:
"@types/unist" "^3.0.0"
vfile "^6.0.0"
vfile-message@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4"
integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position "^4.0.0"
vfile@^6.0.0:
version "6.0.3"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab"
integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==
dependencies:
"@types/unist" "^3.0.0"
vfile-message "^4.0.0"
vite-node@3.2.4: vite-node@3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"
@@ -2340,6 +2814,11 @@ warning@^4.0.0, warning@^4.0.3:
dependencies: dependencies:
loose-envify "^1.0.0" loose-envify "^1.0.0"
web-namespaces@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
whatwg-mimetype@^3.0.0: whatwg-mimetype@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
@@ -2362,3 +2841,8 @@ yaml@^1.10.0:
version "1.10.2" version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
zwitch@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==

View File

@@ -81,9 +81,7 @@ describe('Certificates endpoints', () => {
data: { data: {
domain_names: ['test.com"||echo hello-world||\\\\n test.com"'], domain_names: ['test.com"||echo hello-world||\\\\n test.com"'],
meta: { meta: {
dns_challenge: false, dns_challenge: false,
letsencrypt_agree: true,
letsencrypt_email: 'admin@example.com',
}, },
provider: 'letsencrypt', provider: 'letsencrypt',
}, },
@@ -97,28 +95,4 @@ describe('Certificates endpoints', () => {
expect(data.error.message).to.contain('data/domain_names/0 must match pattern'); expect(data.error.message).to.contain('data/domain_names/0 must match pattern');
}); });
}); });
it('Request Certificate - LE Email Escaped', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: ['test.com"||echo hello-world||\\\\n test.com"'],
meta: {
dns_challenge: false,
letsencrypt_agree: true,
letsencrypt_email: "admin@example.com' --version;echo hello-world",
},
provider: 'letsencrypt',
},
returnOnError: true,
}).then((data) => {
cy.validateSwaggerSchema('post', 400, '/nginx/certificates', data);
expect(data).to.have.property('error');
expect(data.error).to.have.property('message');
expect(data.error).to.have.property('code');
expect(data.error.code).to.equal(400);
expect(data.error.message).to.contain('data/meta/letsencrypt_email must match pattern');
});
});
}); });

View File

@@ -19,8 +19,6 @@ describe('Full Certificate Provisions', () => {
'website1.example.com' 'website1.example.com'
], ],
meta: { meta: {
letsencrypt_email: 'admin@example.com',
letsencrypt_agree: true,
dns_challenge: false dns_challenge: false
}, },
provider: 'letsencrypt' provider: 'letsencrypt'
@@ -42,11 +40,9 @@ describe('Full Certificate Provisions', () => {
'website2.example.com' 'website2.example.com'
], ],
meta: { meta: {
letsencrypt_email: "admin@example.com",
dns_challenge: true, dns_challenge: true,
dns_provider: 'powerdns', dns_provider: 'powerdns',
dns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\r\ndns_powerdns_api_key = npm', dns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\r\ndns_powerdns_api_key = npm',
letsencrypt_agree: true,
propagation_seconds: 5, propagation_seconds: 5,
}, },
provider: 'letsencrypt' provider: 'letsencrypt'