Certificates polish

This commit is contained in:
Jamie Curnow
2018-08-13 19:50:28 +10:00
parent c8592503e3
commit 065727fba2
15 changed files with 563 additions and 256 deletions

View File

@@ -47,7 +47,7 @@
items.push(meta.name);
break;
case 'certificate':
%> <span class="text-teal"><i class="fe fe-shield"></i></span> <%
%> <span class="text-pink"><i class="fe fe-shield"></i></span> <%
if (meta.provider === 'letsencrypt') {
items = meta.domain_names;
} else {

View File

@@ -0,0 +1,18 @@
<div>
<% if (id === 'new') { %>
<div class="title">
<i class="fe fe-shield text-success"></i> Request a new SSL Certificate
</div>
<span class="description">with Let's Encrypt</span>
<% } else if (id > 0) { %>
<div class="title">
<i class="fe fe-shield text-pink"></i> <%- provider === 'other' ? nice_name : domain_names.join(', ') %>
</div>
<span class="description"><%- i18n('ssl', provider) %> &ndash; Expires: <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %></span>
<% } else { %>
<div class="title">
<i class="fe fe-shield-off text-danger"></i> None
</div>
<span class="description">This host will not use HTTPS</span>
<% } %>
</div>

View File

@@ -1,12 +1,12 @@
<div class="card">
<div class="card-status bg-teal"></div>
<div class="card-status bg-pink"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('certificates', 'title') %></h3>
<div class="card-options">
<a href="#" class="btn btn-outline-secondary btn-sm ml-2 help"><i class="fe fe-help-circle"></i></a>
<% if (showAddButton) { %>
<div class="dropdown">
<button type="button" class="btn btn-outline-teal btn-sm ml-2 dropdown-toggle" data-toggle="dropdown">
<button type="button" class="btn btn-outline-pink btn-sm ml-2 dropdown-toggle" data-toggle="dropdown">
<%- i18n('certificates', 'add') %>
</button>
<div class="dropdown-menu">

View File

@@ -57,7 +57,7 @@ module.exports = Mn.View.extend({
title: App.i18n('certificates', 'empty'),
subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}),
link: manage ? App.i18n('certificates', 'add') : null,
btn_color: 'teal',
btn_color: 'pink',
permission: 'certificates',
action: function () {
App.Controller.showNginxCertificateForm();

View File

@@ -52,7 +52,7 @@
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label">Access List</label>
<label class="form-label"><%- i18n('proxy-hosts', 'access-list') %></label>
<select name="access_list_id" class="form-control custom-select">
<option value="0" selected="selected"><%- i18n('access-lists', 'public') %></option>
</select>
@@ -64,76 +64,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>
</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>
<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>

View File

@@ -1,10 +1,11 @@
'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 ProxyHostModel = require('../../../models/proxy-host');
const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs');
const Helpers = require('../../../lib/helpers');
require('jquery-serializejson');
require('jquery-mask-plugin');
@@ -16,36 +17,28 @@ module.exports = Mn.View.extend({
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',
domain_names: 'input[name="domain_names"]',
forward_ip: 'input[name="forward_ip"]',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
certificate_select: 'select[name="certificate_id"]',
ssl_options: '#ssl-options input',
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_options.prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5);
},
'click @ui.save': function (e) {
@@ -63,23 +56,30 @@ module.exports = Mn.View.extend({
data.forward_port = parseInt(data.forward_port, 10);
data.block_exploits = !!data.block_exploits;
data.caching_enabled = !!data.caching_enabled;
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.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;
// 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;
}
});
let must_require_ssl_files = require_ssl_files && !view.model.hasSslFiles('other');
if (domain_err) {
alert('Cannot request Let\'s Encrypt Certificate for wildcard domains');
return;
}
} else {
data.certificate_id = parseInt(data.certificate_id, 0);
}
let method = App.Api.Nginx.ProxyHosts.create;
let is_new = true;
if (this.model.get('id')) {
// edit
@@ -88,55 +88,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.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();
@@ -152,23 +108,20 @@ 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 () {
let view = this;
// IP Address
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');
// Domain names
this.ui.domain_names.selectize({
delimiter: ',',
persist: false,
@@ -181,6 +134,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) {

View File

@@ -88,7 +88,8 @@
"delete": "Delete Proxy Host",
"delete-confirm": "Are you sure you want to delete the Proxy host for: <strong>{domains}</strong>?",
"help-title": "What is a Proxy Host?",
"help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager."
"help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.",
"access-list": "Access List"
},
"redirection-hosts": {
"title": "Redirection Hosts",

View File

@@ -1,6 +1,7 @@
'use strict';
const numeral = require('numeral');
const moment = require('moment');
module.exports = {
@@ -10,5 +11,18 @@ module.exports = {
*/
niceNumber: function (number) {
return numeral(number).format('0,0');
},
/**
* @param {String|Number} date
* @param {String} format
* @returns {String}
*/
formatDbDate: function (date, format) {
if (typeof date === 'number') {
return moment.unix(date).format(format);
}
return moment(date).format(format);
}
};

View File

@@ -1,30 +1,16 @@
'use strict';
const _ = require('underscore');
const Mn = require('backbone.marionette');
const moment = require('moment');
const i18n = require('../app/i18n');
const _ = require('underscore');
const Mn = require('backbone.marionette');
const i18n = require('../app/i18n');
const Helpers = require('./helpers');
let render = Mn.Renderer.render;
Mn.Renderer.render = function (template, data, view) {
data = _.clone(data);
data.i18n = i18n;
/**
* @param {String} date
* @param {String} format
* @returns {String}
*/
data.formatDbDate = function (date, format) {
if (typeof date === 'number') {
return moment.unix(date).format(format);
}
return moment(date).format(format);
};
data = _.clone(data);
data.i18n = i18n;
data.formatDbDate = Helpers.formatDbDate;
return render.call(this, template, data, view);
};

View File

@@ -0,0 +1,192 @@
.selectize-dropdown-header {
position: relative;
padding: 5px 8px;
background: #f8f8f8;
border-bottom: 1px solid #d0d0d0;
-webkit-border-radius: 3px 3px 0 0;
-moz-border-radius: 3px 3px 0 0;
border-radius: 3px 3px 0 0;
}
.selectize-dropdown-header-close {
position: absolute;
top: 50%;
right: 8px;
margin-top: -12px;
font-size: 20px !important;
line-height: 20px;
color: #303030;
opacity: 0.4;
}
.selectize-dropdown-header-close:hover {
color: #000000;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup {
float: left;
border-top: 0 none;
border-right: 1px solid #f2f2f2;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
border-right: 0 none;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
display: none;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
border-top: 0 none;
}
.selectize-control.plugin-remove_button [data-value] {
position: relative;
padding-right: 24px !important;
}
.selectize-control.plugin-remove_button [data-value] .remove {
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: inline-block;
width: 17px;
padding: 2px 0 0 0;
font-size: 12px;
font-weight: bold;
color: inherit;
text-align: center;
text-decoration: none;
vertical-align: middle;
border-left: 1px solid #0073bb;
-webkit-border-radius: 0 2px 2px 0;
-moz-border-radius: 0 2px 2px 0;
border-radius: 0 2px 2px 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.selectize-control.plugin-remove_button [data-value] .remove:hover {
background: rgba(0, 0, 0, 0.05);
}
.selectize-control.plugin-remove_button [data-value].active .remove {
border-left-color: #00578d;
}
.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {
background: none;
}
.selectize-control.plugin-remove_button .disabled [data-value] .remove {
border-left-color: #aaaaaa;
}
.selectize-control {
position: relative;
}
.selectize-dropdown {
font-family: inherit;
font-size: 13px;
-webkit-font-smoothing: inherit;
line-height: 18px;
color: #303030;
}
.selectize-control.single {
display: inline-block;
cursor: text;
background: #ffffff;
}
.selectize-dropdown {
position: absolute;
z-index: 10;
margin: -1px 0 0 0;
background: #ffffff;
border: 1px solid #d0d0d0;
border-top: 0 none;
-webkit-border-radius: 0 0 3px 3px;
-moz-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.selectize-dropdown [data-selectable] {
overflow: hidden;
cursor: pointer;
}
.selectize-dropdown [data-selectable] .highlight {
background: rgba(125, 168, 208, 0.2);
-webkit-border-radius: 1px;
-moz-border-radius: 1px;
border-radius: 1px;
}
.selectize-dropdown [data-selectable],
.selectize-dropdown .optgroup-header {
padding: 5px 8px;
}
.selectize-dropdown .optgroup:first-child .optgroup-header {
border-top: 0 none;
}
.selectize-dropdown .optgroup-header {
color: #303030;
cursor: default;
background: #ffffff;
}
.selectize-dropdown .active {
color: #495c68;
background-color: #f5fafd;
}
.selectize-dropdown .active.create {
color: #495c68;
}
.selectize-dropdown .create {
color: rgba(48, 48, 48, 0.5);
}
.selectize-dropdown-content {
max-height: 200px;
overflow-x: hidden;
overflow-y: auto;
.title {
font-weight: bold;
}
.description {
padding-left: 16px;
}
}
.selectize-dropdown .optgroup-header {
padding-top: 7px;
font-size: 0.85em;
font-weight: bold;
}
.selectize-dropdown .optgroup {
border-top: 1px solid #f0f0f0;
}
.selectize-dropdown .optgroup:first-child {
border-top: 0 none;
}

View File

@@ -1,5 +1,6 @@
@import "~tabler-ui/dist/assets/css/dashboard";
@import "tabler-extra";
@import "selectize";
@import "custom";
/* Before any JS content is loaded */

View File

@@ -1,6 +1,7 @@
$teal: #2bcbba;
$yellow: #f1c40f;
$blue: #467fcf;
$pink: #f66d9b;
/* For Card bodies where I don't want padding */
.card-body.no-padding {
@@ -67,6 +68,26 @@ $blue: #467fcf;
border-color: $blue;
}
/* Pink Outline Buttons */
.btn-outline-pink {
color: $pink;
background-color: transparent;
background-image: none;
border-color: $pink;
}
.btn-outline-pink:hover {
color: #fff;
background-color: $pink;
border-color: $pink;
}
.btn-outline-pink:not(:disabled):not(.disabled):active, .btn-outline-pink:not(:disabled):not(.disabled).active, .show > .btn-outline-pink.dropdown-toggle {
color: #fff;
background-color: $pink;
border-color: $pink;
}
/* dimmer */
.dimmer .loader {