Преглед изворни кода

Certificates UI for all hosts, Access Lists placeholder, audit log tweaks

Jamie Curnow пре 7 година
родитељ
комит
177bb2e888

+ 1 - 0
src/backend/internal/audit-log.js

@@ -19,6 +19,7 @@ const internalAuditLog = {
                 let query = auditLogModel
                     .query()
                     .orderBy('created_on', 'DESC')
+                    .orderBy('id', 'DESC')
                     .limit(100)
                     .allowEager('[user]');
 

+ 83 - 57
src/backend/internal/dead-host.js

@@ -1,11 +1,12 @@
 'use strict';
 
-const _                = require('lodash');
-const error            = require('../lib/error');
-const deadHostModel    = require('../models/dead_host');
-const internalHost     = require('./host');
-const internalNginx    = require('./nginx');
-const internalAuditLog = require('./audit-log');
+const _                   = require('lodash');
+const error               = require('../lib/error');
+const deadHostModel       = require('../models/dead_host');
+const internalHost        = require('./host');
+const internalNginx       = require('./nginx');
+const internalAuditLog    = require('./audit-log');
+const internalCertificate = require('./certificate');
 
 function omissions () {
     return ['is_deleted'];
@@ -19,6 +20,12 @@ const internalDeadHost = {
      * @returns {Promise}
      */
     create: (access, data) => {
+        let create_certificate = data.certificate_id === 'new';
+
+        if (create_certificate) {
+            delete data.certificate_id;
+        }
+
         return access.can('dead_hosts:create', data)
             .then(access_data => {
                 // Get a list of the domain names and check each of them against existing records
@@ -46,14 +53,40 @@ const internalDeadHost = {
                     .omit(omissions())
                     .insertAndFetch(data);
             })
+            .then(row => {
+                if (create_certificate) {
+                    return internalCertificate.createQuickCertificate(access, data)
+                        .then(cert => {
+                            // update host with cert id
+                            return internalDeadHost.update(access, {
+                                id:             row.id,
+                                certificate_id: cert.id
+                            });
+                        })
+                        .then(() => {
+                            return row;
+                        });
+                } else {
+                    return row;
+                }
+            })
+            .then(row => {
+                // re-fetch with cert
+                return internalDeadHost.get(access, {
+                    id:     row.id,
+                    expand: ['certificate', 'owner']
+                });
+            })
             .then(row => {
                 // Configure nginx
                 return internalNginx.configure(deadHostModel, 'dead_host', row)
                     .then(() => {
-                        return internalDeadHost.get(access, {id: row.id, expand: ['owner']});
+                        return row;
                     });
             })
             .then(row => {
+                data.meta = _.assign({}, data.meta || {}, row.meta);
+
                 // Add to audit log
                 return internalAuditLog.add(access, {
                     action:      'created',
@@ -71,11 +104,15 @@ const internalDeadHost = {
      * @param  {Access}  access
      * @param  {Object}  data
      * @param  {Integer} data.id
-     * @param  {String}  [data.email]
-     * @param  {String}  [data.name]
      * @return {Promise}
      */
     update: (access, data) => {
+        let create_certificate = data.certificate_id === 'new';
+
+        if (create_certificate) {
+            delete data.certificate_id;
+        }
+
         return access.can('dead_hosts:update', data.id)
             .then(access_data => {
                 // Get a list of the domain names and check each of them against existing records
@@ -105,13 +142,33 @@ const internalDeadHost = {
                     throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
                 }
 
+                if (create_certificate) {
+                    return internalCertificate.createQuickCertificate(access, {
+                        domain_names: data.domain_names || row.domain_names,
+                        meta:         _.assign({}, row.meta, data.meta)
+                    })
+                        .then(cert => {
+                            // update host with cert id
+                            data.certificate_id = cert.id;
+                        })
+                        .then(() => {
+                            return row;
+                        });
+                } else {
+                    return row;
+                }
+            })
+            .then(row => {
+                // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
+                data = _.assign({}, {
+                    domain_names: row.domain_names
+                },data);
+
                 return deadHostModel
                     .query()
-                    .omit(omissions())
-                    .patchAndFetchById(row.id, data)
+                    .where({id: data.id})
+                    .patch(data)
                     .then(saved_row => {
-                        saved_row.meta = internalHost.cleanMeta(saved_row.meta);
-
                         // Add to audit log
                         return internalAuditLog.add(access, {
                             action:      'updated',
@@ -123,6 +180,19 @@ const internalDeadHost = {
                                 return _.omit(saved_row, omissions());
                             });
                     });
+            })
+            .then(() => {
+                return internalDeadHost.get(access, {
+                    id:     data.id,
+                    expand: ['owner', 'certificate']
+                })
+                    .then(row => {
+                        // Configure nginx
+                        return internalNginx.configure(deadHostModel, 'dead_host', row)
+                            .then(() => {
+                                return _.omit(row, omissions());
+                            });
+                    });
             });
     },
 
@@ -165,7 +235,6 @@ const internalDeadHost = {
             })
             .then(row => {
                 if (row) {
-                    row.meta = internalHost.cleanMeta(row.meta);
                     return _.omit(row, omissions());
                 } else {
                     throw new error.ItemNotFoundError(data.id);
@@ -205,8 +274,6 @@ const internalDeadHost = {
                     })
                     .then(() => {
                         // Add to audit log
-                        row.meta = internalHost.cleanMeta(row.meta);
-
                         return internalAuditLog.add(access, {
                             action:      'deleted',
                             object_type: 'dead-host',
@@ -220,40 +287,6 @@ const internalDeadHost = {
             });
     },
 
-    /**
-     * @param   {Access}  access
-     * @param   {Object}  data
-     * @param   {Integer} data.id
-     * @param   {Object}  data.files
-     * @returns {Promise}
-     */
-    setCerts: (access, data) => {
-        return internalDeadHost.get(access, {id: data.id})
-            .then(row => {
-                _.map(data.files, (file, name) => {
-                    if (internalHost.allowed_ssl_files.indexOf(name) !== -1) {
-                        row.meta[name] = file.data.toString();
-                    }
-                });
-
-                return internalDeadHost.update(access, {
-                    id:   data.id,
-                    meta: row.meta
-                });
-            })
-            .then(row => {
-                return internalAuditLog.add(access, {
-                    action:      'updated',
-                    object_type: 'dead-host',
-                    object_id:   row.id,
-                    meta:        data
-                })
-                    .then(() => {
-                        return _.pick(row.meta, internalHost.allowed_ssl_files);
-                    });
-            });
-    },
-
     /**
      * All Hosts
      *
@@ -289,13 +322,6 @@ const internalDeadHost = {
                 }
 
                 return query;
-            })
-            .then(rows => {
-                rows.map(row => {
-                    row.meta = internalHost.cleanMeta(row.meta);
-                });
-
-                return rows;
             });
     },
 

+ 6 - 3
src/backend/internal/proxy-host.js

@@ -105,8 +105,6 @@ const internalProxyHost = {
      * @param  {Access}  access
      * @param  {Object}  data
      * @param  {Integer} data.id
-     * @param  {String}  [data.email]
-     * @param  {String}  [data.name]
      * @return {Promise}
      */
     update: (access, data) => {
@@ -162,6 +160,11 @@ const internalProxyHost = {
                 }
             })
             .then(row => {
+                // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
+                data = _.assign({}, {
+                    domain_names: row.domain_names
+                },data);
+
                 return proxyHostModel
                     .query()
                     .where({id: data.id})
@@ -190,7 +193,7 @@ const internalProxyHost = {
                             .then(() => {
                                 return _.omit(row, omissions());
                             });
-                    })
+                    });
             });
     },
 

+ 77 - 51
src/backend/internal/redirection-host.js

@@ -6,6 +6,7 @@ const redirectionHostModel = require('../models/redirection_host');
 const internalHost         = require('./host');
 const internalNginx        = require('./nginx');
 const internalAuditLog     = require('./audit-log');
+const internalCertificate  = require('./certificate');
 
 function omissions () {
     return ['is_deleted'];
@@ -19,6 +20,12 @@ const internalRedirectionHost = {
      * @returns {Promise}
      */
     create: (access, data) => {
+        let create_certificate = data.certificate_id === 'new';
+
+        if (create_certificate) {
+            delete data.certificate_id;
+        }
+
         return access.can('redirection_hosts:create', data)
             .then(access_data => {
                 // Get a list of the domain names and check each of them against existing records
@@ -46,14 +53,40 @@ const internalRedirectionHost = {
                     .omit(omissions())
                     .insertAndFetch(data);
             })
+            .then(row => {
+                if (create_certificate) {
+                    return internalCertificate.createQuickCertificate(access, data)
+                        .then(cert => {
+                            // update host with cert id
+                            return internalRedirectionHost.update(access, {
+                                id:             row.id,
+                                certificate_id: cert.id
+                            });
+                        })
+                        .then(() => {
+                            return row;
+                        });
+                } else {
+                    return row;
+                }
+            })
+            .then(row => {
+                // re-fetch with cert
+                return internalRedirectionHost.get(access, {
+                    id:     row.id,
+                    expand: ['certificate', 'owner']
+                });
+            })
             .then(row => {
                 // Configure nginx
                 return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
                     .then(() => {
-                        return internalRedirectionHost.get(access, {id: row.id, expand: ['owner']});
+                        return row;
                     });
             })
             .then(row => {
+                data.meta = _.assign({}, data.meta || {}, row.meta);
+
                 // Add to audit log
                 return internalAuditLog.add(access, {
                     action:      'created',
@@ -71,11 +104,15 @@ const internalRedirectionHost = {
      * @param  {Access}  access
      * @param  {Object}  data
      * @param  {Integer} data.id
-     * @param  {String}  [data.email]
-     * @param  {String}  [data.name]
      * @return {Promise}
      */
     update: (access, data) => {
+        let create_certificate = data.certificate_id === 'new';
+
+        if (create_certificate) {
+            delete data.certificate_id;
+        }
+
         return access.can('redirection_hosts:update', data.id)
             .then(access_data => {
                 // Get a list of the domain names and check each of them against existing records
@@ -105,13 +142,33 @@ const internalRedirectionHost = {
                     throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
                 }
 
+                if (create_certificate) {
+                    return internalCertificate.createQuickCertificate(access, {
+                        domain_names: data.domain_names || row.domain_names,
+                        meta:         _.assign({}, row.meta, data.meta)
+                    })
+                        .then(cert => {
+                            // update host with cert id
+                            data.certificate_id = cert.id;
+                        })
+                        .then(() => {
+                            return row;
+                        });
+                } else {
+                    return row;
+                }
+            })
+            .then(row => {
+                // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
+                data = _.assign({}, {
+                    domain_names: row.domain_names
+                },data);
+
                 return redirectionHostModel
                     .query()
-                    .omit(omissions())
-                    .patchAndFetchById(row.id, data)
+                    .where({id: data.id})
+                    .patch(data)
                     .then(saved_row => {
-                        saved_row.meta = internalHost.cleanMeta(saved_row.meta);
-
                         // Add to audit log
                         return internalAuditLog.add(access, {
                             action:      'updated',
@@ -123,6 +180,19 @@ const internalRedirectionHost = {
                                 return _.omit(saved_row, omissions());
                             });
                     });
+            })
+            .then(() => {
+                return internalRedirectionHost.get(access, {
+                    id:     data.id,
+                    expand: ['owner', 'certificate']
+                })
+                    .then(row => {
+                        // Configure nginx
+                        return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
+                            .then(() => {
+                                return _.omit(row, omissions());
+                            });
+                    });
             });
     },
 
@@ -165,7 +235,6 @@ const internalRedirectionHost = {
             })
             .then(row => {
                 if (row) {
-                    row.meta = internalHost.cleanMeta(row.meta);
                     return _.omit(row, omissions());
                 } else {
                     throw new error.ItemNotFoundError(data.id);
@@ -205,8 +274,6 @@ const internalRedirectionHost = {
                     })
                     .then(() => {
                         // Add to audit log
-                        row.meta = internalHost.cleanMeta(row.meta);
-
                         return internalAuditLog.add(access, {
                             action:      'deleted',
                             object_type: 'redirection-host',
@@ -220,40 +287,6 @@ const internalRedirectionHost = {
             });
     },
 
-    /**
-     * @param   {Access}  access
-     * @param   {Object}  data
-     * @param   {Integer} data.id
-     * @param   {Object}  data.files
-     * @returns {Promise}
-     */
-    setCerts: (access, data) => {
-        return internalRedirectionHost.get(access, {id: data.id})
-            .then(row => {
-                _.map(data.files, (file, name) => {
-                    if (internalHost.allowed_ssl_files.indexOf(name) !== -1) {
-                        row.meta[name] = file.data.toString();
-                    }
-                });
-
-                return internalRedirectionHost.update(access, {
-                    id:   data.id,
-                    meta: row.meta
-                });
-            })
-            .then(row => {
-                return internalAuditLog.add(access, {
-                    action:      'updated',
-                    object_type: 'redirection-host',
-                    object_id:   row.id,
-                    meta:        data
-                })
-                    .then(() => {
-                        return _.pick(row.meta, internalHost.allowed_ssl_files);
-                    });
-            });
-    },
-
     /**
      * All Hosts
      *
@@ -289,13 +322,6 @@ const internalRedirectionHost = {
                 }
 
                 return query;
-            })
-            .then(rows => {
-                rows.map(row => {
-                    row.meta = internalHost.cleanMeta(row.meta);
-                });
-
-                return rows;
             });
     },
 

+ 0 - 2
src/backend/internal/stream.js

@@ -57,8 +57,6 @@ const internalStream = {
      * @param  {Access}  access
      * @param  {Object}  data
      * @param  {Integer} data.id
-     * @param  {String}  [data.email]
-     * @param  {String}  [data.name]
      * @return {Promise}
      */
     update: (access, data) => {

+ 0 - 34
src/backend/routes/api/nginx/dead_hosts.js

@@ -147,38 +147,4 @@ router
             .catch(next);
     });
 
-/**
- * Specific dead-host Certificates
- *
- * /api/nginx/dead-hosts/123/certificates
- */
-router
-    .route('/:host_id/certificates')
-    .options((req, res) => {
-        res.sendStatus(204);
-    })
-    .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
-
-    /**
-     * POST /api/nginx/dead-hosts/123/certificates
-     *
-     * Upload certifications
-     */
-    .post((req, res, next) => {
-        if (!req.files) {
-            res.status(400)
-                .send({error: 'No files were uploaded'});
-        } else {
-            internalDeadHost.setCerts(res.locals.access, {
-                id:    parseInt(req.params.host_id, 10),
-                files: req.files
-            })
-                .then(result => {
-                    res.status(200)
-                        .send(result);
-                })
-                .catch(next);
-        }
-    });
-
 module.exports = router;

+ 0 - 34
src/backend/routes/api/nginx/redirection_hosts.js

@@ -147,38 +147,4 @@ router
             .catch(next);
     });
 
-/**
- * Specific redirection-host Certificates
- *
- * /api/nginx/redirection-hosts/123/certificates
- */
-router
-    .route('/:host_id/certificates')
-    .options((req, res) => {
-        res.sendStatus(204);
-    })
-    .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
-
-    /**
-     * POST /api/nginx/redirection-hosts/123/certificates
-     *
-     * Upload certifications
-     */
-    .post((req, res, next) => {
-        if (!req.files) {
-            res.status(400)
-                .send({error: 'No files were uploaded'});
-        } else {
-            internalRedirectionHost.setCerts(res.locals.access, {
-                id:    parseInt(req.params.host_id, 10),
-                files: req.files
-            })
-                .then(result => {
-                    res.status(200)
-                        .send(result);
-                })
-                .catch(next);
-        }
-    });
-
 module.exports = router;

+ 8 - 20
src/backend/schema/endpoints/dead-hosts.json

@@ -18,15 +18,12 @@
     "domain_names": {
       "$ref": "../definitions.json#/definitions/domain_names"
     },
-    "ssl_enabled": {
-      "$ref": "../definitions.json#/definitions/ssl_enabled"
+    "certificate_id": {
+      "$ref": "../definitions.json#/definitions/certificate_id"
     },
     "ssl_forced": {
       "$ref": "../definitions.json#/definitions/ssl_forced"
     },
-    "ssl_provider": {
-      "$ref": "../definitions.json#/definitions/ssl_provider"
-    },
     "meta": {
       "type": "object",
       "additionalProperties": false,
@@ -54,15 +51,12 @@
     "domain_names": {
       "$ref": "#/definitions/domain_names"
     },
-    "ssl_enabled": {
-      "$ref": "#/definitions/ssl_enabled"
+    "certificate_id": {
+      "$ref": "#/definitions/certificate_id"
     },
     "ssl_forced": {
       "$ref": "#/definitions/ssl_forced"
     },
-    "ssl_provider": {
-      "$ref": "#/definitions/ssl_provider"
-    },
     "meta": {
       "$ref": "#/definitions/meta"
     }
@@ -105,15 +99,12 @@
           "domain_names": {
             "$ref": "#/definitions/domain_names"
           },
-          "ssl_enabled": {
-            "$ref": "#/definitions/ssl_enabled"
+          "certificate_id": {
+            "$ref": "#/definitions/certificate_id"
           },
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
-          "ssl_provider": {
-            "$ref": "#/definitions/ssl_provider"
-          },
           "meta": {
             "$ref": "#/definitions/meta"
           }
@@ -142,15 +133,12 @@
           "domain_names": {
             "$ref": "#/definitions/domain_names"
           },
-          "ssl_enabled": {
-            "$ref": "#/definitions/ssl_enabled"
+          "certificate_id": {
+            "$ref": "#/definitions/certificate_id"
           },
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
-          "ssl_provider": {
-            "$ref": "#/definitions/ssl_provider"
-          },
           "meta": {
             "$ref": "#/definitions/meta"
           }

+ 8 - 20
src/backend/schema/endpoints/redirection-hosts.json

@@ -26,15 +26,12 @@
       "example": true,
       "type": "boolean"
     },
-    "ssl_enabled": {
-      "$ref": "../definitions.json#/definitions/ssl_enabled"
+    "certificate_id": {
+      "$ref": "../definitions.json#/definitions/certificate_id"
     },
     "ssl_forced": {
       "$ref": "../definitions.json#/definitions/ssl_forced"
     },
-    "ssl_provider": {
-      "$ref": "../definitions.json#/definitions/ssl_provider"
-    },
     "block_exploits": {
       "$ref": "../definitions.json#/definitions/block_exploits"
     },
@@ -71,15 +68,12 @@
     "preserve_path": {
       "$ref": "#/definitions/preserve_path"
     },
-    "ssl_enabled": {
-      "$ref": "#/definitions/ssl_enabled"
+    "certificate_id": {
+      "$ref": "#/definitions/certificate_id"
     },
     "ssl_forced": {
       "$ref": "#/definitions/ssl_forced"
     },
-    "ssl_provider": {
-      "$ref": "#/definitions/ssl_provider"
-    },
     "block_exploits": {
       "$ref": "#/definitions/block_exploits"
     },
@@ -132,15 +126,12 @@
           "preserve_path": {
             "$ref": "#/definitions/preserve_path"
           },
-          "ssl_enabled": {
-            "$ref": "#/definitions/ssl_enabled"
+          "certificate_id": {
+            "$ref": "#/definitions/certificate_id"
           },
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
-          "ssl_provider": {
-            "$ref": "#/definitions/ssl_provider"
-          },
           "block_exploits": {
             "$ref": "#/definitions/block_exploits"
           },
@@ -178,15 +169,12 @@
           "preserve_path": {
             "$ref": "#/definitions/preserve_path"
           },
-          "ssl_enabled": {
-            "$ref": "#/definitions/ssl_enabled"
+          "certificate_id": {
+            "$ref": "#/definitions/certificate_id"
           },
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
-          "ssl_provider": {
-            "$ref": "#/definitions/ssl_provider"
-          },
           "block_exploits": {
             "$ref": "#/definitions/block_exploits"
           },

+ 4 - 4
src/frontend/js/app/audit-log/meta.ejs

@@ -6,12 +6,12 @@
     <div class="modal-body">
         <div class="mb-2">
             <div class="tag tag-dark">
-                <%- i18n('audit-log', 'user') %>
-                <span class="tag-addon tag-teal"><%- user.name %></span>
+                <%- i18n('audit-log', action, {name: i18n('audit-log', object_type)}) %>
+                <span class="tag-addon tag-orange">#<%- object_id %></span>
             </div>
             <div class="tag tag-dark">
-                <%- i18n('audit-log', 'action') %>
-                <span class="tag-addon tag-warning"><%- i18n('audit-log', action, {name: i18n('audit-log', object_type)}) %></span>
+                <%- i18n('audit-log', 'user') %>
+                <span class="tag-addon tag-teal"><%- user.name %></span>
             </div>
             <div class="tag tag-dark">
                 <%- i18n('audit-log', 'date') %>

+ 7 - 106
src/frontend/js/app/nginx/access/form.ejs

@@ -1,118 +1,19 @@
 <div class="modal-content">
     <div class="modal-header">
-        <h5 class="modal-title"><%- i18n('proxy-hosts', 'form-title', {id: id}) %></h5>
+        <h5 class="modal-title"><%- i18n('access-lists', 'form-title', {id: id}) %></h5>
         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
     </div>
-    <div class="modal-body has-tabs">
+    <div class="modal-body">
         <form>
-            <ul class="nav nav-tabs" role="tablist">
-                <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
-                <li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
-            </ul>
-            <div class="tab-content">
-                <!-- Details -->
-                <div role="tabpanel" class="tab-pane active" id="details">
-                    <div class="row">
-
-                        <div class="col-sm-12 col-md-12">
-                            <div class="form-group">
-                                <label class="form-label"><%- i18n('all-hosts', 'domain-names') %> <span class="form-required">*</span></label>
-                                <input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>" required>
-                            </div>
-                        </div>
-                        <div class="col-sm-8 col-md-8">
-                            <div class="form-group">
-                                <label class="form-label"><%- i18n('proxy-hosts', 'forward-ip') %><span class="form-required">*</span></label>
-                                <input type="text" name="forward_ip" class="form-control text-monospace" placeholder="000.000.000.000" value="<%- forward_ip %>" autocomplete="off" maxlength="15" required>
-                            </div>
-                        </div>
-                        <div class="col-sm-4 col-md-4">
-                            <div class="form-group">
-                                <label class="form-label"><%- i18n('proxy-hosts', 'forward-port') %> <span class="form-required">*</span></label>
-                                <input name="forward_port" type="number" class="form-control text-monospace" placeholder="80" value="<%- forward_port %>" required>
-                            </div>
-                        </div>
+            <div class="row">
+                <div class="col-sm-12 col-md-12">
+                    <div class="form-group">
+                        <label class="form-label"><%- i18n('str', 'name') %> <span class="form-required">*</span></label>
+                        <input type="text" name="name" class="form-control" value="<%- name %>" required>
                     </div>
                 </div>
 
-                <!-- SSL -->
-                <div role="tabpanel" class="tab-pane" id="ssl-options">
-                    <div class="row">
-                        <div class="col-sm-6 col-md-6">
-                            <div class="form-group">
-                                <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="ssl_enabled" value="1"<%- ssl_enabled ? ' checked' : '' %>>
-                                    <span class="custom-switch-indicator"></span>
-                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'enable-ssl') %></span>
-                                </label>
-                            </div>
-                        </div>
-                        <div class="col-sm-6 col-md-6">
-                            <div class="form-group">
-                                <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="ssl_forced" value="1"<%- ssl_forced ? ' checked' : '' %><%- ssl_enabled ? '' : ' disabled' %>>
-                                    <span class="custom-switch-indicator"></span>
-                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'force-ssl') %></span>
-                                </label>
-                            </div>
-                        </div>
-                        <div class="col-sm-12 col-md-12">
-                            <div class="form-group">
-                                <label class="form-label"><%- i18n('all-hosts', 'cert-provider') %></label>
-                                <div class="selectgroup w-100">
-                                    <label class="selectgroup-item">
-                                        <input type="radio" name="ssl_provider" value="letsencrypt" class="selectgroup-input"<%- ssl_provider !== 'other' ? ' checked' : '' %>>
-                                        <span class="selectgroup-button"><%- i18n('ssl', 'letsencrypt') %></span>
-                                    </label>
-                                    <label class="selectgroup-item">
-                                        <input type="radio" name="ssl_provider" value="other" class="selectgroup-input"<%- ssl_provider === 'other' ? ' checked' : '' %>>
-                                        <span class="selectgroup-button"><%- i18n('ssl', 'other') %></span>
-                                    </label>
-                                </div>
-                            </div>
-                        </div>
-
-                        <!-- Lets encrypt -->
-                        <div class="col-sm-12 col-md-12 letsencrypt-ssl">
-                            <div class="form-group">
-                                <label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label>
-                                <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required>
-                            </div>
-                        </div>
-                        <div class="col-sm-12 col-md-12 letsencrypt-ssl">
-                            <div class="form-group">
-                                <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required<%- getLetsencryptAgree() ? ' checked' : '' %>>
-                                    <span class="custom-switch-indicator"></span>
-                                    <span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span>
-                                </label>
-                            </div>
-                        </div>
-
-                        <!-- Other -->
-                        <div class="col-sm-12 col-md-12 other-ssl">
-                            <div class="form-group">
-                                <div class="form-label"><%- i18n('all-hosts', 'other-certificate') %></div>
-                                <div class="custom-file">
-                                    <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]" id="other_ssl_certificate">
-                                    <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
-                                </div>
-                            </div>
-                        </div>
-                        <div class="col-sm-12 col-md-12 other-ssl">
-                            <div class="form-group">
-                                <div class="form-label"><%- i18n('all-hosts', 'other-certificate-key') %></div>
-                                <div class="custom-file">
-                                    <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]" id="other_ssl_certificate_key">
-                                    <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
             </div>
-
-
         </form>
     </div>
     <div class="modal-footer">

+ 14 - 138
src/frontend/js/app/nginx/access/form.js

@@ -1,10 +1,9 @@
 'use strict';
 
-const _              = require('underscore');
-const Mn             = require('backbone.marionette');
-const App            = require('../../main');
-const ProxyHostModel = require('../../../models/proxy-host');
-const template       = require('./form.ejs');
+const Mn              = require('backbone.marionette');
+const App             = require('../../main');
+const AccessListModel = require('../../../models/access-list');
+const template        = require('./form.ejs');
 
 require('jquery-serializejson');
 require('jquery-mask-plugin');
@@ -13,41 +12,15 @@ require('selectize');
 module.exports = Mn.View.extend({
     template:  template,
     className: 'modal-dialog',
-    max_file_size: 5120,
 
     ui: {
-        form:                      'form',
-        domain_names:              'input[name="domain_names"]',
-        forward_ip:                'input[name="forward_ip"]',
-        buttons:                   '.modal-footer button',
-        cancel:                    'button.cancel',
-        save:                      'button.save',
-        ssl_enabled:               'input[name="ssl_enabled"]',
-        ssl_options:               '#ssl-options input',
-        ssl_provider:              'input[name="ssl_provider"]',
-        other_ssl_certificate:     '#other_ssl_certificate',
-        other_ssl_certificate_key: '#other_ssl_certificate_key',
-
-        // SSL hiding and showing
-        all_ssl:         '.letsencrypt-ssl, .other-ssl',
-        letsencrypt_ssl: '.letsencrypt-ssl',
-        other_ssl:       '.other-ssl'
+        form:    'form',
+        buttons: '.modal-footer button',
+        cancel:  'button.cancel',
+        save:    'button.save'
     },
 
     events: {
-        'change @ui.ssl_enabled': function () {
-            let enabled = this.ui.ssl_enabled.prop('checked');
-            this.ui.ssl_options.not(this.ui.ssl_enabled).prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5);
-            this.ui.ssl_provider.trigger('change');
-        },
-
-        'change @ui.ssl_provider': function () {
-            let enabled  = this.ui.ssl_enabled.prop('checked');
-            let provider = this.ui.ssl_provider.filter(':checked').val();
-            this.ui.all_ssl.hide().find('input').prop('disabled', true);
-            this.ui[provider + '_ssl'].show().find('input').prop('disabled', !enabled);
-        },
-
         'click @ui.save': function (e) {
             e.preventDefault();
 
@@ -60,90 +33,26 @@ module.exports = Mn.View.extend({
             let data = this.ui.form.serializeJSON();
 
             // Manipulate
-            data.forward_port = parseInt(data.forward_port, 10);
-            _.map(data, function (item, idx) {
-                if (typeof item === 'string' && item === '1') {
-                    item = true;
-                } else if (typeof item === 'object' && item !== null) {
-                    _.map(item, function (item2, idx2) {
-                        if (typeof item2 === 'string' && item2 === '1') {
-                            item[idx2] = true;
-                        }
-                    });
-                }
-                data[idx] = item;
-            });
+            // ...
 
-            if (typeof data.domain_names === 'string' && data.domain_names) {
-                data.domain_names = data.domain_names.split(',');
-            }
-
-            let require_ssl_files = typeof data.ssl_enabled !== 'undefined' && data.ssl_enabled && typeof data.ssl_provider !== 'undefined' && data.ssl_provider === 'other';
-            let ssl_files         = [];
-            let method            = App.Api.Nginx.ProxyHosts.create;
-            let is_new            = true;
-
-            let must_require_ssl_files = require_ssl_files && !view.model.hasSslFiles('other');
+            let method = App.Api.Nginx.AccessLists.create;
+            let is_new = true;
 
             if (this.model.get('id')) {
                 // edit
                 is_new  = false;
-                method  = App.Api.Nginx.ProxyHosts.update;
+                method  = App.Api.Nginx.AccessLists.update;
                 data.id = this.model.get('id');
             }
 
-            // check files are attached
-            if (require_ssl_files) {
-                if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) {
-                    if (must_require_ssl_files) {
-                        alert('certificate file is not attached');
-                        return;
-                    }
-                } else {
-                    if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) {
-                        alert('certificate file is too large (> 5kb)');
-                        return;
-                    }
-                    ssl_files.push({name: 'other_certificate', file: this.ui.other_ssl_certificate[0].files[0]});
-                }
-
-                if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) {
-                    if (must_require_ssl_files) {
-                        alert('certificate key file is not attached');
-                        return;
-                    }
-                } else {
-                    if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) {
-                        alert('certificate key file is too large (> 5kb)');
-                        return;
-                    }
-                    ssl_files.push({name: 'other_certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]});
-                }
-            }
-
             this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
             method(data)
                 .then(result => {
                     view.model.set(result);
 
-                    // Now upload the certs if we need to
-                    if (ssl_files.length) {
-                        let form_data = new FormData();
-
-                        ssl_files.map(function (file) {
-                            form_data.append(file.name, file.file);
-                        });
-
-                        return App.Api.Nginx.ProxyHosts.setCerts(view.model.get('id'), form_data)
-                            .then(result => {
-                                view.model.set('meta', _.assign({}, view.model.get('meta'), result));
-                            });
-                    }
-                })
-                .then(() => {
                     App.UI.closeModal(function () {
                         if (is_new) {
-                            App.Controller.showNginxProxy();
+                            App.Controller.showNginxAccess();
                         }
                     });
                 })
@@ -154,42 +63,9 @@ module.exports = Mn.View.extend({
         }
     },
 
-    templateContext: {
-        getLetsencryptEmail: function () {
-            return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email');
-        },
-
-        getLetsencryptAgree: function () {
-            return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false;
-        }
-    },
-
-    onRender: function () {
-        this.ui.forward_ip.mask('099.099.099.099', {
-            clearIfNotMatch: true,
-            placeholder:     '000.000.000.000'
-        });
-
-        this.ui.ssl_enabled.trigger('change');
-        this.ui.ssl_provider.trigger('change');
-
-        this.ui.domain_names.selectize({
-            delimiter:    ',',
-            persist:      false,
-            maxOptions:   15,
-            create:       function (input) {
-                return {
-                    value: input,
-                    text:  input
-                };
-            },
-            createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/
-        });
-    },
-
     initialize: function (options) {
         if (typeof options.model === 'undefined' || !options.model) {
-            this.model = new ProxyHostModel.Model();
+            this.model = new AccessListModel.Model();
         }
     }
 });

+ 1 - 1
src/frontend/js/app/nginx/dead/delete.ejs

@@ -8,7 +8,7 @@
             <div class="row">
                 <div class="col-sm-12 col-md-12">
                     <%= i18n('dead-hosts', 'delete-confirm', {domains: domain_names.join(', ')}) %>
-                    <% if (ssl_enabled) { %>
+                    <% if (certificate_id) { %>
                         <br><br>
                         <%- i18n('ssl', 'delete-ssl') %>
                     <% } %>

+ 12 - 47
src/frontend/js/app/nginx/dead/form.ejs

@@ -26,76 +26,41 @@
                 <!-- SSL -->
                 <div role="tabpanel" class="tab-pane" id="ssl-options">
                     <div class="row">
-                        <div class="col-sm-6 col-md-6">
+                        <div class="col-sm-12 col-md-12">
                             <div class="form-group">
-                                <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="ssl_enabled" value="1"<%- ssl_enabled ? ' checked' : '' %>>
-                                    <span class="custom-switch-indicator"></span>
-                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'enable-ssl') %></span>
-                                </label>
+                                <label class="form-label">SSL Certificate</label>
+                                <select name="certificate_id" class="form-control custom-select" placeholder="None">
+                                    <option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>>None</option>
+                                    <option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}">Request a new SSL Certificate</option>
+                                </select>
                             </div>
                         </div>
-                        <div class="col-sm-6 col-md-6">
+                        <div class="col-sm-12 col-md-12">
                             <div class="form-group">
                                 <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="ssl_forced" value="1"<%- ssl_forced ? ' checked' : '' %><%- ssl_enabled ? '' : ' disabled' %>>
+                                    <input type="checkbox" class="custom-switch-input" name="ssl_forced" value="1"<%- ssl_forced ? ' checked' : '' %><%- certificate_id ? '' : ' disabled' %>>
                                     <span class="custom-switch-indicator"></span>
                                     <span class="custom-switch-description"><%- i18n('all-hosts', 'force-ssl') %></span>
                                 </label>
                             </div>
                         </div>
-                        <div class="col-sm-12 col-md-12">
-                            <div class="form-group">
-                                <label class="form-label"><%- i18n('all-hosts', 'cert-provider') %></label>
-                                <div class="selectgroup w-100">
-                                    <label class="selectgroup-item">
-                                        <input type="radio" name="ssl_provider" value="letsencrypt" class="selectgroup-input"<%- ssl_provider !== 'other' ? ' checked' : '' %>>
-                                        <span class="selectgroup-button"><%- i18n('ssl', 'letsencrypt') %></span>
-                                    </label>
-                                    <label class="selectgroup-item">
-                                        <input type="radio" name="ssl_provider" value="other" class="selectgroup-input"<%- ssl_provider === 'other' ? ' checked' : '' %>>
-                                        <span class="selectgroup-button"><%- i18n('ssl', 'other') %></span>
-                                    </label>
-                                </div>
-                            </div>
-                        </div>
 
                         <!-- Lets encrypt -->
-                        <div class="col-sm-12 col-md-12 letsencrypt-ssl">
+                        <div class="col-sm-12 col-md-12 letsencrypt">
                             <div class="form-group">
                                 <label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label>
-                                <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required>
+                                <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required disabled>
                             </div>
                         </div>
-                        <div class="col-sm-12 col-md-12 letsencrypt-ssl">
+                        <div class="col-sm-12 col-md-12 letsencrypt">
                             <div class="form-group">
                                 <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required<%- getLetsencryptAgree() ? ' checked' : '' %>>
+                                    <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required disabled>
                                     <span class="custom-switch-indicator"></span>
                                     <span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span>
                                 </label>
                             </div>
                         </div>
-
-                        <!-- Other -->
-                        <div class="col-sm-12 col-md-12 other-ssl">
-                            <div class="form-group">
-                                <div class="form-label"><%- i18n('all-hosts', 'other-certificate') %></div>
-                                <div class="custom-file">
-                                    <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]" id="other_ssl_certificate">
-                                    <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
-                                </div>
-                            </div>
-                        </div>
-                        <div class="col-sm-12 col-md-12 other-ssl">
-                            <div class="form-group">
-                                <div class="form-label"><%- i18n('all-hosts', 'other-certificate-key') %></div>
-                                <div class="custom-file">
-                                    <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]" id="other_ssl_certificate_key">
-                                    <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
-                                </div>
-                            </div>
-                        </div>
                     </div>
                 </div>
             </div>

+ 81 - 94
src/frontend/js/app/nginx/dead/form.js

@@ -1,49 +1,41 @@
 'use strict';
 
-const _             = require('underscore');
-const Mn            = require('backbone.marionette');
-const App           = require('../../main');
-const DeadHostModel = require('../../../models/dead-host');
-const template      = require('./form.ejs');
+const Mn                   = require('backbone.marionette');
+const App                  = require('../../main');
+const DeadHostModel        = require('../../../models/dead-host');
+const template             = require('./form.ejs');
+const certListItemTemplate = require('../certificates-list-item.ejs');
+const Helpers              = require('../../../lib/helpers');
 
 require('jquery-serializejson');
 require('selectize');
 
 module.exports = Mn.View.extend({
-    template:      template,
-    className:     'modal-dialog',
-    max_file_size: 5120,
+    template:  template,
+    className: 'modal-dialog',
 
     ui: {
-        form:                      'form',
-        domain_names:              'input[name="domain_names"]',
-        buttons:                   '.modal-footer button',
-        cancel:                    'button.cancel',
-        save:                      'button.save',
-        ssl_enabled:               'input[name="ssl_enabled"]',
-        ssl_options:               '#ssl-options input',
-        ssl_provider:              'input[name="ssl_provider"]',
-        other_ssl_certificate:     '#other_ssl_certificate',
-        other_ssl_certificate_key: '#other_ssl_certificate_key',
-
-        // SSL hiding and showing
-        all_ssl:         '.letsencrypt-ssl, .other-ssl',
-        letsencrypt_ssl: '.letsencrypt-ssl',
-        other_ssl:       '.other-ssl'
+        form:               'form',
+        domain_names:       'input[name="domain_names"]',
+        buttons:            '.modal-footer button',
+        cancel:             'button.cancel',
+        save:               'button.save',
+        certificate_select: 'select[name="certificate_id"]',
+        ssl_forced:         'input[name="ssl_forced"]',
+        letsencrypt:        '.letsencrypt'
     },
 
     events: {
-        'change @ui.ssl_enabled': function () {
-            let enabled = this.ui.ssl_enabled.prop('checked');
-            this.ui.ssl_options.not(this.ui.ssl_enabled).prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5);
-            this.ui.ssl_provider.trigger('change');
-        },
+        'change @ui.certificate_select': function () {
+            let id = this.ui.certificate_select.val();
+            if (id === 'new') {
+                this.ui.letsencrypt.show().find('input').prop('disabled', false);
+            } else {
+                this.ui.letsencrypt.hide().find('input').prop('disabled', true);
+            }
 
-        'change @ui.ssl_provider': function () {
-            let enabled  = this.ui.ssl_enabled.prop('checked');
-            let provider = this.ui.ssl_provider.filter(':checked').val();
-            this.ui.all_ssl.hide().find('input').prop('disabled', true);
-            this.ui[provider + '_ssl'].show().find('input').prop('disabled', !enabled);
+            let enabled = id === 'new' || parseInt(id, 10) > 0;
+            this.ui.ssl_forced.prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5);
         },
 
         'click @ui.save': function (e) {
@@ -58,23 +50,35 @@ module.exports = Mn.View.extend({
             let data = this.ui.form.serializeJSON();
 
             // Manipulate
-            data.ssl_enabled = !!data.ssl_enabled;
-            data.ssl_forced  = !!data.ssl_forced;
-
-            if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') {
-                data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree;
+            if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
+                data.ssl_forced = true;
             }
 
             if (typeof data.domain_names === 'string' && data.domain_names) {
                 data.domain_names = data.domain_names.split(',');
             }
 
-            let require_ssl_files = typeof data.ssl_enabled !== 'undefined' && data.ssl_enabled && typeof data.ssl_provider !== 'undefined' && data.ssl_provider === 'other';
-            let ssl_files         = [];
-            let method            = App.Api.Nginx.DeadHosts.create;
-            let is_new            = true;
+            // Check for any domain names containing wildcards, which are not allowed with letsencrypt
+            if (data.certificate_id === 'new') {
+                let domain_err = false;
+                data.domain_names.map(function (name) {
+                    if (name.match(/\*/im)) {
+                        domain_err = true;
+                    }
+                });
+
+                if (domain_err) {
+                    alert('Cannot request Let\'s Encrypt Certificate for wildcard domains');
+                    return;
+                }
+
+                data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1';
+            } else {
+                data.certificate_id = parseInt(data.certificate_id, 0);
+            }
 
-            let must_require_ssl_files = require_ssl_files && !view.model.hasSslFiles('other');
+            let method = App.Api.Nginx.DeadHosts.create;
+            let is_new = true;
 
             if (this.model.get('id')) {
                 // edit
@@ -83,55 +87,11 @@ module.exports = Mn.View.extend({
                 data.id = this.model.get('id');
             }
 
-            // check files are attached
-            if (require_ssl_files) {
-                if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) {
-                    if (must_require_ssl_files) {
-                        alert('certificate file is not attached');
-                        return;
-                    }
-                } else {
-                    if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) {
-                        alert('certificate file is too large (> 5kb)');
-                        return;
-                    }
-                    ssl_files.push({name: 'other_certificate', file: this.ui.other_ssl_certificate[0].files[0]});
-                }
-
-                if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) {
-                    if (must_require_ssl_files) {
-                        alert('certificate key file is not attached');
-                        return;
-                    }
-                } else {
-                    if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) {
-                        alert('certificate key file is too large (> 5kb)');
-                        return;
-                    }
-                    ssl_files.push({name: 'other_certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]});
-                }
-            }
-
             this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
             method(data)
                 .then(result => {
                     view.model.set(result);
 
-                    // Now upload the certs if we need to
-                    if (ssl_files.length) {
-                        let form_data = new FormData();
-
-                        ssl_files.map(function (file) {
-                            form_data.append(file.name, file.file);
-                        });
-
-                        return App.Api.Nginx.DeadHosts.setCerts(view.model.get('id'), form_data)
-                            .then(result => {
-                                view.model.set('meta', _.assign({}, view.model.get('meta'), result));
-                            });
-                    }
-                })
-                .then(() => {
                     App.UI.closeModal(function () {
                         if (is_new) {
                             App.Controller.showNginxDead();
@@ -147,18 +107,14 @@ module.exports = Mn.View.extend({
 
     templateContext: {
         getLetsencryptEmail: function () {
-            return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email');
-        },
-
-        getLetsencryptAgree: function () {
-            return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false;
+            return App.Cache.User.get('email');
         }
     },
 
     onRender: function () {
-        this.ui.ssl_enabled.trigger('change');
-        this.ui.ssl_provider.trigger('change');
+        let view = this;
 
+        // Domain names
         this.ui.domain_names.selectize({
             delimiter:    ',',
             persist:      false,
@@ -171,6 +127,37 @@ module.exports = Mn.View.extend({
             },
             createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/
         });
+
+        // Certificates
+        this.ui.letsencrypt.hide();
+        this.ui.certificate_select.selectize({
+            valueField:       'id',
+            labelField:       'nice_name',
+            searchField:      ['nice_name', 'domain_names'],
+            create:           false,
+            preload:          true,
+            allowEmptyOption: true,
+            render:           {
+                option: function (item) {
+                    item.i18n         = App.i18n;
+                    item.formatDbDate = Helpers.formatDbDate;
+                    return certListItemTemplate(item);
+                }
+            },
+            load:             function (query, callback) {
+                App.Api.Nginx.Certificates.getAll()
+                    .then(rows => {
+                        callback(rows);
+                    })
+                    .catch(err => {
+                        console.error(err);
+                        callback();
+                    });
+            },
+            onLoad:           function () {
+                view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
+            }
+        });
     },
 
     initialize: function (options) {

+ 5 - 4
src/frontend/js/app/nginx/proxy/form.js

@@ -12,9 +12,8 @@ require('jquery-mask-plugin');
 require('selectize');
 
 module.exports = Mn.View.extend({
-    template:      template,
-    className:     'modal-dialog',
-    max_file_size: 5120,
+    template:  template,
+    className: 'modal-dialog',
 
     ui: {
         form:               'form',
@@ -68,7 +67,7 @@ module.exports = Mn.View.extend({
             // Check for any domain names containing wildcards, which are not allowed with letsencrypt
             if (data.certificate_id === 'new') {
                 let domain_err = false;
-                data.domain_names.map(function(name) {
+                data.domain_names.map(function (name) {
                     if (name.match(/\*/im)) {
                         domain_err = true;
                     }
@@ -78,6 +77,8 @@ module.exports = Mn.View.extend({
                     alert('Cannot request Let\'s Encrypt Certificate for wildcard domains');
                     return;
                 }
+
+                data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1';
             } else {
                 data.certificate_id = parseInt(data.certificate_id, 0);
             }

+ 1 - 1
src/frontend/js/app/nginx/redirection/delete.ejs

@@ -8,7 +8,7 @@
             <div class="row">
                 <div class="col-sm-12 col-md-12">
                     <%= i18n('redirection-hosts', 'delete-confirm', {domains: domain_names.join(', ')}) %>
-                    <% if (ssl_enabled) { %>
+                    <% if (certificate_id) { %>
                         <br><br>
                         <%- i18n('ssl', 'delete-ssl') %>
                     <% } %>

+ 12 - 47
src/frontend/js/app/nginx/redirection/form.ejs

@@ -50,76 +50,41 @@
                 <!-- SSL -->
                 <div role="tabpanel" class="tab-pane" id="ssl-options">
                     <div class="row">
-                        <div class="col-sm-6 col-md-6">
+                        <div class="col-sm-12 col-md-12">
                             <div class="form-group">
-                                <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="ssl_enabled" value="1"<%- ssl_enabled ? ' checked' : '' %>>
-                                    <span class="custom-switch-indicator"></span>
-                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'enable-ssl') %></span>
-                                </label>
+                                <label class="form-label">SSL Certificate</label>
+                                <select name="certificate_id" class="form-control custom-select" placeholder="None">
+                                    <option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>>None</option>
+                                    <option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}">Request a new SSL Certificate</option>
+                                </select>
                             </div>
                         </div>
-                        <div class="col-sm-6 col-md-6">
+                        <div class="col-sm-12 col-md-12">
                             <div class="form-group">
                                 <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="ssl_forced" value="1"<%- ssl_forced ? ' checked' : '' %><%- ssl_enabled ? '' : ' disabled' %>>
+                                    <input type="checkbox" class="custom-switch-input" name="ssl_forced" value="1"<%- ssl_forced ? ' checked' : '' %><%- certificate_id ? '' : ' disabled' %>>
                                     <span class="custom-switch-indicator"></span>
                                     <span class="custom-switch-description"><%- i18n('all-hosts', 'force-ssl') %></span>
                                 </label>
                             </div>
                         </div>
-                        <div class="col-sm-12 col-md-12">
-                            <div class="form-group">
-                                <label class="form-label"><%- i18n('all-hosts', 'cert-provider') %></label>
-                                <div class="selectgroup w-100">
-                                    <label class="selectgroup-item">
-                                        <input type="radio" name="ssl_provider" value="letsencrypt" class="selectgroup-input"<%- ssl_provider !== 'other' ? ' checked' : '' %>>
-                                        <span class="selectgroup-button"><%- i18n('ssl', 'letsencrypt') %></span>
-                                    </label>
-                                    <label class="selectgroup-item">
-                                        <input type="radio" name="ssl_provider" value="other" class="selectgroup-input"<%- ssl_provider === 'other' ? ' checked' : '' %>>
-                                        <span class="selectgroup-button"><%- i18n('ssl', 'other') %></span>
-                                    </label>
-                                </div>
-                            </div>
-                        </div>
 
                         <!-- Lets encrypt -->
-                        <div class="col-sm-12 col-md-12 letsencrypt-ssl">
+                        <div class="col-sm-12 col-md-12 letsencrypt">
                             <div class="form-group">
                                 <label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label>
-                                <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required>
+                                <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required disabled>
                             </div>
                         </div>
-                        <div class="col-sm-12 col-md-12 letsencrypt-ssl">
+                        <div class="col-sm-12 col-md-12 letsencrypt">
                             <div class="form-group">
                                 <label class="custom-switch">
-                                    <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required<%- getLetsencryptAgree() ? ' checked' : '' %>>
+                                    <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required disabled>
                                     <span class="custom-switch-indicator"></span>
                                     <span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span>
                                 </label>
                             </div>
                         </div>
-
-                        <!-- Other -->
-                        <div class="col-sm-12 col-md-12 other-ssl">
-                            <div class="form-group">
-                                <div class="form-label"><%- i18n('all-hosts', 'other-certificate') %></div>
-                                <div class="custom-file">
-                                    <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]" id="other_ssl_certificate">
-                                    <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
-                                </div>
-                            </div>
-                        </div>
-                        <div class="col-sm-12 col-md-12 other-ssl">
-                            <div class="form-group">
-                                <div class="form-label"><%- i18n('all-hosts', 'other-certificate-key') %></div>
-                                <div class="custom-file">
-                                    <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]" id="other_ssl_certificate_key">
-                                    <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
-                                </div>
-                            </div>
-                        </div>
                     </div>
                 </div>
             </div>

+ 77 - 89
src/frontend/js/app/nginx/redirection/form.js

@@ -1,49 +1,41 @@
 'use strict';
 
-const _                    = require('underscore');
 const Mn                   = require('backbone.marionette');
 const App                  = require('../../main');
 const RedirectionHostModel = require('../../../models/redirection-host');
 const template             = require('./form.ejs');
+const certListItemTemplate = require('../certificates-list-item.ejs');
+const Helpers              = require('../../../lib/helpers');
 
 require('jquery-serializejson');
 require('selectize');
 
 module.exports = Mn.View.extend({
-    template:      template,
-    className:     'modal-dialog',
-    max_file_size: 5120,
+    template:  template,
+    className: 'modal-dialog',
 
     ui: {
-        form:                      'form',
-        domain_names:              'input[name="domain_names"]',
-        buttons:                   '.modal-footer button',
-        cancel:                    'button.cancel',
-        save:                      'button.save',
-        ssl_enabled:               'input[name="ssl_enabled"]',
-        ssl_options:               '#ssl-options input',
-        ssl_provider:              'input[name="ssl_provider"]',
-        other_ssl_certificate:     '#other_ssl_certificate',
-        other_ssl_certificate_key: '#other_ssl_certificate_key',
-
-        // SSL hiding and showing
-        all_ssl:         '.letsencrypt-ssl, .other-ssl',
-        letsencrypt_ssl: '.letsencrypt-ssl',
-        other_ssl:       '.other-ssl'
+        form:               'form',
+        domain_names:       'input[name="domain_names"]',
+        buttons:            '.modal-footer button',
+        cancel:             'button.cancel',
+        save:               'button.save',
+        certificate_select: 'select[name="certificate_id"]',
+        ssl_forced:         'input[name="ssl_forced"]',
+        letsencrypt:        '.letsencrypt'
     },
 
     events: {
-        'change @ui.ssl_enabled': function () {
-            let enabled = this.ui.ssl_enabled.prop('checked');
-            this.ui.ssl_options.not(this.ui.ssl_enabled).prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5);
-            this.ui.ssl_provider.trigger('change');
-        },
+        'change @ui.certificate_select': function () {
+            let id = this.ui.certificate_select.val();
+            if (id === 'new') {
+                this.ui.letsencrypt.show().find('input').prop('disabled', false);
+            } else {
+                this.ui.letsencrypt.hide().find('input').prop('disabled', true);
+            }
 
-        'change @ui.ssl_provider': function () {
-            let enabled  = this.ui.ssl_enabled.prop('checked');
-            let provider = this.ui.ssl_provider.filter(':checked').val();
-            this.ui.all_ssl.hide().find('input').prop('disabled', true);
-            this.ui[provider + '_ssl'].show().find('input').prop('disabled', !enabled);
+            let enabled = id === 'new' || parseInt(id, 10) > 0;
+            this.ui.ssl_forced.prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5);
         },
 
         'click @ui.save': function (e) {
@@ -60,23 +52,36 @@ module.exports = Mn.View.extend({
             // Manipulate
             data.block_exploits = !!data.block_exploits;
             data.preserve_path  = !!data.preserve_path;
-            data.ssl_enabled    = !!data.ssl_enabled;
-            data.ssl_forced     = !!data.ssl_forced;
 
-            if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') {
-                data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree;
+            if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
+                data.ssl_forced = true;
             }
 
             if (typeof data.domain_names === 'string' && data.domain_names) {
                 data.domain_names = data.domain_names.split(',');
             }
 
-            let require_ssl_files = typeof data.ssl_enabled !== 'undefined' && data.ssl_enabled && typeof data.ssl_provider !== 'undefined' && data.ssl_provider === 'other';
-            let ssl_files         = [];
-            let method            = App.Api.Nginx.RedirectionHosts.create;
-            let is_new            = true;
+            // Check for any domain names containing wildcards, which are not allowed with letsencrypt
+            if (data.certificate_id === 'new') {
+                let domain_err = false;
+                data.domain_names.map(function (name) {
+                    if (name.match(/\*/im)) {
+                        domain_err = true;
+                    }
+                });
+
+                if (domain_err) {
+                    alert('Cannot request Let\'s Encrypt Certificate for wildcard domains');
+                    return;
+                }
+
+                data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1';
+            } else {
+                data.certificate_id = parseInt(data.certificate_id, 0);
+            }
 
-            let must_require_ssl_files = require_ssl_files && !view.model.hasSslFiles('other');
+            let method = App.Api.Nginx.RedirectionHosts.create;
+            let is_new = true;
 
             if (this.model.get('id')) {
                 // edit
@@ -85,55 +90,11 @@ module.exports = Mn.View.extend({
                 data.id = this.model.get('id');
             }
 
-            // check files are attached
-            if (require_ssl_files) {
-                if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) {
-                    if (must_require_ssl_files) {
-                        alert('certificate file is not attached');
-                        return;
-                    }
-                } else {
-                    if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) {
-                        alert('certificate file is too large (> 5kb)');
-                        return;
-                    }
-                    ssl_files.push({name: 'other_certificate', file: this.ui.other_ssl_certificate[0].files[0]});
-                }
-
-                if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) {
-                    if (must_require_ssl_files) {
-                        alert('certificate key file is not attached');
-                        return;
-                    }
-                } else {
-                    if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) {
-                        alert('certificate key file is too large (> 5kb)');
-                        return;
-                    }
-                    ssl_files.push({name: 'other_certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]});
-                }
-            }
-
             this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
             method(data)
                 .then(result => {
                     view.model.set(result);
 
-                    // Now upload the certs if we need to
-                    if (ssl_files.length) {
-                        let form_data = new FormData();
-
-                        ssl_files.map(function (file) {
-                            form_data.append(file.name, file.file);
-                        });
-
-                        return App.Api.Nginx.RedirectionHosts.setCerts(view.model.get('id'), form_data)
-                            .then(result => {
-                                view.model.set('meta', _.assign({}, view.model.get('meta'), result));
-                            });
-                    }
-                })
-                .then(() => {
                     App.UI.closeModal(function () {
                         if (is_new) {
                             App.Controller.showNginxRedirection();
@@ -149,18 +110,14 @@ module.exports = Mn.View.extend({
 
     templateContext: {
         getLetsencryptEmail: function () {
-            return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email');
-        },
-
-        getLetsencryptAgree: function () {
-            return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false;
+            return App.Cache.User.get('email');
         }
     },
 
     onRender: function () {
-        this.ui.ssl_enabled.trigger('change');
-        this.ui.ssl_provider.trigger('change');
+        let view = this;
 
+        // Domain names
         this.ui.domain_names.selectize({
             delimiter:    ',',
             persist:      false,
@@ -173,6 +130,37 @@ module.exports = Mn.View.extend({
             },
             createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/
         });
+
+        // Certificates
+        this.ui.letsencrypt.hide();
+        this.ui.certificate_select.selectize({
+            valueField:       'id',
+            labelField:       'nice_name',
+            searchField:      ['nice_name', 'domain_names'],
+            create:           false,
+            preload:          true,
+            allowEmptyOption: true,
+            render:           {
+                option: function (item) {
+                    item.i18n         = App.i18n;
+                    item.formatDbDate = Helpers.formatDbDate;
+                    return certListItemTemplate(item);
+                }
+            },
+            load:             function (query, callback) {
+                App.Api.Nginx.Certificates.getAll()
+                    .then(rows => {
+                        callback(rows);
+                    })
+                    .catch(err => {
+                        console.error(err);
+                        callback();
+                    });
+            },
+            onLoad:           function () {
+                view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
+            }
+        });
     },
 
     initialize: function (options) {

+ 1 - 1
src/frontend/js/i18n/messages.json

@@ -149,6 +149,7 @@
       "title": "Access Lists",
       "empty": "There are no Access Lists",
       "add": "Add Access List",
+      "form-title": "{id, select, undefined{New} other{Edit}} Access List",
       "delete": "Delete Access List",
       "delete-confirm": "Are you sure you want to delete this access list? Any hosts using it will need to be updated later.",
       "public": "Publicly Accessible",
@@ -196,7 +197,6 @@
       "deleted": "Deleted {name}",
       "meta-title": "Details for Event",
       "view-meta": "View Details",
-      "action": "Action",
       "date": "Date"
     }
   }

+ 2 - 1
src/frontend/js/models/access-list.js

@@ -7,9 +7,10 @@ const model = Backbone.Model.extend({
 
     defaults: function () {
         return {
-            id:              0,
+            id:              undefined,
             created_on:      null,
             modified_on:     null,
+            name:            '',
             // The following are expansions:
             owner:           null
         };

+ 1 - 1
src/frontend/js/models/certificate.js

@@ -7,7 +7,7 @@ const model = Backbone.Model.extend({
 
     defaults: function () {
         return {
-            id:                0,
+            id:                undefined,
             created_on:        null,
             modified_on:       null,
             provider:          '',

+ 1 - 1
src/frontend/js/models/dead-host.js

@@ -7,7 +7,7 @@ const model = Backbone.Model.extend({
 
     defaults: function () {
         return {
-            id:             0,
+            id:             undefined,
             created_on:     null,
             modified_on:    null,
             domain_names:   [],

+ 1 - 1
src/frontend/js/models/proxy-host.js

@@ -7,7 +7,7 @@ const model = Backbone.Model.extend({
 
     defaults: function () {
         return {
-            id:              0,
+            id:              undefined,
             created_on:      null,
             modified_on:     null,
             domain_names:    [],

+ 1 - 1
src/frontend/js/models/redirection-host.js

@@ -7,7 +7,7 @@ const model = Backbone.Model.extend({
 
     defaults: function () {
         return {
-            id:                  0,
+            id:                  undefined,
             created_on:          null,
             modified_on:         null,
             domain_names:        [],

+ 1 - 1
src/frontend/js/models/stream.js

@@ -7,7 +7,7 @@ const model = Backbone.Model.extend({
 
     defaults: function () {
         return {
-            id:              0,
+            id:              undefined,
             created_on:      null,
             modified_on:     null,
             incoming_port:   null,

+ 1 - 1
src/frontend/js/models/user.js

@@ -8,7 +8,7 @@ const model = Backbone.Model.extend({
 
     defaults: function () {
         return {
-            id:          0,
+            id:          undefined,
             name:        '',
             nickname:    '',
             email:       '',