Jamie Curnow пре 1 месец
родитељ
комит
83a2c79e16

+ 2 - 7
frontend/src/api/backend/uploadCertificate.ts

@@ -1,14 +1,9 @@
 import * as api from "./base";
 import type { Certificate } from "./models";
 
-export async function uploadCertificate(
-	id: number,
-	certificate: string,
-	certificateKey: string,
-	intermediateCertificate?: string,
-): Promise<Certificate> {
+export async function uploadCertificate(id: number, data: FormData): Promise<Certificate> {
 	return await api.post({
 		url: `/nginx/certificates/${id}/upload`,
-		data: { certificate, certificateKey, intermediateCertificate },
+		data,
 	});
 }

+ 2 - 6
frontend/src/api/backend/validateCertificate.ts

@@ -1,13 +1,9 @@
 import * as api from "./base";
 import type { ValidatedCertificateResponse } from "./responseTypes";
 
-export async function validateCertificate(
-	certificate: string,
-	certificateKey: string,
-	intermediateCertificate?: string,
-): Promise<ValidatedCertificateResponse> {
+export async function validateCertificate(data: FormData): Promise<ValidatedCertificateResponse> {
 	return await api.post({
 		url: "/nginx/certificates/validate",
-		data: { certificate, certificateKey, intermediateCertificate },
+		data,
 	});
 }

+ 22 - 6
frontend/src/components/Table/Formatter/DomainsFormatter.tsx

@@ -1,8 +1,10 @@
+import type { ReactNode } from "react";
 import { DateTimeFormat, T } from "src/locale";
 
 interface Props {
 	domains: string[];
 	createdOn?: string;
+	niceName?: string;
 }
 
 const DomainLink = ({ domain }: { domain: string }) => {
@@ -24,14 +26,28 @@ const DomainLink = ({ domain }: { domain: string }) => {
 	);
 };
 
-export function DomainsFormatter({ domains, createdOn }: Props) {
+export function DomainsFormatter({ domains, createdOn, niceName }: Props) {
+	const elms: ReactNode[] = [];
+	if (domains.length === 0 && !niceName) {
+		elms.push(
+			<span key="nice-name" className="badge bg-danger-lt me-2">
+				Unknown
+			</span>,
+		);
+	}
+	if (niceName) {
+		elms.push(
+			<span key="nice-name" className="badge bg-info-lt me-2">
+				{niceName}
+			</span>,
+		);
+	}
+
+	domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} />));
+
 	return (
 		<div className="flex-fill">
-			<div className="font-weight-medium">
-				{domains.map((domain: string) => (
-					<DomainLink key={domain} domain={domain} />
-				))}
-			</div>
+			<div className="font-weight-medium">{...elms}</div>
 			{createdOn ? (
 				<div className="text-secondary mt-1">
 					<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />

+ 4 - 0
frontend/src/locale/lang/en.json

@@ -24,6 +24,9 @@
   "auditlogs": "Audit Logs",
   "cancel": "Cancel",
   "certificate": "Certificate",
+  "certificate.custom-certificate": "Certificate",
+  "certificate.custom-certificate-key": "Certificate Key",
+  "certificate.custom-intermediate": "Intermediate Certificate",
   "certificate.in-use": "In Use",
   "certificate.none.subtitle": "No certificate assigned",
   "certificate.none.subtitle.for-http": "This host will not use HTTPS",
@@ -31,6 +34,7 @@
   "certificate.not-in-use": "Not Used",
   "certificates": "Certificates",
   "certificates.custom": "Custom Certificate",
+  "certificates.custom.warning": "Key files protected with a passphrase are not supported.",
   "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!",

+ 12 - 0
frontend/src/locale/src/en.json

@@ -74,6 +74,15 @@
 	"certificate": {
 		"defaultMessage": "Certificate"
 	},
+	"certificate.custom-certificate": {
+		"defaultMessage": "Certificate"
+	},
+	"certificate.custom-certificate-key": {
+		"defaultMessage": "Certificate Key"
+	},
+	"certificate.custom-intermediate": {
+		"defaultMessage": "Intermediate Certificate"
+	},
 	"certificate.in-use": {
 		"defaultMessage": "In Use"
 	},
@@ -95,6 +104,9 @@
 	"certificates.custom": {
 		"defaultMessage": "Custom Certificate"
 	},
+	"certificates.custom.warning": {
+		"defaultMessage": "Key files protected with a passphrase are not supported."
+	},
 	"certificates.dns.credentials": {
 		"defaultMessage": "Credentials File Content"
 	},

+ 164 - 20
frontend/src/modals/CustomCertificateModal.tsx

@@ -1,11 +1,14 @@
+import { IconAlertTriangle } from "@tabler/icons-react";
+import { useQueryClient } from "@tanstack/react-query";
 import EasyModal, { type InnerModalProps } from "ez-modal-react";
-import { Form, Formik } from "formik";
+import { Field, 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 { type Certificate, createCertificate, uploadCertificate, validateCertificate } from "src/api/backend";
+import { Button } from "src/components";
 import { T } from "src/locale";
+import { validateString } from "src/modules/Validations";
 import { showObjectSuccess } from "src/notifications";
 
 const showCustomCertificateModal = () => {
@@ -13,7 +16,7 @@ const showCustomCertificateModal = () => {
 };
 
 const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
-	const { mutate: setProxyHost } = useSetProxyHost();
+	const queryClient = useQueryClient();
 	const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
 	const [isSubmitting, setIsSubmitting] = useState(false);
 
@@ -22,17 +25,35 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
 		setIsSubmitting(true);
 		setErrorMsg(null);
 
-		setProxyHost(values, {
-			onError: (err: any) => setErrorMsg(<T id={err.message} />),
-			onSuccess: () => {
-				showObjectSuccess("certificate", "saved");
-				remove();
-			},
-			onSettled: () => {
-				setIsSubmitting(false);
-				setSubmitting(false);
-			},
-		});
+		try {
+			const { niceName, provider, certificate, certificateKey, intermediateCertificate } = values;
+			const formData = new FormData();
+
+			formData.append("certificate", certificate);
+			formData.append("certificate_key", certificateKey);
+			if (intermediateCertificate !== null) {
+				formData.append("intermediate_certificate", intermediateCertificate);
+			}
+
+			// Validate
+			await validateCertificate(formData);
+
+			// Create certificate, as other without anything else
+			const cert = await createCertificate({ niceName, provider } as Certificate);
+
+			// Upload the certificates to the created certificate
+			await uploadCertificate(cert.id, formData);
+
+			// Success
+			showObjectSuccess("certificate", "saved");
+			remove();
+		} catch (err: any) {
+			setErrorMsg(<T id={err.message} />);
+		}
+
+		queryClient.invalidateQueries({ queryKey: ["certificates"] });
+		setIsSubmitting(false);
+		setSubmitting(false);
 	};
 
 	return (
@@ -40,7 +61,11 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
 			<Formik
 				initialValues={
 					{
-						domainNames: [],
+						niceName: "",
+						provider: "other",
+						certificate: null,
+						certificateKey: null,
+						intermediateCertificate: null,
 					} as any
 				}
 				onSubmit={onSubmit}
@@ -49,7 +74,7 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
 					<Form>
 						<Modal.Header closeButton>
 							<Modal.Title>
-								<T id="object.add" tData={{ object: "certificate" }} />
+								<T id="object.add" tData={{ object: "lets-encrypt-via-dns" }} />
 							</Modal.Title>
 						</Modal.Header>
 						<Modal.Body className="p-0">
@@ -57,9 +82,128 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
 								{errorMsg}
 							</Alert>
 							<div className="card m-0 border-0">
-								<div className="card-header">asd</div>
 								<div className="card-body">
-									<DomainNamesField />
+									<p className="text-warning">
+										<IconAlertTriangle size={16} className="me-1" />
+										<T id="certificates.custom.warning" />
+									</p>
+									<Field name="niceName" validate={validateString(1, 255)}>
+										{({ field, form }: any) => (
+											<div className="mb-3">
+												<label htmlFor="niceName" className="form-label">
+													<T id="column.name" />
+												</label>
+												<input
+													id="niceName"
+													type="text"
+													required
+													autoComplete="off"
+													className="form-control"
+													{...field}
+												/>
+												{form.errors.niceName ? (
+													<div className="invalid-feedback">
+														{form.errors.niceName && form.touched.niceName
+															? form.errors.niceName
+															: null}
+													</div>
+												) : null}
+											</div>
+										)}
+									</Field>
+									<Field name="certificateKey">
+										{({ field, form }: any) => (
+											<div className="mb-3">
+												<label htmlFor="certificateKey" className="form-label">
+													<T id="certificate.custom-certificate-key" />
+												</label>
+												<input
+													id="certificateKey"
+													type="file"
+													required
+													autoComplete="off"
+													className="form-control"
+													onChange={(event) => {
+														form.setFieldValue(
+															field.name,
+															event.currentTarget.files?.length
+																? event.currentTarget.files[0]
+																: null,
+														);
+													}}
+												/>
+												{form.errors.certificateKey ? (
+													<div className="invalid-feedback">
+														{form.errors.certificateKey && form.touched.certificateKey
+															? form.errors.certificateKey
+															: null}
+													</div>
+												) : null}
+											</div>
+										)}
+									</Field>
+									<Field name="certificate">
+										{({ field, form }: any) => (
+											<div className="mb-3">
+												<label htmlFor="certificate" className="form-label">
+													<T id="certificate.custom-certificate" />
+												</label>
+												<input
+													id="certificate"
+													type="file"
+													required
+													autoComplete="off"
+													className="form-control"
+													onChange={(event) => {
+														form.setFieldValue(
+															field.name,
+															event.currentTarget.files?.length
+																? event.currentTarget.files[0]
+																: null,
+														);
+													}}
+												/>
+												{form.errors.certificate ? (
+													<div className="invalid-feedback">
+														{form.errors.certificate && form.touched.certificate
+															? form.errors.certificate
+															: null}
+													</div>
+												) : null}
+											</div>
+										)}
+									</Field>
+									<Field name="intermediateCertificate">
+										{({ field, form }: any) => (
+											<div className="mb-3">
+												<label htmlFor="intermediateCertificate" className="form-label">
+													<T id="certificate.custom-intermediate" />
+												</label>
+												<input
+													id="intermediateCertificate"
+													type="file"
+													autoComplete="off"
+													className="form-control"
+													onChange={(event) => {
+														form.setFieldValue(
+															field.name,
+															event.currentTarget.files?.length
+																? event.currentTarget.files[0]
+																: null,
+														);
+													}}
+												/>
+												{form.errors.intermediateCertificate ? (
+													<div className="invalid-feedback">
+														{form.errors.intermediateCertificate &&
+														form.touched.intermediateCertificate
+															? form.errors.intermediateCertificate
+															: null}
+													</div>
+												) : null}
+											</div>
+										)}
+									</Field>
 								</div>
 							</div>
 						</Modal.Body>
@@ -70,7 +214,7 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
 							<Button
 								type="submit"
 								actionType="primary"
-								className="ms-auto bg-lime"
+								className="ms-auto bg-pink"
 								data-bs-dismiss="modal"
 								isLoading={isSubmitting}
 								disabled={isSubmitting}

+ 10 - 1
frontend/src/pages/Certificates/Table.tsx

@@ -40,7 +40,13 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
 				header: intl.formatMessage({ id: "column.name" }),
 				cell: (info: any) => {
 					const value = info.getValue();
-					return <DomainsFormatter domains={value.domainNames} createdOn={value.createdOn} />;
+					return (
+						<DomainsFormatter
+							domains={value.domainNames}
+							createdOn={value.createdOn}
+							niceName={value.niceName}
+						/>
+					);
 				},
 			}),
 			columnHelper.accessor((row: any) => row.provider, {
@@ -50,6 +56,9 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
 					if (info.getValue() === "letsencrypt") {
 						return <T id="lets-encrypt" />;
 					}
+					if (info.getValue() === "other") {
+						return <T id="certificates.custom" />;
+					}
 					return <T id={info.getValue()} />;
 				},
 			}),