Certificates section react work

This commit is contained in:
Jamie Curnow
2025-10-26 00:28:39 +10:00
parent 5b7013b8d5
commit bb6c9c8daf
24 changed files with 596 additions and 121 deletions

View File

@@ -4,7 +4,6 @@ import type { Certificate } from "./models";
export async function createCertificate(item: Certificate): Promise<Certificate> {
return await api.post({
url: "/nginx/certificates",
// todo: only use whitelist of fields for this data
data: item,
});
}

View File

@@ -1,10 +1,10 @@
import * as api from "./base";
export async function testHttpCertificate(domains: string[]): Promise<Record<string, string>> {
return await api.get({
return await api.post({
url: "/nginx/certificates/test-http",
params: {
domains: domains.join(","),
data: {
domains,
},
});
}

View File

@@ -58,7 +58,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
options?.unshift({
value: 0,
label: intl.formatMessage({ id: "access-list.public" }),
subLabel: "No basic auth required",
subLabel: intl.formatMessage({ id: "access-list.public.subtitle" }),
icon: <IconLockOpen2 size={14} className="text-red" />,
});

View File

@@ -1,8 +1,10 @@
import { IconAlertTriangle } from "@tabler/icons-react";
import { Field, useFormikContext } from "formik";
import { useState } from "react";
import Select, { type ActionMeta } from "react-select";
import type { DNSProvider } from "src/api/backend";
import { useDnsProviders } from "src/hooks";
import { T } from "src/locale";
import styles from "./DNSProviderFields.module.css";
interface DNSProviderOption {
@@ -10,7 +12,11 @@ interface DNSProviderOption {
readonly label: string;
readonly credentials: string;
}
export function DNSProviderFields() {
interface Props {
showBoundaryBox?: boolean;
}
export function DNSProviderFields({ showBoundaryBox = false }: Props) {
const { values, setFieldValue } = useFormikContext();
const { data: dnsProviders, isLoading } = useDnsProviders();
const [dnsProviderId, setDnsProviderId] = useState<string | null>(null);
@@ -31,17 +37,17 @@ export function DNSProviderFields() {
})) || [];
return (
<div className={styles.dnsChallengeWarning}>
<p className="text-info">
This section requires some knowledge about Certbot and DNS plugins. Please consult the respective
plugins documentation.
<div className={showBoundaryBox ? styles.dnsChallengeWarning : undefined}>
<p className="text-warning">
<IconAlertTriangle size={16} className="me-1" />
<T id="certificates.dns.warning" />
</p>
<Field name="meta.dnsProvider">
{({ field }: any) => (
<div className="row">
<label htmlFor="dnsProvider" className="form-label">
DNS Provider
<T id="certificates.dns.provider" />
</label>
<Select
className="react-select-container"
@@ -66,7 +72,7 @@ export function DNSProviderFields() {
{({ field }: any) => (
<div className="mt-3">
<label htmlFor="dnsProviderCredentials" className="form-label">
Credentials File Content
<T id="certificates.dns.credentials" />
</label>
<textarea
id="dnsProviderCredentials"
@@ -78,13 +84,12 @@ export function DNSProviderFields() {
/>
<div>
<small className="text-muted">
This plugin requires a configuration file containing an API token or other
credentials to your provider
<T id="certificates.dns.credentials-note" />
</small>
</div>
<div>
<small className="text-danger">
This data will be stored as plaintext in the database and in a file!
<T id="certificates.dns.credentials-warning" />
</small>
</div>
</div>
@@ -94,20 +99,18 @@ export function DNSProviderFields() {
{({ field }: any) => (
<div className="mt-3">
<label htmlFor="propagationSeconds" className="form-label">
Propagation Seconds
<T id="certificates.dns.propagation-seconds" />
</label>
<input
id="propagationSeconds"
type="number"
x
className="form-control"
min={0}
max={600}
{...field}
/>
<small className="text-muted">
Leave empty to use the plugins default value. Number of seconds to wait for DNS
propagation.
<T id="certificates.dns.propagation-seconds-note" />
</small>
</div>
)}

View File

@@ -18,14 +18,16 @@ interface Props {
dnsProviderWildcardSupported?: boolean;
name?: string;
label?: string;
onChange?: (domains: string[]) => void;
}
export function DomainNamesField({
name = "domainNames",
label = "domain-names",
id = "domainNames",
maxDomains,
isWildcardPermitted = true,
dnsProviderWildcardSupported = true,
isWildcardPermitted = false,
dnsProviderWildcardSupported = false,
onChange,
}: Props) {
const { setFieldValue } = useFormikContext();
@@ -34,6 +36,7 @@ export function DomainNamesField({
return i.value;
});
setFieldValue(name, doms);
onChange?.(doms);
};
const helperTexts: ReactNode[] = [];

View File

@@ -11,7 +11,7 @@ interface Props {
initialValues: ProxyLocation[];
name?: string;
}
export function LocationsFields({ initialValues, name = "items" }: Props) {
export function LocationsFields({ initialValues, name = "locations" }: Props) {
const [values, setValues] = useState<ProxyLocation[]>(initialValues || []);
const { setFieldValue } = useFormikContext();
const [advVisible, setAdvVisible] = useState<number[]>([]);

View File

@@ -30,6 +30,7 @@ export function NginxConfigField({
fontFamily: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
borderRadius: "0.3rem",
minHeight: "200px",
backgroundColor: "var(--tblr-bg-surface-dark)",
}}
{...field}
/>

View File

@@ -3,7 +3,7 @@ import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend";
import { useCertificates } from "src/hooks";
import { DateTimeFormat, T } from "src/locale";
import { DateTimeFormat, intl, T } from "src/locale";
interface CertOption {
readonly value: number | "new";
@@ -75,9 +75,7 @@ export function SSLCertificateField({
data?.map((cert: Certificate) => ({
value: cert.id,
label: cert.niceName,
subLabel: `${cert.provider === "letsencrypt" ? "Let's Encrypt" : cert.provider} &mdash; Expires: ${
cert.expiresOn ? DateTimeFormat(cert.expiresOn) : "N/A"
}`,
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} &mdash; ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? DateTimeFormat(cert.expiresOn) : "N/A" })}`,
icon: <IconShield size={14} className="text-pink" />,
})) || [];
@@ -85,8 +83,8 @@ export function SSLCertificateField({
if (allowNew) {
options?.unshift({
value: "new",
label: "Request a new Certificate",
subLabel: "with Let's Encrypt",
label: intl.formatMessage({ id: "certificates.request.title" }),
subLabel: intl.formatMessage({ id: "certificates.request.subtitle" }),
icon: <IconShield size={14} className="text-lime" />,
});
}
@@ -95,8 +93,10 @@ export function SSLCertificateField({
if (!required) {
options?.unshift({
value: 0,
label: "None",
subLabel: forHttp ? "This host will not use HTTPS" : "No certificate assigned",
label: intl.formatMessage({ id: "certificate.none.title" }),
subLabel: forHttp
? intl.formatMessage({ id: "certificate.none.subtitle.for-http" })
: intl.formatMessage({ id: "certificate.none.subtitle" }),
icon: <IconShield size={14} className="text-red" />,
});
}

View File

@@ -136,8 +136,8 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
</label>
)}
</Field>
{requireDomainNames ? <DomainNamesField /> : null}
{dnsChallenge ? <DNSProviderFields /> : null}
{requireDomainNames ? <DomainNamesField isWildcardPermitted dnsProviderWildcardSupported /> : null}
{dnsChallenge ? <DNSProviderFields showBoundaryBox /> : null}
</>
) : null}
</div>

View File

@@ -64,7 +64,7 @@ export function EventFormatter({ row }: Props) {
<div className="flex-fill">
<div className="font-weight-medium">
{getIcon(row)}
<T id={`event.${row.action}-${row.objectType}`} />
<T id={`object.event.${row.action}`} tData={{ object: row.objectType }} />
&mdash; <span className="badge">{getEventValue(row)}</span>
</div>
<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div>

View File

@@ -6,6 +6,7 @@
"access-list.help.rules-order": "Note that the allow and deny directives will be applied in the order they are defined.",
"access-list.pass-auth": "Pass Auth to Upstream",
"access-list.public": "Publicly Accessible",
"access-list.public.subtitle": "No basic auth required",
"access-list.satisfy-any": "Satisfy Any",
"access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}",
"access-lists": "Access Lists",
@@ -21,8 +22,28 @@
"auditlogs": "Audit Logs",
"cancel": "Cancel",
"certificate": "Certificate",
"certificate.none.subtitle": "No certificate assigned",
"certificate.none.subtitle.for-http": "This host will not use HTTPS",
"certificate.none.title": "None",
"certificates": "Certificates",
"certificates.custom": "Custom Certificate",
"certificates.dns.credentials": "Credentials File Content",
"certificates.dns.credentials-note": "This plugin requires a configuration file containing an API token or other credentials for your provider",
"certificates.dns.credentials-warning": "This data will be stored as plaintext in the database and in a file!",
"certificates.dns.propagation-seconds": "Propagation Seconds",
"certificates.dns.propagation-seconds-note": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.",
"certificates.dns.provider": "DNS Provider",
"certificates.dns.warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.",
"certificates.http.reachability-404": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.",
"certificates.http.reachability-failed-to-check": "Failed to check the reachability due to a communication error with site24x7.com.",
"certificates.http.reachability-not-resolved": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.",
"certificates.http.reachability-ok": "Your server is reachable and creating certificates should be possible.",
"certificates.http.reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
"certificates.http.reachability-wrong-data": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
"certificates.http.test-results": "Test Results",
"certificates.http.warning": "These domains must be already configured to point to this installation.",
"certificates.request.subtitle": "with Let's Encrypt",
"certificates.request.title": "Request a new Certificate",
"column.access": "Access",
"column.authorization": "Authorization",
"column.authorizations": "Authorizations",
@@ -74,22 +95,7 @@
"error.max-domains": "Too many domains, max is {max}",
"error.passwords-must-match": "Passwords must match",
"error.required": "This is required",
"event.created-access-list": "Created Access List",
"event.created-dead-host": "Created 404 Host",
"event.created-redirection-host": "Created Redirection Host",
"event.created-stream": "Created Stream",
"event.created-user": "Created User",
"event.deleted-dead-host": "Deleted 404 Host",
"event.deleted-stream": "Deleted Stream",
"event.deleted-user": "Deleted User",
"event.disabled-dead-host": "Disabled 404 Host",
"event.disabled-redirection-host": "Disabled Redirection Host",
"event.disabled-stream": "Disabled Stream",
"event.enabled-dead-host": "Enabled 404 Host",
"event.enabled-redirection-host": "Enabled Redirection Host",
"event.enabled-stream": "Enabled Stream",
"event.updated-redirection-host": "Updated Redirection Host",
"event.updated-user": "Updated User",
"expires.on": "Expires: {date}",
"footer.github-fork": "Fork me on Github",
"host.flags.block-exploits": "Block Common Exploits",
"host.flags.cache-assets": "Cache Assets",
@@ -101,6 +107,8 @@
"hosts": "Hosts",
"http-only": "HTTP Only",
"lets-encrypt": "Let's Encrypt",
"lets-encrypt-via-dns": "Let's Encrypt via DNS",
"lets-encrypt-via-http": "Let's Encrypt via HTTP",
"loading": "Loading…",
"login.title": "Login to your account",
"nginx-config.label": "Custom Nginx Configuration",
@@ -121,6 +129,11 @@
"object.delete.content": "Are you sure you want to delete this {object}?",
"object.edit": "Edit {object}",
"object.empty": "There are no {objects}",
"object.event.created": "Created {object}",
"object.event.deleted": "Deleted {object}",
"object.event.disabled": "Disabled {object}",
"object.event.enabled": "Enabled {object}",
"object.event.updated": "Updated {object}",
"offline": "Offline",
"online": "Online",
"options": "Options",
@@ -158,6 +171,7 @@
"streams.count": "{count} {count, plural, one {Stream} other {Streams}}",
"streams.tcp": "TCP",
"streams.udp": "UDP",
"test": "Test",
"user": "User",
"user.change-password": "Change Password",
"user.confirm-password": "Confirm Password",

View File

@@ -20,6 +20,9 @@
"access-list.public": {
"defaultMessage": "Publicly Accessible"
},
"access-list.public.subtitle": {
"defaultMessage": "No basic auth required"
},
"access-list.satisfy-any": {
"defaultMessage": "Satisfy Any"
},
@@ -65,12 +68,72 @@
"certificate": {
"defaultMessage": "Certificate"
},
"certificate.none.subtitle": {
"defaultMessage": "No certificate assigned"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "This host will not use HTTPS"
},
"certificate.none.title": {
"defaultMessage": "None"
},
"certificates": {
"defaultMessage": "Certificates"
},
"certificates.custom": {
"defaultMessage": "Custom Certificate"
},
"certificates.dns.credentials": {
"defaultMessage": "Credentials File Content"
},
"certificates.dns.credentials-note": {
"defaultMessage": "This plugin requires a configuration file containing an API token or other credentials for your provider"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "This data will be stored as plaintext in the database and in a file!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propagation Seconds"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation."
},
"certificates.dns.provider": {
"defaultMessage": "DNS Provider"
},
"certificates.dns.warning": {
"defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation."
},
"certificates.http.reachability-404": {
"defaultMessage": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Your server is reachable and creating certificates should be possible."
},
"certificates.http.reachability-other": {
"defaultMessage": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running."
},
"certificates.http.test-results": {
"defaultMessage": "Test Results"
},
"certificates.http.warning": {
"defaultMessage": "These domains must be already configured to point to this installation."
},
"certificates.request.subtitle": {
"defaultMessage": "with Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Request a new Certificate"
},
"column.access": {
"defaultMessage": "Access"
},
@@ -224,53 +287,8 @@
"error.required": {
"defaultMessage": "This is required"
},
"event.created-access-list": {
"defaultMessage": "Created Access List"
},
"event.created-dead-host": {
"defaultMessage": "Created 404 Host"
},
"event.created-redirection-host": {
"defaultMessage": "Created Redirection Host"
},
"event.created-stream": {
"defaultMessage": "Created Stream"
},
"event.created-user": {
"defaultMessage": "Created User"
},
"event.deleted-dead-host": {
"defaultMessage": "Deleted 404 Host"
},
"event.deleted-stream": {
"defaultMessage": "Deleted Stream"
},
"event.deleted-user": {
"defaultMessage": "Deleted User"
},
"event.disabled-dead-host": {
"defaultMessage": "Disabled 404 Host"
},
"event.disabled-redirection-host": {
"defaultMessage": "Disabled Redirection Host"
},
"event.disabled-stream": {
"defaultMessage": "Disabled Stream"
},
"event.enabled-dead-host": {
"defaultMessage": "Enabled 404 Host"
},
"event.enabled-redirection-host": {
"defaultMessage": "Enabled Redirection Host"
},
"event.enabled-stream": {
"defaultMessage": "Enabled Stream"
},
"event.updated-redirection-host": {
"defaultMessage": "Updated Redirection Host"
},
"event.updated-user": {
"defaultMessage": "Updated User"
"expires.on": {
"defaultMessage": "Expires: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork me on Github"
@@ -305,6 +323,12 @@
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Loading…"
},
@@ -365,6 +389,21 @@
"object.empty": {
"defaultMessage": "There are no {objects}"
},
"object.event.created": {
"defaultMessage": "Created {object}"
},
"object.event.deleted": {
"defaultMessage": "Deleted {object}"
},
"object.event.disabled": {
"defaultMessage": "Disabled {object}"
},
"object.event.enabled": {
"defaultMessage": "Enabled {object}"
},
"object.event.updated": {
"defaultMessage": "Updated {object}"
},
"offline": {
"defaultMessage": "Offline"
},
@@ -476,6 +515,9 @@
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"user": {
"defaultMessage": "User"
},

View File

@@ -0,0 +1,88 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { Button, DomainNamesField } from "src/components";
import { useSetProxyHost } from "src/hooks";
import { T } from "src/locale";
import { showObjectSuccess } from "src/notifications";
const showCustomCertificateModal = () => {
EasyModal.show(CustomCertificateModal);
};
const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
const { mutate: setProxyHost } = useSetProxyHost();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
setProxyHost(values, {
onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => {
showObjectSuccess("certificate", "saved");
remove();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
return (
<Modal show={visible} onHide={remove}>
<Formik
initialValues={
{
domainNames: [],
} as any
}
onSubmit={onSubmit}
>
{() => (
<Form>
<Modal.Header closeButton>
<Modal.Title>
<T id="object.add" tData={{ object: "certificate" }} />
</Modal.Title>
</Modal.Header>
<Modal.Body className="p-0">
<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
{errorMsg}
</Alert>
<div className="card m-0 border-0">
<div className="card-header">asd</div>
<div className="card-body">
<DomainNamesField />
</div>
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
type="submit"
actionType="primary"
className="ms-auto bg-lime"
data-bs-dismiss="modal"
isLoading={isSubmitting}
disabled={isSubmitting}
>
<T id="save" />
</Button>
</Modal.Footer>
</Form>
)}
</Formik>
</Modal>
);
});
export { showCustomCertificateModal };

View File

@@ -0,0 +1,89 @@
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { createCertificate } from "src/api/backend";
import { Button, DNSProviderFields, DomainNamesField } from "src/components";
import { T } from "src/locale";
import { showObjectSuccess } from "src/notifications";
const showDNSCertificateModal = () => {
EasyModal.show(DNSCertificateModal);
};
const DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
try {
await createCertificate(values);
showObjectSuccess("certificate", "saved");
remove();
} catch (err: any) {
setErrorMsg(<T id={err.message} />);
}
queryClient.invalidateQueries({ queryKey: ["certificates"] });
setIsSubmitting(false);
setSubmitting(false);
};
return (
<Modal show={visible} onHide={remove}>
<Formik
initialValues={
{
domainNames: [],
provider: "letsencrypt",
} as any
}
onSubmit={onSubmit}
>
{() => (
<Form>
<Modal.Header closeButton>
<Modal.Title>
<T id="object.add" tData={{ object: "lets-encrypt-via-dns" }} />
</Modal.Title>
</Modal.Header>
<Modal.Body className="p-0">
<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
{errorMsg}
</Alert>
<div className="card m-0 border-0">
<div className="card-body">
<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />
<DNSProviderFields />
</div>
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
type="submit"
actionType="primary"
className="ms-auto bg-pink"
data-bs-dismiss="modal"
isLoading={isSubmitting}
disabled={isSubmitting}
>
<T id="save" />
</Button>
</Modal.Footer>
</Form>
)}
</Formik>
</Modal>
);
});
export { showDNSCertificateModal };

View File

@@ -131,7 +131,7 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
<div className="card-body">
<div className="tab-content">
<div className="tab-pane active show" id="tab-details" role="tabpanel">
<DomainNamesField isWildcardPermitted />
<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />
</div>
<div className="tab-pane" id="tab-ssl" role="tabpanel">
<SSLCertificateField

View File

@@ -1,3 +1,4 @@
import CodeEditor from "@uiw/react-textarea-code-editor";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
@@ -39,9 +40,22 @@ const EventDetailsModal = EasyModal.create(({ id, visible, remove }: Props) => {
<EventFormatter row={data} />
</div>
<hr className="mt-4 mb-3" />
<pre>
<code>{JSON.stringify(data.meta, null, 2)}</code>
</pre>
<CodeEditor
language="json"
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",
backgroundColor: "var(--tblr-bg-surface-dark)",
}}
readOnly
value={JSON.stringify(data.meta, null, 2)}
/>
</div>
</Modal.Body>
<Modal.Footer>

View File

@@ -0,0 +1,188 @@
import { IconAlertTriangle } from "@tabler/icons-react";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { createCertificate, testHttpCertificate } from "src/api/backend";
import { Button, DomainNamesField } from "src/components";
import { T } from "src/locale";
import { showObjectSuccess } from "src/notifications";
const showHTTPCertificateModal = () => {
EasyModal.show(HTTPCertificateModal);
};
const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [domains, setDomains] = useState([] as string[]);
const [isTesting, setIsTesting] = useState(false);
const [testResults, setTestResults] = useState(null as Record<string, string> | null);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
try {
await createCertificate(values);
showObjectSuccess("certificate", "saved");
remove();
} catch (err: any) {
setErrorMsg(<T id={err.message} />);
}
setIsSubmitting(false);
setSubmitting(false);
};
const handleTest = async () => {
setIsTesting(true);
setErrorMsg(null);
setTestResults(null);
try {
const result = await testHttpCertificate(domains);
setTestResults(result);
} catch (err: any) {
setErrorMsg(<T id={err.message} />);
}
setIsTesting(false);
};
const parseTestResults = () => {
const elms = [];
for (const domain in testResults) {
const status = testResults[domain];
if (status === "ok") {
elms.push(
<p>
<strong>{domain}:</strong> <T id="certificates.http.reachability-ok" />
</p>,
);
} else {
if (status === "no-host") {
elms.push(
<p>
<strong>{domain}:</strong> <T id="certificates.http.reachability-not-resolved" />
</p>,
);
} else if (status === "failed") {
elms.push(
<p>
<strong>{domain}:</strong> <T id="certificates.http.reachability-failed-to-check" />
</p>,
);
} else if (status === "404") {
elms.push(
<p>
<strong>{domain}:</strong> <T id="certificates.http.reachability-404" />
</p>,
);
} else if (status === "wrong-data") {
elms.push(
<p>
<strong>{domain}:</strong> <T id="certificates.http.reachability-wrong-data" />
</p>,
);
} else if (status.startsWith("other:")) {
const code = status.substring(6);
elms.push(
<p>
<strong>{domain}:</strong> <T id="certificates.http.reachability-other" data={{ code }} />
</p>,
);
} else {
// This should never happen
elms.push(
<p>
<strong>{domain}:</strong> ?
</p>,
);
}
}
}
return <>{elms}</>;
};
return (
<Modal show={visible} onHide={remove}>
<Formik
initialValues={
{
domainNames: [],
provider: "letsencrypt",
} as any
}
onSubmit={onSubmit}
>
{() => (
<Form>
<Modal.Header closeButton>
<Modal.Title>
<T id="object.add" tData={{ object: "lets-encrypt-via-http" }} />
</Modal.Title>
</Modal.Header>
<Modal.Body className="p-0">
<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
{errorMsg}
</Alert>
<div className="card m-0 border-0">
<div className="card-body">
<p className="text-warning">
<IconAlertTriangle size={16} className="me-1" />
<T id="certificates.http.warning" />
</p>
<DomainNamesField
onChange={(doms) => {
setDomains(doms);
setTestResults(null);
}}
/>
</div>
{testResults ? (
<div className="card-footer">
<h5>
<T id="certificates.http.test-results" />
</h5>
{parseTestResults()}
</div>
) : null}
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting || isTesting}>
<T id="cancel" />
</Button>
<div className="ms-auto">
<Button
type="button"
actionType="secondary"
className="me-3"
data-bs-dismiss="modal"
isLoading={isTesting}
disabled={isSubmitting || domains.length === 0}
onClick={handleTest}
>
<T id="test" />
</Button>
<Button
type="submit"
actionType="primary"
className="bg-pink"
data-bs-dismiss="modal"
isLoading={isSubmitting}
disabled={isSubmitting || isTesting}
>
<T id="save" />
</Button>
</div>
</Modal.Footer>
</Form>
)}
</Formik>
</Modal>
);
});
export { showHTTPCertificateModal };

View File

@@ -159,7 +159,7 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
<div className="card-body">
<div className="tab-content">
<div className="tab-pane active show" id="tab-details" role="tabpanel">
<DomainNamesField isWildcardPermitted />
<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />
<div className="row">
<div className="col-md-3">
<Field name="forwardScheme">

View File

@@ -144,7 +144,7 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
<div className="card-body">
<div className="tab-content">
<div className="tab-pane active show" id="tab-details" role="tabpanel">
<DomainNamesField isWildcardPermitted />
<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />
<div className="row">
<div className="col-md-4">
<Field name="forwardScheme">

View File

@@ -1,8 +1,11 @@
export * from "./AccessListModal";
export * from "./ChangePasswordModal";
export * from "./CustomCertificateModal";
export * from "./DeadHostModal";
export * from "./DeleteConfirmModal";
export * from "./DNSCertificateModal";
export * from "./EventDetailsModal";
export * from "./HTTPCertificateModal";
export * from "./PermissionsModal";
export * from "./ProxyHostModal";
export * from "./RedirectionHostModal";

View File

@@ -5,6 +5,7 @@ import type { Certificate } from "src/api/backend";
import { DomainsFormatter, EmptyData, GravatarFormatter } from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
interface Props {
data: Certificate[];
@@ -121,17 +122,28 @@ export default function Table({ data, isFetching }: Props) {
href="#"
onClick={(e) => {
e.preventDefault();
// onNew();
showHTTPCertificateModal();
}}
>
<T id="lets-encrypt" />
<T id="lets-encrypt-via-http" />
</a>
<a
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
// onNewCustom();
showDNSCertificateModal();
}}
>
<T id="lets-encrypt-via-dns" />
</a>
<div className="dropdown-divider" />
<a
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
showCustomCertificateModal();
}}
>
<T id="certificates.custom" />

View File

@@ -3,6 +3,7 @@ import Alert from "react-bootstrap/Alert";
import { LoadingPage } from "src/components";
import { useCertificates } from "src/hooks";
import { T } from "src/locale";
import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
import Table from "./Table";
export default function TableWrapper() {
@@ -54,10 +55,35 @@ export default function TableWrapper() {
<T id="object.add" tData={{ object: "certificate" }} />
</button>
<div className="dropdown-menu">
<a className="dropdown-item" href="#">
<T id="lets-encrypt" />
<a
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
showHTTPCertificateModal();
}}
>
<T id="lets-encrypt-via-http" />
</a>
<a className="dropdown-item" href="#">
<a
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
showDNSCertificateModal();
}}
>
<T id="lets-encrypt-via-dns" />
</a>
<div className="dropdown-divider" />
<a
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
showCustomCertificateModal();
}}
>
<T id="certificates.custom" />
</a>
</div>

View File

@@ -118,15 +118,9 @@ const Dashboard = () => {
- check mobile
- add help docs for host types
- REDO SCREENSHOTS in docs folder
- translations for:
- src/components/Form/AccessField.tsx
- src/components/Form/SSLCertificateField.tsx
- src/components/Form/DNSProviderFields.tsx
- search codebase for "TODO"
- update documentation to add development notes for translations
- use syntax highligting for audit logs json output
- double check output of access field selection on proxy host dialog, after access lists are completed
- proxy host custom locations dialog
- check permissions in all places
More for api, then implement here:

View File

@@ -37,13 +37,12 @@ export default function TableWrapper() {
let filtered = null;
if (search && data) {
filtered = data?.filter((_item) => {
return true;
// TODO
// item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||
// item.forwardDomainName.toLowerCase().includes(search)
// );
});
filtered = data?.filter(
(item) =>
item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||
item.forwardHost.toLowerCase().includes(search) ||
`${item.forwardPort}`.includes(search),
);
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");