Skip to content

Commit 6c9531e

Browse files
committed
Adds feature to Edit Custom certificates.
1 parent 79d28f0 commit 6c9531e

File tree

10 files changed

+158
-17
lines changed

10 files changed

+158
-17
lines changed

backend/internal/certificate.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ const internalCertificate = {
272272
.then(utils.omitRow(omissions()))
273273
.then((saved_row) => {
274274
saved_row.meta = internalCertificate.cleanMeta(saved_row.meta);
275-
data.meta = internalCertificate.cleanMeta(data.meta);
275+
if (data.meta) {
276+
data.meta = internalCertificate.cleanMeta(data.meta);
277+
}
276278

277279
// Add row.nice_name for custom certs
278280
if (saved_row.provider === 'other') {

backend/routes/nginx/certificates.js

+18
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,24 @@ router
147147
.catch(next);
148148
})
149149

150+
/**
151+
* PUT /api/nginx/certificates/123
152+
*
153+
* Updates a specific certificate
154+
*/
155+
.put((req, res, next) => {
156+
const data = { id: req.params.certificate_id, ...req.body };
157+
apiValidator(schema.getValidationSchema('/nginx/certificates/{certID}', 'put'), data)
158+
.then((data) => {
159+
return internalCertificate.update(res.locals.access, data);
160+
})
161+
.then((result) => {
162+
res.status(201)
163+
.send(result);
164+
})
165+
.catch(next);
166+
})
167+
150168
/**
151169
* DELETE /api/nginx/certificates/123
152170
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"operationId": "updateCertificate",
3+
"summary": "Update a Certificate",
4+
"tags": ["Certificates"],
5+
"security": [
6+
{
7+
"BearerAuth": ["certificates"]
8+
}
9+
],
10+
"parameters": [
11+
{
12+
"in": "path",
13+
"name": "certID",
14+
"schema": {
15+
"type": "integer",
16+
"minimum": 1
17+
},
18+
"required": true,
19+
"example": 1
20+
}
21+
],
22+
"requestBody": {
23+
"description": "Certificate Payload",
24+
"required": true,
25+
"content": {
26+
"application/json": {
27+
"schema": {
28+
"type": "object",
29+
"additionalProperties": false,
30+
"required": ["provider"],
31+
"properties": {
32+
"id": {
33+
"$ref": "../../../../common.json#/properties/id"
34+
},
35+
"provider": {
36+
"$ref": "../../../../components/certificate-object.json#/properties/provider"
37+
},
38+
"nice_name": {
39+
"$ref": "../../../../components/certificate-object.json#/properties/nice_name"
40+
}
41+
}
42+
}
43+
}
44+
}
45+
},
46+
"responses": {
47+
"200": {
48+
"description": "200 response",
49+
"content": {
50+
"application/json": {
51+
"examples": {
52+
"default": {
53+
"value": {
54+
"id": 4,
55+
"created_on": "2024-10-09T05:31:58.000Z",
56+
"modified_on": "2024-10-09T05:32:11.000Z",
57+
"owner_user_id": 1,
58+
"provider": "letsencrypt",
59+
"nice_name": "test.example.com",
60+
"domain_names": ["test.example.com"],
61+
"expires_on": "2025-01-07T04:34:18.000Z",
62+
"meta": {
63+
"letsencrypt_email": "[email protected]",
64+
"letsencrypt_agree": true,
65+
"dns_challenge": false
66+
}
67+
}
68+
}
69+
},
70+
"schema": {
71+
"$ref": "../../../../components/certificate-object.json"
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}

backend/schema/swagger.json

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@
7070
"get": {
7171
"$ref": "./paths/nginx/certificates/certID/get.json"
7272
},
73+
"put": {
74+
"$ref": "./paths/nginx/certificates/certID/put.json"
75+
},
7376
"delete": {
7477
"$ref": "./paths/nginx/certificates/certID/delete.json"
7578
}

frontend/js/app/nginx/certificates/form.ejs

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="modal-content">
22
<div class="modal-header">
3-
<h5 class="modal-title"><%- i18n('certificates', 'form-title', {provider: provider}) %></h5>
3+
<h5 class="modal-title"><%- i18n('certificates', 'form-title' , {provider: provider, id: id}) %></h5>
44
<button type="button" class="close cancel non-loader-content" aria-label="Close" data-dismiss="modal">&nbsp;</button>
55
</div>
66
<div class="modal-body">
@@ -148,18 +148,26 @@
148148
</div>
149149
<div class="col-sm-12 col-md-12 other-ssl">
150150
<div class="form-group">
151-
<div class="form-label"><%- i18n('certificates', 'other-certificate-key') %><span class="form-required">*</span></div>
151+
<div class="form-label"><%- i18n('certificates', 'other-certificate-key') %>
152+
<% if (isNew()) { %>
153+
<span class="form-required">*</span>
154+
<% } %>
155+
</div>
152156
<div class="custom-file">
153-
<input type="file" class="custom-file-input" name="meta[other_certificate_key]" id="other_certificate_key" required>
157+
<input type="file" class="custom-file-input" name="meta[other_certificate_key]" id="other_certificate_key" <%- isNew() ? 'required' : '' %>>
154158
<label id="other_certificate_key_label" class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
155159
</div>
156160
</div>
157161
</div>
158162
<div class="col-sm-12 col-md-12 other-ssl">
159163
<div class="form-group">
160-
<div class="form-label"><%- i18n('certificates', 'other-certificate') %><span class="form-required">*</span></div>
164+
<div class="form-label"><%- i18n('certificates', 'other-certificate') %>
165+
<% if (isNew()) { %>
166+
<span class="form-required">*</span>
167+
<% } %>
168+
</div>
161169
<div class="custom-file">
162-
<input type="file" class="custom-file-input" name="meta[other_certificate]" id="other_certificate">
170+
<input type="file" class="custom-file-input" name="meta[other_certificate]" id="other_certificate" <%- isNew() ? 'required' : '' %>>
163171
<label id="other_certificate_label" class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
164172
</div>
165173
</div>

frontend/js/app/nginx/certificates/form.js

+25-10
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ module.exports = Mn.View.extend({
9090
}
9191

9292
let data = this.ui.form.serializeJSON();
93+
data.id = this.model.get('id');
9394
data.provider = this.model.get('provider');
9495
let ssl_files = [];
9596

@@ -125,30 +126,38 @@ module.exports = Mn.View.extend({
125126
if (typeof data.domain_names === 'string' && data.domain_names) {
126127
data.domain_names = data.domain_names.split(',');
127128
}
128-
} else if (data.provider === 'other' && !this.model.hasSslFiles()) {
129+
} else if (data.provider === 'other') {
130+
const isNew = data.id == null;
129131
// check files are attached
130-
if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) {
132+
// Check Certificate
133+
const hasCertificateFile = this.ui.other_certificate[0].files.length && this.ui.other_certificate[0].files[0].size
134+
if (isNew && !hasCertificateFile) {
131135
alert('Certificate file is not attached');
132136
return;
133-
} else {
137+
}
138+
if (hasCertificateFile) {
134139
if (this.ui.other_certificate[0].files[0].size > this.max_file_size) {
135140
alert('Certificate file is too large (> 100kb)');
136141
return;
137142
}
138-
ssl_files.push({name: 'certificate', file: this.ui.other_certificate[0].files[0]});
143+
ssl_files.push({ name: 'certificate', file: this.ui.other_certificate[0].files[0] });
139144
}
140145

141-
if (!this.ui.other_certificate_key[0].files.length || !this.ui.other_certificate_key[0].files[0].size) {
146+
// Check Certificate Key
147+
const hasCertificateKeyFile = this.ui.other_certificate_key[0].files.length && this.ui.other_certificate_key[0].files[0].size
148+
if (isNew && !hasCertificateKeyFile) {
142149
alert('Certificate key file is not attached');
143150
return;
144-
} else {
151+
}
152+
if (hasCertificateKeyFile) {
145153
if (this.ui.other_certificate_key[0].files[0].size > this.max_file_size) {
146154
alert('Certificate key file is too large (> 100kb)');
147155
return;
148156
}
149-
ssl_files.push({name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0]});
157+
ssl_files.push({ name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0] });
150158
}
151159

160+
// Check Intermediate Certificate
152161
if (this.ui.other_intermediate_certificate[0].files.length && this.ui.other_intermediate_certificate[0].files[0].size) {
153162
if (this.ui.other_intermediate_certificate[0].files[0].size > this.max_file_size) {
154163
alert('Intermediate Certificate file is too large (> 100kb)');
@@ -170,20 +179,23 @@ module.exports = Mn.View.extend({
170179
}
171180

172181
new Promise(resolve => {
173-
if (data.provider === 'other') {
182+
if (data.provider === 'other' && ssl_files.length) {
174183
resolve(App.Api.Nginx.Certificates.validate(form_data));
175184
} else {
176185
resolve();
177186
}
178187
})
179188
.then(() => {
180-
return App.Api.Nginx.Certificates.create(data);
189+
return data.id == null
190+
? App.Api.Nginx.Certificates.create(data)
191+
: App.Api.Nginx.Certificates.update(data);
181192
})
182193
.then(result => {
183194
this.model.set(result);
184195

185196
// Now upload the certs if we need to
186-
if (data.provider === 'other') {
197+
const hasCertificateFiles = form_data.has('certificate') || form_data.has('certificate_key') || form_data.has('intermediate_certificate');
198+
if (data.provider === 'other' && hasCertificateFiles) {
187199
return App.Api.Nginx.Certificates.upload(this.model.get('id'), form_data)
188200
.then(result => {
189201
this.model.set('meta', _.assign({}, this.model.get('meta'), result));
@@ -240,6 +252,9 @@ module.exports = Mn.View.extend({
240252
this.getUI(ui).text(e.target.files[0].name)
241253
},
242254
templateContext: {
255+
isNew: function () {
256+
return this.isNew();
257+
},
243258
getLetsencryptEmail: function () {
244259
return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email');
245260
},

frontend/js/app/nginx/certificates/list/item.ejs

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
<a href="#" class="test dropdown-item"><i class="dropdown-icon fe fe-globe"></i> <%- i18n('certificates', 'test-reachability') %></a>
5454
<% } %>
5555
<div class="dropdown-divider"></div>
56+
<% } else { %>
57+
<a href="#" class="replace dropdown-item"><i class="dropdown-icon fe fe-edit"></i>
58+
<%- i18n('certificates', 'edit') %></a>
59+
<div class="dropdown-divider"></div>
5660
<% } %>
5761
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
5862
<% if (active_domain_names().length > 0) { %>

frontend/js/app/nginx/certificates/list/item.js

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = Mn.View.extend({
1111
ui: {
1212
host_link: '.host-link',
1313
renew: 'a.renew',
14+
replace: 'a.replace',
1415
delete: 'a.delete',
1516
download: 'a.download',
1617
test: 'a.test'
@@ -22,6 +23,11 @@ module.exports = Mn.View.extend({
2223
App.Controller.showNginxCertificateRenew(this.model);
2324
},
2425

26+
'click @ui.replace': function (e) {
27+
e.preventDefault();
28+
App.Controller.showNginxCertificateForm(this.model);
29+
},
30+
2531
'click @ui.delete': function (e) {
2632
e.preventDefault();
2733
App.Controller.showNginxCertificateDeleteConfirm(this.model);

frontend/js/i18n/messages.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
"title": "SSL Certificates",
188188
"empty": "There are no SSL Certificates",
189189
"add": "Add SSL Certificate",
190-
"form-title": "Add {provider, select, letsencrypt{Let's Encrypt} other{Custom}} Certificate",
190+
"form-title": "{id, select, undefined{Add} other{Edit}} {provider, select, letsencrypt{Let's Encrypt} other{Custom}} Certificate",
191191
"delete": "Delete SSL Certificate",
192192
"delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.",
193193
"help-title": "SSL Certificates",
@@ -196,6 +196,7 @@
196196
"other-certificate-key": "Certificate Key",
197197
"other-intermediate-certificate": "Intermediate Certificate",
198198
"force-renew": "Renew Now",
199+
"edit": "Edit",
199200
"test-reachability": "Test Server Reachability",
200201
"reachability-title": "Test Server Reachability",
201202
"reachability-info": "Test whether the domains are reachable from the public internet using Site24x7. This is not necessary when using the DNS Challenge.",

frontend/js/models/certificate.js

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ const model = Backbone.Model.extend({
2121
};
2222
},
2323

24+
/**
25+
* @returns {Boolean}
26+
*/
27+
isNew: function () {
28+
return this.get('id') == null;
29+
},
30+
2431
/**
2532
* @returns {Boolean}
2633
*/

0 commit comments

Comments
 (0)