Skip to content

Commit 83a2c79

Browse files
committed
Custom certificate upload
1 parent 0de26f2 commit 83a2c79

File tree

7 files changed

+216
-40
lines changed

7 files changed

+216
-40
lines changed
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import * as api from "./base";
22
import type { Certificate } from "./models";
33

4-
export async function uploadCertificate(
5-
id: number,
6-
certificate: string,
7-
certificateKey: string,
8-
intermediateCertificate?: string,
9-
): Promise<Certificate> {
4+
export async function uploadCertificate(id: number, data: FormData): Promise<Certificate> {
105
return await api.post({
116
url: `/nginx/certificates/${id}/upload`,
12-
data: { certificate, certificateKey, intermediateCertificate },
7+
data,
138
});
149
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import * as api from "./base";
22
import type { ValidatedCertificateResponse } from "./responseTypes";
33

4-
export async function validateCertificate(
5-
certificate: string,
6-
certificateKey: string,
7-
intermediateCertificate?: string,
8-
): Promise<ValidatedCertificateResponse> {
4+
export async function validateCertificate(data: FormData): Promise<ValidatedCertificateResponse> {
95
return await api.post({
106
url: "/nginx/certificates/validate",
11-
data: { certificate, certificateKey, intermediateCertificate },
7+
data,
128
});
139
}

frontend/src/components/Table/Formatter/DomainsFormatter.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import type { ReactNode } from "react";
12
import { DateTimeFormat, T } from "src/locale";
23

34
interface Props {
45
domains: string[];
56
createdOn?: string;
7+
niceName?: string;
68
}
79

810
const DomainLink = ({ domain }: { domain: string }) => {
@@ -24,14 +26,28 @@ const DomainLink = ({ domain }: { domain: string }) => {
2426
);
2527
};
2628

27-
export function DomainsFormatter({ domains, createdOn }: Props) {
29+
export function DomainsFormatter({ domains, createdOn, niceName }: Props) {
30+
const elms: ReactNode[] = [];
31+
if (domains.length === 0 && !niceName) {
32+
elms.push(
33+
<span key="nice-name" className="badge bg-danger-lt me-2">
34+
Unknown
35+
</span>,
36+
);
37+
}
38+
if (niceName) {
39+
elms.push(
40+
<span key="nice-name" className="badge bg-info-lt me-2">
41+
{niceName}
42+
</span>,
43+
);
44+
}
45+
46+
domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} />));
47+
2848
return (
2949
<div className="flex-fill">
30-
<div className="font-weight-medium">
31-
{domains.map((domain: string) => (
32-
<DomainLink key={domain} domain={domain} />
33-
))}
34-
</div>
50+
<div className="font-weight-medium">{...elms}</div>
3551
{createdOn ? (
3652
<div className="text-secondary mt-1">
3753
<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />

frontend/src/locale/lang/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
"auditlogs": "Audit Logs",
2525
"cancel": "Cancel",
2626
"certificate": "Certificate",
27+
"certificate.custom-certificate": "Certificate",
28+
"certificate.custom-certificate-key": "Certificate Key",
29+
"certificate.custom-intermediate": "Intermediate Certificate",
2730
"certificate.in-use": "In Use",
2831
"certificate.none.subtitle": "No certificate assigned",
2932
"certificate.none.subtitle.for-http": "This host will not use HTTPS",
3033
"certificate.none.title": "None",
3134
"certificate.not-in-use": "Not Used",
3235
"certificates": "Certificates",
3336
"certificates.custom": "Custom Certificate",
37+
"certificates.custom.warning": "Key files protected with a passphrase are not supported.",
3438
"certificates.dns.credentials": "Credentials File Content",
3539
"certificates.dns.credentials-note": "This plugin requires a configuration file containing an API token or other credentials for your provider",
3640
"certificates.dns.credentials-warning": "This data will be stored as plaintext in the database and in a file!",

frontend/src/locale/src/en.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@
7474
"certificate": {
7575
"defaultMessage": "Certificate"
7676
},
77+
"certificate.custom-certificate": {
78+
"defaultMessage": "Certificate"
79+
},
80+
"certificate.custom-certificate-key": {
81+
"defaultMessage": "Certificate Key"
82+
},
83+
"certificate.custom-intermediate": {
84+
"defaultMessage": "Intermediate Certificate"
85+
},
7786
"certificate.in-use": {
7887
"defaultMessage": "In Use"
7988
},
@@ -95,6 +104,9 @@
95104
"certificates.custom": {
96105
"defaultMessage": "Custom Certificate"
97106
},
107+
"certificates.custom.warning": {
108+
"defaultMessage": "Key files protected with a passphrase are not supported."
109+
},
98110
"certificates.dns.credentials": {
99111
"defaultMessage": "Credentials File Content"
100112
},

frontend/src/modals/CustomCertificateModal.tsx

Lines changed: 164 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1+
import { IconAlertTriangle } from "@tabler/icons-react";
2+
import { useQueryClient } from "@tanstack/react-query";
13
import EasyModal, { type InnerModalProps } from "ez-modal-react";
2-
import { Form, Formik } from "formik";
4+
import { Field, Form, Formik } from "formik";
35
import { type ReactNode, useState } from "react";
46
import { Alert } from "react-bootstrap";
57
import Modal from "react-bootstrap/Modal";
6-
import { Button, DomainNamesField } from "src/components";
7-
import { useSetProxyHost } from "src/hooks";
8+
import { type Certificate, createCertificate, uploadCertificate, validateCertificate } from "src/api/backend";
9+
import { Button } from "src/components";
810
import { T } from "src/locale";
11+
import { validateString } from "src/modules/Validations";
912
import { showObjectSuccess } from "src/notifications";
1013

1114
const showCustomCertificateModal = () => {
1215
EasyModal.show(CustomCertificateModal);
1316
};
1417

1518
const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
16-
const { mutate: setProxyHost } = useSetProxyHost();
19+
const queryClient = useQueryClient();
1720
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
1821
const [isSubmitting, setIsSubmitting] = useState(false);
1922

@@ -22,25 +25,47 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
2225
setIsSubmitting(true);
2326
setErrorMsg(null);
2427

25-
setProxyHost(values, {
26-
onError: (err: any) => setErrorMsg(<T id={err.message} />),
27-
onSuccess: () => {
28-
showObjectSuccess("certificate", "saved");
29-
remove();
30-
},
31-
onSettled: () => {
32-
setIsSubmitting(false);
33-
setSubmitting(false);
34-
},
35-
});
28+
try {
29+
const { niceName, provider, certificate, certificateKey, intermediateCertificate } = values;
30+
const formData = new FormData();
31+
32+
formData.append("certificate", certificate);
33+
formData.append("certificate_key", certificateKey);
34+
if (intermediateCertificate !== null) {
35+
formData.append("intermediate_certificate", intermediateCertificate);
36+
}
37+
38+
// Validate
39+
await validateCertificate(formData);
40+
41+
// Create certificate, as other without anything else
42+
const cert = await createCertificate({ niceName, provider } as Certificate);
43+
44+
// Upload the certificates to the created certificate
45+
await uploadCertificate(cert.id, formData);
46+
47+
// Success
48+
showObjectSuccess("certificate", "saved");
49+
remove();
50+
} catch (err: any) {
51+
setErrorMsg(<T id={err.message} />);
52+
}
53+
54+
queryClient.invalidateQueries({ queryKey: ["certificates"] });
55+
setIsSubmitting(false);
56+
setSubmitting(false);
3657
};
3758

3859
return (
3960
<Modal show={visible} onHide={remove}>
4061
<Formik
4162
initialValues={
4263
{
43-
domainNames: [],
64+
niceName: "",
65+
provider: "other",
66+
certificate: null,
67+
certificateKey: null,
68+
intermediateCertificate: null,
4469
} as any
4570
}
4671
onSubmit={onSubmit}
@@ -49,17 +74,136 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
4974
<Form>
5075
<Modal.Header closeButton>
5176
<Modal.Title>
52-
<T id="object.add" tData={{ object: "certificate" }} />
77+
<T id="object.add" tData={{ object: "lets-encrypt-via-dns" }} />
5378
</Modal.Title>
5479
</Modal.Header>
5580
<Modal.Body className="p-0">
5681
<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
5782
{errorMsg}
5883
</Alert>
5984
<div className="card m-0 border-0">
60-
<div className="card-header">asd</div>
6185
<div className="card-body">
62-
<DomainNamesField />
86+
<p className="text-warning">
87+
<IconAlertTriangle size={16} className="me-1" />
88+
<T id="certificates.custom.warning" />
89+
</p>
90+
<Field name="niceName" validate={validateString(1, 255)}>
91+
{({ field, form }: any) => (
92+
<div className="mb-3">
93+
<label htmlFor="niceName" className="form-label">
94+
<T id="column.name" />
95+
</label>
96+
<input
97+
id="niceName"
98+
type="text"
99+
required
100+
autoComplete="off"
101+
className="form-control"
102+
{...field}
103+
/>
104+
{form.errors.niceName ? (
105+
<div className="invalid-feedback">
106+
{form.errors.niceName && form.touched.niceName
107+
? form.errors.niceName
108+
: null}
109+
</div>
110+
) : null}
111+
</div>
112+
)}
113+
</Field>
114+
<Field name="certificateKey">
115+
{({ field, form }: any) => (
116+
<div className="mb-3">
117+
<label htmlFor="certificateKey" className="form-label">
118+
<T id="certificate.custom-certificate-key" />
119+
</label>
120+
<input
121+
id="certificateKey"
122+
type="file"
123+
required
124+
autoComplete="off"
125+
className="form-control"
126+
onChange={(event) => {
127+
form.setFieldValue(
128+
field.name,
129+
event.currentTarget.files?.length
130+
? event.currentTarget.files[0]
131+
: null,
132+
);
133+
}}
134+
/>
135+
{form.errors.certificateKey ? (
136+
<div className="invalid-feedback">
137+
{form.errors.certificateKey && form.touched.certificateKey
138+
? form.errors.certificateKey
139+
: null}
140+
</div>
141+
) : null}
142+
</div>
143+
)}
144+
</Field>
145+
<Field name="certificate">
146+
{({ field, form }: any) => (
147+
<div className="mb-3">
148+
<label htmlFor="certificate" className="form-label">
149+
<T id="certificate.custom-certificate" />
150+
</label>
151+
<input
152+
id="certificate"
153+
type="file"
154+
required
155+
autoComplete="off"
156+
className="form-control"
157+
onChange={(event) => {
158+
form.setFieldValue(
159+
field.name,
160+
event.currentTarget.files?.length
161+
? event.currentTarget.files[0]
162+
: null,
163+
);
164+
}}
165+
/>
166+
{form.errors.certificate ? (
167+
<div className="invalid-feedback">
168+
{form.errors.certificate && form.touched.certificate
169+
? form.errors.certificate
170+
: null}
171+
</div>
172+
) : null}
173+
</div>
174+
)}
175+
</Field>
176+
<Field name="intermediateCertificate">
177+
{({ field, form }: any) => (
178+
<div className="mb-3">
179+
<label htmlFor="intermediateCertificate" className="form-label">
180+
<T id="certificate.custom-intermediate" />
181+
</label>
182+
<input
183+
id="intermediateCertificate"
184+
type="file"
185+
autoComplete="off"
186+
className="form-control"
187+
onChange={(event) => {
188+
form.setFieldValue(
189+
field.name,
190+
event.currentTarget.files?.length
191+
? event.currentTarget.files[0]
192+
: null,
193+
);
194+
}}
195+
/>
196+
{form.errors.intermediateCertificate ? (
197+
<div className="invalid-feedback">
198+
{form.errors.intermediateCertificate &&
199+
form.touched.intermediateCertificate
200+
? form.errors.intermediateCertificate
201+
: null}
202+
</div>
203+
) : null}
204+
</div>
205+
)}
206+
</Field>
63207
</div>
64208
</div>
65209
</Modal.Body>
@@ -70,7 +214,7 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal
70214
<Button
71215
type="submit"
72216
actionType="primary"
73-
className="ms-auto bg-lime"
217+
className="ms-auto bg-pink"
74218
data-bs-dismiss="modal"
75219
isLoading={isSubmitting}
76220
disabled={isSubmitting}

frontend/src/pages/Certificates/Table.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
4040
header: intl.formatMessage({ id: "column.name" }),
4141
cell: (info: any) => {
4242
const value = info.getValue();
43-
return <DomainsFormatter domains={value.domainNames} createdOn={value.createdOn} />;
43+
return (
44+
<DomainsFormatter
45+
domains={value.domainNames}
46+
createdOn={value.createdOn}
47+
niceName={value.niceName}
48+
/>
49+
);
4450
},
4551
}),
4652
columnHelper.accessor((row: any) => row.provider, {
@@ -50,6 +56,9 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
5056
if (info.getValue() === "letsencrypt") {
5157
return <T id="lets-encrypt" />;
5258
}
59+
if (info.getValue() === "other") {
60+
return <T id="certificates.custom" />;
61+
}
5362
return <T id={info.getValue()} />;
5463
},
5564
}),

0 commit comments

Comments
 (0)