* Fix wrapping when too many hosts are shown (#207)

* Update npm packages, fixes CVE-2019-10757

* Revert some breaking packages

* Major overhaul

- Docker buildx support in CI
- Cypress API Testing in CI
- Restructured folder layout (insert clean face meme)
- Added Swagger documentation and validate API against that (to be completed)
- Use common base image for all supported archs, which includes updated nginx with ipv6 support
- Updated certbot and changes required for it
- Large amount of Hosts names will wrap in UI
- Updated packages for frontend
- Version bump 2.1.0

* Updated documentation

* Fix JWT expire time going crazy. Now set to 1day

* Backend JS formatting rules

* Remove v1 importer, I doubt anyone is using v1 anymore

* Added backend formatting rules and enforce them
in Jenkins builds

* Fix CI, doesn't need a tty

* Thanks bcrypt. Why can't you just be normal.

* Cleanup after syntax check

Co-authored-by: Marcelo Castagna <margaale@users.noreply.github.com>
This commit is contained in:
jc21
2020-02-19 15:55:06 +11:00
committed by GitHub
parent bf036cbb88
commit bb0f4bfa62
517 changed files with 26256 additions and 11724 deletions

688
frontend/js/app/api.js Normal file
View File

@ -0,0 +1,688 @@
const $ = require('jquery');
const _ = require('underscore');
const Tokens = require('./tokens');
/**
* @param {String} message
* @param {*} debug
* @param {Number} code
* @constructor
*/
const ApiError = function (message, debug, code) {
let temp = Error.call(this, message);
temp.name = this.name = 'ApiError';
this.stack = temp.stack;
this.message = temp.message;
this.debug = debug;
this.code = code;
};
ApiError.prototype = Object.create(Error.prototype, {
constructor: {
value: ApiError,
writable: true,
configurable: true
}
});
/**
*
* @param {String} verb
* @param {String} path
* @param {Object} [data]
* @param {Object} [options]
* @returns {Promise}
*/
function fetch(verb, path, data, options) {
options = options || {};
return new Promise(function (resolve, reject) {
let api_url = '/api/';
let url = api_url + path;
let token = Tokens.getTopToken();
if ((typeof options.contentType === 'undefined' || options.contentType.match(/json/im)) && typeof data === 'object') {
data = JSON.stringify(data);
}
$.ajax({
url: url,
data: typeof data === 'object' ? JSON.stringify(data) : data,
type: verb,
dataType: 'json',
contentType: options.contentType || 'application/json; charset=UTF-8',
processData: options.processData || true,
crossDomain: true,
timeout: options.timeout ? options.timeout : 30000,
xhrFields: {
withCredentials: true
},
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null));
},
success: function (data, textStatus, response) {
let total = response.getResponseHeader('X-Dataset-Total');
if (total !== null) {
resolve({
data: data,
pagination: {
total: parseInt(total, 10),
offset: parseInt(response.getResponseHeader('X-Dataset-Offset'), 10),
limit: parseInt(response.getResponseHeader('X-Dataset-Limit'), 10)
}
});
} else {
resolve(response);
}
},
error: function (xhr, status, error_thrown) {
let code = 400;
if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') {
error_thrown = xhr.responseJSON.error.message;
code = xhr.responseJSON.error.code || 500;
}
reject(new ApiError(error_thrown, xhr.responseText, code));
}
});
});
}
/**
*
* @param {Array} expand
* @returns {String}
*/
function makeExpansionString(expand) {
let items = [];
_.forEach(expand, function (exp) {
items.push(encodeURIComponent(exp));
});
return items.join(',');
}
/**
* @param {String} path
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
function getAllObjects(path, expand, query) {
let params = [];
if (typeof expand === 'object' && expand !== null && expand.length) {
params.push('expand=' + makeExpansionString(expand));
}
if (typeof query === 'string') {
params.push('query=' + query);
}
return fetch('get', path + (params.length ? '?' + params.join('&') : ''));
}
function FileUpload(path, fd) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
let token = Tokens.getTopToken();
xhr.open('POST', '/api/' + path);
xhr.overrideMimeType('text/plain');
xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null));
xhr.send(fd);
xhr.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE) {
if (xhr.status !== 200 && xhr.status !== 201) {
reject(new Error('Upload failed: ' + xhr.status));
} else {
resolve(xhr.responseText);
}
}
};
});
}
module.exports = {
status: function () {
return fetch('get', '');
},
Tokens: {
/**
* @param {String} identity
* @param {String} secret
* @param {Boolean} [wipe] Will wipe the stack before adding to it again if login was successful
* @returns {Promise}
*/
login: function (identity, secret, wipe) {
return fetch('post', 'tokens', {identity: identity, secret: secret})
.then(response => {
if (response.token) {
if (wipe) {
Tokens.clearTokens();
}
// Set storage token
Tokens.addToken(response.token);
return response.token;
} else {
Tokens.clearTokens();
throw(new Error('No token returned'));
}
});
},
/**
* @returns {Promise}
*/
refresh: function () {
return fetch('get', 'tokens')
.then(response => {
if (response.token) {
Tokens.setCurrentToken(response.token);
return response.token;
} else {
Tokens.clearTokens();
throw(new Error('No token returned'));
}
});
}
},
Users: {
/**
* @param {Number|String} user_id
* @param {Array} [expand]
* @returns {Promise}
*/
getById: function (user_id, expand) {
return fetch('get', 'users/' + user_id + (typeof expand === 'object' && expand.length ? '?expand=' + makeExpansionString(expand) : ''));
},
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('users', expand, query);
},
/**
* @param {Object} data
* @returns {Promise}
*/
create: function (data) {
return fetch('post', 'users', data);
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'users/' + id, data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
delete: function (id) {
return fetch('delete', 'users/' + id);
},
/**
*
* @param {Number} id
* @param {Object} auth
* @returns {Promise}
*/
setPassword: function (id, auth) {
return fetch('put', 'users/' + id + '/auth', auth);
},
/**
* @param {Number} id
* @returns {Promise}
*/
loginAs: function (id) {
return fetch('post', 'users/' + id + '/login');
},
/**
*
* @param {Number} id
* @param {Object} perms
* @returns {Promise}
*/
setPermissions: function (id, perms) {
return fetch('put', 'users/' + id + '/permissions', perms);
}
},
Nginx: {
ProxyHosts: {
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('nginx/proxy-hosts', expand, query);
},
/**
* @param {Object} data
*/
create: function (data) {
return fetch('post', 'nginx/proxy-hosts', data);
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'nginx/proxy-hosts/' + id, data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
delete: function (id) {
return fetch('delete', 'nginx/proxy-hosts/' + id);
},
/**
* @param {Number} id
* @returns {Promise}
*/
get: function (id) {
return fetch('get', 'nginx/proxy-hosts/' + id);
},
/**
* @param {Number} id
* @returns {Promise}
*/
enable: function (id) {
return fetch('post', 'nginx/proxy-hosts/' + id + '/enable');
},
/**
* @param {Number} id
* @returns {Promise}
*/
disable: function (id) {
return fetch('post', 'nginx/proxy-hosts/' + id + '/disable');
}
},
RedirectionHosts: {
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('nginx/redirection-hosts', expand, query);
},
/**
* @param {Object} data
*/
create: function (data) {
return fetch('post', 'nginx/redirection-hosts', data);
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'nginx/redirection-hosts/' + id, data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
delete: function (id) {
return fetch('delete', 'nginx/redirection-hosts/' + id);
},
/**
* @param {Number} id
* @returns {Promise}
*/
get: function (id) {
return fetch('get', 'nginx/redirection-hosts/' + id);
},
/**
* @param {Number} id
* @param {FormData} form_data
* @params {Promise}
*/
setCerts: function (id, form_data) {
return FileUpload('nginx/redirection-hosts/' + id + '/certificates', form_data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
enable: function (id) {
return fetch('post', 'nginx/redirection-hosts/' + id + '/enable');
},
/**
* @param {Number} id
* @returns {Promise}
*/
disable: function (id) {
return fetch('post', 'nginx/redirection-hosts/' + id + '/disable');
}
},
Streams: {
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('nginx/streams', expand, query);
},
/**
* @param {Object} data
*/
create: function (data) {
return fetch('post', 'nginx/streams', data);
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'nginx/streams/' + id, data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
delete: function (id) {
return fetch('delete', 'nginx/streams/' + id);
},
/**
* @param {Number} id
* @returns {Promise}
*/
get: function (id) {
return fetch('get', 'nginx/streams/' + id);
},
/**
* @param {Number} id
* @returns {Promise}
*/
enable: function (id) {
return fetch('post', 'nginx/streams/' + id + '/enable');
},
/**
* @param {Number} id
* @returns {Promise}
*/
disable: function (id) {
return fetch('post', 'nginx/streams/' + id + '/disable');
}
},
DeadHosts: {
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('nginx/dead-hosts', expand, query);
},
/**
* @param {Object} data
*/
create: function (data) {
return fetch('post', 'nginx/dead-hosts', data);
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'nginx/dead-hosts/' + id, data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
delete: function (id) {
return fetch('delete', 'nginx/dead-hosts/' + id);
},
/**
* @param {Number} id
* @returns {Promise}
*/
get: function (id) {
return fetch('get', 'nginx/dead-hosts/' + id);
},
/**
* @param {Number} id
* @param {FormData} form_data
* @params {Promise}
*/
setCerts: function (id, form_data) {
return FileUpload('nginx/dead-hosts/' + id + '/certificates', form_data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
enable: function (id) {
return fetch('post', 'nginx/dead-hosts/' + id + '/enable');
},
/**
* @param {Number} id
* @returns {Promise}
*/
disable: function (id) {
return fetch('post', 'nginx/dead-hosts/' + id + '/disable');
}
},
AccessLists: {
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('nginx/access-lists', expand, query);
},
/**
* @param {Object} data
*/
create: function (data) {
return fetch('post', 'nginx/access-lists', data);
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'nginx/access-lists/' + id, data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
delete: function (id) {
return fetch('delete', 'nginx/access-lists/' + id);
}
},
Certificates: {
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('nginx/certificates', expand, query);
},
/**
* @param {Object} data
*/
create: function (data) {
return fetch('post', 'nginx/certificates', data);
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'nginx/certificates/' + id, data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
delete: function (id) {
return fetch('delete', 'nginx/certificates/' + id);
},
/**
* @param {Number} id
* @param {FormData} form_data
* @params {Promise}
*/
upload: function (id, form_data) {
return FileUpload('nginx/certificates/' + id + '/upload', form_data);
},
/**
* @param {FormData} form_data
* @params {Promise}
*/
validate: function (form_data) {
return FileUpload('nginx/certificates/validate', form_data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
renew: function (id) {
return fetch('post', 'nginx/certificates/' + id + '/renew');
}
}
},
AuditLog: {
/**
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (expand, query) {
return getAllObjects('audit-log', expand, query);
}
},
Reports: {
/**
* @returns {Promise}
*/
getHostStats: function () {
return fetch('get', 'reports/hosts');
}
},
Settings: {
/**
* @param {String} setting_id
* @returns {Promise}
*/
getById: function (setting_id) {
return fetch('get', 'settings/' + setting_id);
},
/**
* @returns {Promise}
*/
getAll: function () {
return getAllObjects('settings');
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'settings/' + id, data);
}
}
};

View File

@ -0,0 +1,80 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- user.avatar || '/images/default-avatar.jpg' %>)">
<span class="avatar-status <%- user.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div>
<% if (user.is_deleted) {
%>
<span class="mdi-format-strikethrough" title="Deleted"><%- user.name %></span>
<%
} else {
%>
<%- user.name %>
<%
}
%>
</div>
</td>
<td>
<div>
<%
var items = [];
switch (object_type) {
case 'proxy-host':
%> <span class="text-success"><i class="fe fe-zap"></i></span> <%
items = meta.domain_names;
break;
case 'redirection-host':
%> <span class="text-yellow"><i class="fe fe-shuffle"></i></span> <%
items = meta.domain_names;
break;
case 'stream':
%> <span class="text-blue"><i class="fe fe-radio"></i></span> <%
items.push(meta.incoming_port);
break;
case 'dead-host':
%> <span class="text-danger"><i class="fe fe-zap-off"></i></span> <%
items = meta.domain_names;
break;
case 'access-list':
%> <span class="text-teal"><i class="fe fe-lock"></i></span> <%
items.push(meta.name);
break;
case 'user':
%> <span class="text-teal"><i class="fe fe-user"></i></span> <%
items.push(meta.name);
break;
case 'certificate':
%> <span class="text-pink"><i class="fe fe-shield"></i></span> <%
if (meta.provider === 'letsencrypt') {
items = meta.domain_names;
} else {
items.push(meta.nice_name);
}
break;
}
%>&nbsp;<%- i18n('audit-log', action, {name: i18n('audit-log', object_type)}) %>
&mdash;
<%
if (items && items.length) {
items.map(function(item) {
%>
<span class="tag"><%- item %></span>
<%
});
} else {
%>
#<%- object_id %>
<%
}
%>
</div>
<div class="small text-muted">
<%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %>
</div>
</td>
<td class="text-right">
<a href="#" class="meta btn btn-secondary btn-sm"><%- i18n('audit-log', 'view-meta') %></a>
</td>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const Controller = require('../../controller');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
meta: 'a.meta'
},
events: {
'click @ui.meta': function (e) {
e.preventDefault();
Controller.showAuditMeta(this.model);
}
},
templateContext: {
more: function() {
switch (this.object_type) {
case 'redirection-host':
case 'stream':
case 'proxy-host':
return this.meta.domain_names.join(', ');
}
return '#' + (this.object_id || '?');
}
}
});

View File

@ -0,0 +1,9 @@
<thead>
<th width="30">&nbsp;</th>
<th>User</th>
<th>Event</th>
<th>&nbsp;</th>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,27 @@
const Mn = require('backbone.marionette');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,15 @@
<div class="card">
<div class="card-status bg-teal"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('audit-log', 'title') %></h3>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,53 @@
const Mn = require('backbone.marionette');
const App = require('../main');
const AuditLogModel = require('../../models/audit-log');
const ListView = require('./list/main');
const template = require('./main.ejs');
const ErrorView = require('../error/main');
const EmptyView = require('../empty/main');
module.exports = Mn.View.extend({
id: 'audit-log',
template: template,
ui: {
list_region: '.list-region',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
onRender: function () {
let view = this;
App.Api.AuditLog.getAll(['user'])
.then(response => {
if (!view.isDestroyed() && response && response.length) {
view.showChildView('list_region', new ListView({
collection: new AuditLogModel.Collection(response)
}));
} else {
view.showChildView('list_region', new EmptyView({
title: App.i18n('audit-log', 'empty'),
subtitle: App.i18n('audit-log', 'empty-subtitle')
}));
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showAuditLog();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

View File

@ -0,0 +1,27 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('audit-log', 'meta-title') %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<div class="mb-2">
<div class="tag tag-dark">
<%- 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', 'user') %>
<span class="tag-addon tag-teal"><%- user.name %></span>
</div>
<div class="tag tag-dark">
<%- i18n('audit-log', 'date') %>
<span class="tag-addon tag-primary"><%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %></span>
</div>
</div>
<pre><%- JSON.stringify(meta, null, 2) %></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'close') %></button>
</div>
</div>

View File

@ -0,0 +1,7 @@
const Mn = require('backbone.marionette');
const template = require('./meta.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog wide'
});

10
frontend/js/app/cache.js Normal file
View File

@ -0,0 +1,10 @@
const UserModel = require('../models/user');
let cache = {
User: new UserModel.Model(),
locale: 'en',
version: null
};
module.exports = cache;

View File

@ -0,0 +1,434 @@
const Backbone = require('backbone');
const Cache = require('./cache');
const Tokens = require('./tokens');
module.exports = {
/**
* @param {String} route
* @param {Object} [options]
* @returns {Boolean}
*/
navigate: function (route, options) {
options = options || {};
Backbone.history.navigate(route.toString(), options);
return true;
},
/**
* Login
*/
showLogin: function () {
window.location = '/login';
},
/**
* Users
*/
showUsers: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './users/main'], (App, View) => {
controller.navigate('/users');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* User Form
*
* @param [model]
*/
showUserForm: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './user/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Permissions Form
*
* @param model
*/
showUserPermissions: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './user/permissions'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Password Form
*
* @param model
*/
showUserPasswordForm: function (model) {
if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) {
require(['./main', './user/password'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* User Delete Confirm
*
* @param model
*/
showUserDeleteConfirm: function (model) {
if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) {
require(['./main', './user/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Dashboard
*/
showDashboard: function () {
let controller = this;
require(['./main', './dashboard/main'], (App, View) => {
controller.navigate('/');
App.UI.showAppContent(new View());
});
},
/**
* Nginx Proxy Hosts
*/
showNginxProxy: function () {
if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
let controller = this;
require(['./main', './nginx/proxy/main'], (App, View) => {
controller.navigate('/nginx/proxy');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Proxy Host Form
*
* @param [model]
*/
showNginxProxyForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
require(['./main', './nginx/proxy/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Proxy Host Delete Confirm
*
* @param model
*/
showNginxProxyDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
require(['./main', './nginx/proxy/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Redirection Hosts
*/
showNginxRedirection: function () {
if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
let controller = this;
require(['./main', './nginx/redirection/main'], (App, View) => {
controller.navigate('/nginx/redirection');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Redirection Host Form
*
* @param [model]
*/
showNginxRedirectionForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
require(['./main', './nginx/redirection/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Proxy Redirection Delete Confirm
*
* @param model
*/
showNginxRedirectionDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
require(['./main', './nginx/redirection/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Stream Hosts
*/
showNginxStream: function () {
if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
let controller = this;
require(['./main', './nginx/stream/main'], (App, View) => {
controller.navigate('/nginx/stream');
App.UI.showAppContent(new View());
});
}
},
/**
* Stream Form
*
* @param [model]
*/
showNginxStreamForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
require(['./main', './nginx/stream/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Stream Delete Confirm
*
* @param model
*/
showNginxStreamDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
require(['./main', './nginx/stream/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Dead Hosts
*/
showNginxDead: function () {
if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
let controller = this;
require(['./main', './nginx/dead/main'], (App, View) => {
controller.navigate('/nginx/404');
App.UI.showAppContent(new View());
});
}
},
/**
* Dead Host Form
*
* @param [model]
*/
showNginxDeadForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
require(['./main', './nginx/dead/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Dead Host Delete Confirm
*
* @param model
*/
showNginxDeadDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
require(['./main', './nginx/dead/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Help Dialog
*
* @param {String} title
* @param {String} content
*/
showHelp: function (title, content) {
require(['./main', './help/main'], function (App, View) {
App.UI.showModalDialog(new View({title: title, content: content}));
});
},
/**
* Nginx Access
*/
showNginxAccess: function () {
if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
let controller = this;
require(['./main', './nginx/access/main'], (App, View) => {
controller.navigate('/nginx/access');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Access List Form
*
* @param [model]
*/
showNginxAccessListForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
require(['./main', './nginx/access/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Access List Delete Confirm
*
* @param model
*/
showNginxAccessListDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
require(['./main', './nginx/access/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Nginx Certificates
*/
showNginxCertificates: function () {
if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
let controller = this;
require(['./main', './nginx/certificates/main'], (App, View) => {
controller.navigate('/nginx/certificates');
App.UI.showAppContent(new View());
});
}
},
/**
* Nginx Certificate Form
*
* @param [model]
*/
showNginxCertificateForm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Certificate Renew
*
* @param model
*/
showNginxCertificateRenew: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/renew'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Certificate Delete Confirm
*
* @param model
*/
showNginxCertificateDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Audit Log
*/
showAuditLog: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './audit-log/main'], (App, View) => {
controller.navigate('/audit-log');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* Audit Log Metadata
*
* @param model
*/
showAuditMeta: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './audit-log/meta'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/**
* Settings
*/
showSettings: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './settings/main'], (App, View) => {
controller.navigate('/settings');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* Settings Item Form
*
* @param model
*/
showSettingForm: function (model) {
if (Cache.User.isAdmin()) {
if (model.get('id') === 'default-site') {
require(['./main', './settings/default-site/main'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
}
},
/**
* Logout
*/
logout: function () {
Tokens.dropTopToken();
this.showLogin();
}
};

View File

@ -0,0 +1,67 @@
<div class="page-header">
<h1 class="page-title"><%- i18n('dashboard', 'title', {name: getUserName()}) %></h1>
</div>
<% if (columns) { %>
<div class="row">
<% if (canShow('proxy_hosts')) { %>
<div class="col-sm-<%- 24 / columns %> col-lg-<%- 12 / columns %>">
<div class="card p-3">
<div class="d-flex align-items-center">
<span class="stamp stamp-md bg-green mr-3">
<i class="fe fe-zap"></i>
</span>
<div>
<h4 class="m-0"><a href="/nginx/proxy"><%- getHostStat('proxy') %> <small><%- i18n('proxy-hosts', 'title') %></small></a></h4>
</div>
</div>
</div>
</div>
<% } %>
<% if (canShow('redirection_hosts')) { %>
<div class="col-sm-<%- 24 / columns %> col-lg-<%- 12 / columns %>">
<div class="card p-3">
<div class="d-flex align-items-center">
<span class="stamp stamp-md bg-yellow mr-3">
<i class="fe fe-shuffle"></i>
</span>
<div>
<h4 class="m-0"><a href="/nginx/redirection"><%- getHostStat('redirection') %> <small><%- i18n('redirection-hosts', 'title') %></small></a></h4>
</div>
</div>
</div>
</div>
<% } %>
<% if (canShow('streams')) { %>
<div class="col-sm-<%- 24 / columns %> col-lg-<%- 12 / columns %>">
<div class="card p-3">
<div class="d-flex align-items-center">
<span class="stamp stamp-md bg-blue mr-3">
<i class="fe fe-radio"></i>
</span>
<div>
<h4 class="m-0"><a href="/nginx/stream"><%- getHostStat('stream') %> <small> <%- i18n('streams', 'title') %></small></a></h4>
</div>
</div>
</div>
</div>
<% } %>
<% if (canShow('dead_hosts')) { %>
<div class="col-sm-<%- 24 / columns %> col-lg-<%- 12 / columns %>">
<div class="card p-3">
<div class="d-flex align-items-center">
<span class="stamp stamp-md bg-red mr-3">
<i class="fe fe-zap-off"></i>
</span>
<div>
<h4 class="m-0"><a href="/nginx/404"><%- getHostStat('dead') %> <small><%- i18n('dead-hosts', 'title') %></small></a></h4>
</div>
</div>
</div>
</div>
<% } %>
</div>
<% } %>

View File

@ -0,0 +1,92 @@
const Mn = require('backbone.marionette');
const Cache = require('../cache');
const Controller = require('../controller');
const Api = require('../api');
const Helpers = require('../../lib/helpers');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
template: template,
id: 'dashboard',
columns: 0,
stats: {},
ui: {
links: 'a'
},
events: {
'click @ui.links': function (e) {
e.preventDefault();
Controller.navigate($(e.currentTarget).attr('href'), true);
}
},
templateContext: function () {
let view = this;
return {
getUserName: function () {
return Cache.User.get('nickname') || Cache.User.get('name');
},
getHostStat: function (type) {
if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') {
return Helpers.niceNumber(view.stats.hosts[type]);
}
return '-';
},
canShow: function (perm) {
return Cache.User.isAdmin() || Cache.User.canView(perm);
},
columns: view.columns
};
},
onRender: function () {
let view = this;
if (typeof view.stats.hosts === 'undefined') {
Api.Reports.getHostStats()
.then(response => {
if (!view.isDestroyed()) {
view.stats.hosts = response;
view.render();
}
})
.catch(err => {
console.log(err);
});
}
},
/**
* @param {Object} [model]
*/
preRender: function (model) {
this.columns = 0;
// calculate the available columns based on permissions for the objects
// and store as a variable
//let view = this;
let perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
perms.map(perm => {
this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
});
// Prevent double rendering on initial calls
if (typeof model !== 'undefined') {
this.render();
}
},
initialize: function () {
this.preRender();
this.listenTo(Cache.User, 'change', this.preRender);
}
});

View File

@ -0,0 +1,11 @@
<% if (title) { %>
<h1 class="h2 mb-3"><%- title %></h1>
<% }
if (subtitle) { %>
<p class="h4 text-muted font-weight-normal mb-7"><%- subtitle %></p>
<% }
if (link) { %>
<a class="btn btn-<%- btn_color %>" href="#"><%- link %></a>
<% } %>

View File

@ -0,0 +1,33 @@
const Mn = require('backbone.marionette');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
className: 'text-center m-7',
template: template,
options: {
btn_color: 'teal'
},
ui: {
action: 'a'
},
events: {
'click @ui.action': function (e) {
e.preventDefault();
this.getOption('action')();
}
},
templateContext: function () {
return {
title: this.getOption('title'),
subtitle: this.getOption('subtitle'),
link: this.getOption('link'),
action: typeof this.getOption('action') === 'function',
btn_color: this.getOption('btn_color')
};
}
});

View File

@ -0,0 +1,7 @@
<i class="fe fe-alert-triangle mr-2" aria-hidden="true"></i>
<%= code ? '<strong>' + code + '</strong> &mdash; ' : '' %>
<%- message %>
<% if (retry) { %>
<br><br><a href="#" class="btn btn-sm btn-warning retry"><%- i18n('str', 'try-again') %></a>
<% } %>

View File

@ -0,0 +1,27 @@
const Mn = require('backbone.marionette');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'alert alert-icon alert-warning m-5',
ui: {
retry: 'a.retry'
},
events: {
'click @ui.retry': function (e) {
e.preventDefault();
this.getOption('retry')();
}
},
templateContext: function () {
return {
message: this.getOption('message'),
code: this.getOption('code'),
retry: typeof this.getOption('retry') === 'function'
};
}
});

View File

@ -0,0 +1,12 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- title %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<%= content %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'close') %></button>
</div>
</div>

View File

@ -0,0 +1,16 @@
const Mn = require('backbone.marionette');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog wide',
templateContext: function () {
let content = this.getOption('content').split("\n");
return {
title: this.getOption('title'),
content: '<p>' + content.join('</p><p>') + '</p>'
};
}
});

23
frontend/js/app/i18n.js Normal file
View File

@ -0,0 +1,23 @@
const Cache = ('./cache');
const messages = require('../i18n/messages.json');
/**
* @param {String} namespace
* @param {String} key
* @param {Object} [data]
*/
module.exports = function (namespace, key, data) {
let locale = Cache.locale;
// check that the locale exists
if (typeof messages[locale] === 'undefined') {
locale = 'en';
}
if (typeof messages[locale][namespace] !== 'undefined' && typeof messages[locale][namespace][key] !== 'undefined') {
return messages[locale][namespace][key](data);
} else if (locale !== 'en' && typeof messages['en'][namespace] !== 'undefined' && typeof messages['en'][namespace][key] !== 'undefined') {
return messages['en'][namespace][key](data);
}
return '(MISSING: ' + namespace + '/' + key + ')';
};

155
frontend/js/app/main.js Normal file
View File

@ -0,0 +1,155 @@
const _ = require('underscore');
const Backbone = require('backbone');
const Mn = require('../lib/marionette');
const Cache = require('./cache');
const Controller = require('./controller');
const Router = require('./router');
const Api = require('./api');
const Tokens = require('./tokens');
const UI = require('./ui/main');
const i18n = require('./i18n');
const App = Mn.Application.extend({
Cache: Cache,
Api: Api,
UI: null,
i18n: i18n,
Controller: Controller,
region: {
el: '#app',
replaceElement: true
},
onStart: function (app, options) {
console.log(i18n('main', 'welcome'));
// Check if token is coming through
if (this.getParam('token')) {
Tokens.addToken(this.getParam('token'));
}
// Check if we are still logged in by refreshing the token
Api.status()
.then(result => {
Cache.version = [result.version.major, result.version.minor, result.version.revision].join('.');
})
.then(Api.Tokens.refresh)
.then(this.bootstrap)
.then(() => {
console.info(i18n('main', 'logged-in', Cache.User.attributes));
this.bootstrapTimer();
this.refreshTokenTimer();
this.UI = new UI();
this.UI.on('render', () => {
new Router(options);
Backbone.history.start({pushState: true});
// Ask the admin use to change their details
if (Cache.User.get('email') === 'admin@example.com') {
Controller.showUserForm(Cache.User);
}
});
this.getRegion().show(this.UI);
})
.catch(err => {
console.warn('Not logged in:', err.message);
Controller.showLogin();
});
},
History: {
replace: function (data) {
window.history.replaceState(_.extend(window.history.state || {}, data), document.title);
},
get: function (attr) {
return window.history.state ? window.history.state[attr] : undefined;
}
},
getParam: function (name) {
name = name.replace(/[\[\]]/g, '\\$&');
let url = window.location.href;
let regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
let results = regex.exec(url);
if (!results) {
return null;
}
if (!results[2]) {
return '';
}
return decodeURIComponent(results[2].replace(/\+/g, ' '));
},
/**
* Get user and other base info to start prime the cache and the application
*
* @returns {Promise}
*/
bootstrap: function () {
return Api.Users.getById('me', ['permissions'])
.then(response => {
Cache.User.set(response);
Tokens.setCurrentName(response.nickname || response.name);
});
},
/**
* Bootstraps the user from time to time
*/
bootstrapTimer: function () {
setTimeout(() => {
Api.status()
.then(result => {
let version = [result.version.major, result.version.minor, result.version.revision].join('.');
if (version !== Cache.version) {
document.location.reload();
}
})
.then(this.bootstrap)
.then(() => {
this.bootstrapTimer();
})
.catch(err => {
if (err.message !== 'timeout' && err.code && err.code !== 400) {
console.log(err);
console.error(err.message);
console.info('Not logged in?');
Controller.showLogin();
} else {
this.bootstrapTimer();
}
});
}, 30 * 1000); // 30 seconds
},
refreshTokenTimer: function () {
setTimeout(() => {
return Api.Tokens.refresh()
.then(this.bootstrap)
.then(() => {
this.refreshTokenTimer();
})
.catch(err => {
if (err.message !== 'timeout' && err.code && err.code !== 400) {
console.log(err);
console.error(err.message);
console.info('Not logged in?');
Controller.showLogin();
} else {
this.refreshTokenTimer();
}
});
}, 10 * 60 * 1000);
}
});
const app = new App();
module.exports = app;

View File

@ -0,0 +1,23 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('access-lists', 'delete') %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<%= i18n('access-lists', 'delete-confirm') %>
<% if (proxy_host_count) { %>
<br><br>
<%- i18n('access-lists', 'delete-has-hosts', {count: proxy_host_count}) %>
<% } %>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-danger save"><%- i18n('str', 'sure') %></button>
</div>
</div>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./delete.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
App.Api.Nginx.AccessLists.delete(this.model.get('id'))
.then(() => {
App.Controller.showNginxAccess();
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -0,0 +1,34 @@
<div class="modal-content">
<div class="modal-header">
<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">
<form>
<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>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'username') %></label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'password') %></label>
</div>
</div>
</div>
<div class="items"><!-- items --></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,110 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const AccessListModel = require('../../../models/access-list');
const template = require('./form.ejs');
const ItemView = require('./form/item');
require('jquery-serializejson');
const ItemsView = Mn.CollectionView.extend({
childView: ItemView
});
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
items_region: '.items',
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
regions: {
items_region: '@ui.items_region'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let form_data = this.ui.form.serializeJSON();
let items_data = [];
form_data.username.map(function (val, idx) {
if (val.trim().length) {
items_data.push({
username: val.trim(),
password: form_data.password[idx]
});
}
});
if (!items_data.length) {
alert('You must specify at least 1 Username and Password combination');
return;
}
let data = {
name: form_data.name,
items: items_data
};
let method = App.Api.Nginx.AccessLists.create;
let is_new = true;
if (this.model.get('id')) {
// edit
is_new = false;
method = App.Api.Nginx.AccessLists.update;
data.id = this.model.get('id');
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
method(data)
.then(result => {
view.model.set(result);
App.UI.closeModal(function () {
if (is_new) {
App.Controller.showNginxAccess();
}
});
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
onRender: function () {
let items = this.model.get('items');
// Add empty items to the end of the list. This is cheating but hey I don't have the time to do it right
let items_to_add = 5 - items.length;
if (items_to_add) {
for (let i = 0; i < items_to_add; i++) {
items.push({});
}
}
this.showChildView('items_region', new ItemsView({
collection: new Backbone.Collection(items)
}));
},
initialize: function (options) {
if (typeof options.model === 'undefined' || !options.model) {
this.model = new AccessListModel.Model();
}
}
});

View File

@ -0,0 +1,10 @@
<div class="col-sm-6 col-md-6">
<div class="form-group">
<input type="text" name="username[]" class="form-control" value="<%- typeof username !== 'undefined' ? username : '' %>">
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<input type="password" name="password[]" class="form-control" placeholder="<%- typeof hint !== 'undefined' ? hint : '' %>" value="">
</div>
</div>

View File

@ -0,0 +1,7 @@
const Mn = require('backbone.marionette');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'row'
});

View File

@ -0,0 +1,31 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div>
<%- name %>
</div>
<div class="small text-muted">
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
</div>
</td>
<td>
<%- i18n('access-lists', 'item-count', {count: items.length || 0}) %>
</td>
<td>
<%- i18n('access-lists', 'proxy-host-count', {count: proxy_host_count}) %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
<div class="dropdown-divider"></div>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
</div>
</div>
</td>
<% } %>

View File

@ -0,0 +1,33 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
edit: 'a.edit',
delete: 'a.delete'
},
events: {
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showNginxAccessListForm(this.model);
},
'click @ui.delete': function (e) {
e.preventDefault();
App.Controller.showNginxAccessListDeleteConfirm(this.model);
}
},
templateContext: {
canManage: App.Cache.User.canManage('access_lists')
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,12 @@
<thead>
<th width="30">&nbsp;</th>
<th><%- i18n('str', 'name') %></th>
<th><%- i18n('users', 'title') %></th>
<th><%- i18n('proxy-hosts', 'title') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
templateContext: {
canManage: App.Cache.User.canManage('access_lists')
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,20 @@
<div class="card">
<div class="card-status bg-teal"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('access-lists', '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) { %>
<a href="#" class="btn btn-outline-teal btn-sm ml-2 add-item"><%- i18n('access-lists', 'add') %></a>
<% } %>
</div>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,81 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const AccessListModel = require('../../../models/access-list');
const ListView = require('./list/main');
const ErrorView = require('../../error/main');
const EmptyView = require('../../empty/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'nginx-access',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
help: '.help',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
events: {
'click @ui.add': function (e) {
e.preventDefault();
App.Controller.showNginxAccessListForm();
},
'click @ui.help': function (e) {
e.preventDefault();
App.Controller.showHelp(App.i18n('access-lists', 'help-title'), App.i18n('access-lists', 'help-content'));
}
},
templateContext: {
showAddButton: App.Cache.User.canManage('access_lists')
},
onRender: function () {
let view = this;
App.Api.Nginx.AccessLists.getAll(['owner', 'items'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {
view.showChildView('list_region', new ListView({
collection: new AccessListModel.Collection(response)
}));
} else {
let manage = App.Cache.User.canManage('access_lists');
view.showChildView('list_region', new EmptyView({
title: App.i18n('access-lists', 'empty'),
subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}),
link: manage ? App.i18n('access-lists', 'add') : null,
btn_color: 'teal',
permission: 'access_lists',
action: function () {
App.Controller.showNginxAccessListForm();
}
}));
}
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showNginxAccess();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

View File

@ -0,0 +1,18 @@
<div>
<% if (id === 'new') { %>
<div class="title">
<i class="fe fe-shield text-success"></i> <%- i18n('all-hosts', 'new-cert') %>
</div>
<span class="description"><%- i18n('all-hosts', 'with-le') %></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> <%- i18n('all-hosts', 'none') %>
</div>
<span class="description"><%- i18n('all-hosts', 'no-ssl') %></span>
<% } %>
</div>

View File

@ -0,0 +1,19 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('certificates', 'delete') %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<%= i18n('certificates', 'delete-confirm') %>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-danger save"><%- i18n('str', 'sure') %></button>
</div>
</div>

View File

@ -0,0 +1,31 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./delete.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
App.Api.Nginx.Certificates.delete(this.model.get('id'))
.then(() => {
App.Controller.showNginxCertificates();
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -0,0 +1,76 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('certificates', 'form-title', {provider: provider}) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<% if (provider === 'letsencrypt') { %>
<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 class="text-blue"><i class="fe fe-alert-triangle"></i> <%- i18n('ssl', 'hosts-warning') %></div>
</div>
</div>
<div class="col-sm-12 col-md-12">
<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">
<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>
<% } else if (provider === 'other') { %>
<!-- Other -->
<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 name="nice_name" type="text" class="form-control" placeholder="" value="<%- nice_name %>" required>
</div>
</div>
<div class="col-sm-12 col-md-12 other-ssl">
<div class="form-group">
<div class="form-label"><%- i18n('certificates', 'other-certificate-key') %><span class="form-required">*</span></div>
<div class="custom-file">
<input type="file" class="custom-file-input" name="meta[other_certificate_key]" id="other_certificate_key" required>
<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('certificates', 'other-certificate') %><span class="form-required">*</span></div>
<div class="custom-file">
<input type="file" class="custom-file-input" name="meta[other_certificate]" id="other_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('certificates', 'other-intermediate-certificate') %></div>
<div class="custom-file">
<input type="file" class="custom-file-input" name="meta[other_intermediate_certificate]" id="other_intermediate_certificate">
<label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
</div>
</div>
</div>
<% } %>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,156 @@
const _ = require('underscore');
const Mn = require('backbone.marionette');
const App = require('../../main');
const CertificateModel = require('../../../models/certificate');
const template = require('./form.ejs');
require('jquery-serializejson');
require('selectize');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
max_file_size: 102400,
ui: {
form: 'form',
domain_names: 'input[name="domain_names"]',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
other_certificate: '#other_certificate',
other_certificate_key: '#other_certificate_key',
other_intermediate_certificate: '#other_intermediate_certificate'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
data.provider = this.model.get('provider');
// Manipulate
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 ssl_files = [];
// check files are attached
if (this.model.get('provider') === 'other' && !this.model.hasSslFiles()) {
if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) {
alert('Certificate file is not attached');
return;
} else {
if (this.ui.other_certificate[0].files[0].size > this.max_file_size) {
alert('Certificate file is too large (> 100kb)');
return;
}
ssl_files.push({name: 'certificate', file: this.ui.other_certificate[0].files[0]});
}
if (!this.ui.other_certificate_key[0].files.length || !this.ui.other_certificate_key[0].files[0].size) {
alert('Certificate key file is not attached');
return;
} else {
if (this.ui.other_certificate_key[0].files[0].size > this.max_file_size) {
alert('Certificate key file is too large (> 100kb)');
return;
}
ssl_files.push({name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0]});
}
if (this.ui.other_intermediate_certificate[0].files.length && this.ui.other_intermediate_certificate[0].files[0].size) {
if (this.ui.other_intermediate_certificate[0].files[0].size > this.max_file_size) {
alert('Intermediate Certificate file is too large (> 100kb)');
return;
}
ssl_files.push({name: 'intermediate_certificate', file: this.ui.other_intermediate_certificate[0].files[0]});
}
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
// compile file data
let form_data = new FormData();
if (view.model.get('provider') && ssl_files.length) {
ssl_files.map(function (file) {
form_data.append(file.name, file.file);
});
}
new Promise(resolve => {
if (view.model.get('provider') === 'other') {
resolve(App.Api.Nginx.Certificates.validate(form_data));
} else {
resolve();
}
})
.then(() => {
return App.Api.Nginx.Certificates.create(data);
})
.then(result => {
view.model.set(result);
// Now upload the certs if we need to
if (view.model.get('provider') === 'other') {
return App.Api.Nginx.Certificates.upload(view.model.get('id'), form_data)
.then(result => {
view.model.set('meta', _.assign({}, view.model.get('meta'), result));
});
}
})
.then(() => {
App.UI.closeModal(function () {
App.Controller.showNginxCertificates();
});
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
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.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 CertificateModel.Model({provider: 'letsencrypt'});
}
}
});

View File

@ -0,0 +1,49 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div class="wrap">
<%
if (provider === 'letsencrypt') {
domain_names.map(function(host) {
if (host.indexOf('*') === -1) {
%>
<span class="tag host-link hover-pink" rel="https://<%- host %>"><%- host %></span>
<%
} else {
%>
<span class="tag"><%- host %></span>
<%
}
});
} else {
%><%- nice_name %><%
}
%>
</div>
<div class="small text-muted">
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
</div>
</td>
<td>
<%- i18n('ssl', provider) %>
</td>
<td class="<%- isExpired() ? 'text-danger' : '' %>">
<%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<% if (provider === 'letsencrypt') { %>
<a href="#" class="renew dropdown-item"><i class="dropdown-icon fe fe-refresh-cw"></i> <%- i18n('certificates', 'force-renew') %></a>
<div class="dropdown-divider"></div>
<% } %>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
</div>
</div>
</td>
<% } %>

View File

@ -0,0 +1,44 @@
const Mn = require('backbone.marionette');
const moment = require('moment');
const App = require('../../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
host_link: '.host-link',
renew: 'a.renew',
delete: 'a.delete'
},
events: {
'click @ui.renew': function (e) {
e.preventDefault();
App.Controller.showNginxCertificateRenew(this.model);
},
'click @ui.delete': function (e) {
e.preventDefault();
App.Controller.showNginxCertificateDeleteConfirm(this.model);
},
'click @ui.host_link': function (e) {
e.preventDefault();
let win = window.open($(e.currentTarget).attr('rel'), '_blank');
win.focus();
}
},
templateContext: {
canManage: App.Cache.User.canManage('certificates'),
isExpired: function () {
return moment(this.expires_on).isBefore(moment());
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,12 @@
<thead>
<th width="30">&nbsp;</th>
<th><%- i18n('str', 'name') %></th>
<th><%- i18n('all-hosts', 'cert-provider') %></th>
<th><%- i18n('str', 'expires') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
templateContext: {
canManage: App.Cache.User.canManage('certificates')
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,28 @@
<div class="card">
<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-pink btn-sm ml-2 dropdown-toggle" data-toggle="dropdown">
<%- i18n('certificates', 'add') %>
</button>
<div class="dropdown-menu">
<a class="dropdown-item add-item" data-cert="letsencrypt" href="#"><%- i18n('ssl', 'letsencrypt') %></a>
<a class="dropdown-item add-item" data-cert="other" href="#"><%- i18n('ssl', 'other') %></a>
</div>
</div>
<% } %>
</div>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,82 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const CertificateModel = require('../../../models/certificate');
const ListView = require('./list/main');
const ErrorView = require('../../error/main');
const EmptyView = require('../../empty/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'nginx-certificates',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
help: '.help',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
events: {
'click @ui.add': function (e) {
e.preventDefault();
let model = new CertificateModel.Model({provider: $(e.currentTarget).data('cert')});
App.Controller.showNginxCertificateForm(model);
},
'click @ui.help': function (e) {
e.preventDefault();
App.Controller.showHelp(App.i18n('certificates', 'help-title'), App.i18n('certificates', 'help-content'));
}
},
templateContext: {
showAddButton: App.Cache.User.canManage('certificates')
},
onRender: function () {
let view = this;
App.Api.Nginx.Certificates.getAll(['owner'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {
view.showChildView('list_region', new ListView({
collection: new CertificateModel.Collection(response)
}));
} else {
let manage = App.Cache.User.canManage('certificates');
view.showChildView('list_region', new EmptyView({
title: App.i18n('certificates', 'empty'),
subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}),
link: manage ? App.i18n('certificates', 'add') : null,
btn_color: 'pink',
permission: 'certificates',
action: function () {
App.Controller.showNginxCertificateForm();
}
}));
}
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showNginxCertificates();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

View File

@ -0,0 +1,14 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('certificates', 'renew-title') %></h5>
</div>
<div class="modal-body">
<div class="waiting text-center">
<%= i18n('str', 'please-wait') %>
</div>
<div class="alert alert-danger error" role="alert"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal" disabled><%- i18n('str', 'close') %></button>
</div>
</div>

View File

@ -0,0 +1,31 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./renew.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
waiting: '.waiting',
error: '.error',
close: 'button.cancel'
},
onRender: function () {
this.ui.error.hide();
App.Api.Nginx.Certificates.renew(this.model.get('id'))
.then((result) => {
this.model.set(result);
setTimeout(() => {
App.UI.closeModal();
}, 1000);
})
.catch((err) => {
this.ui.waiting.hide();
this.ui.error.text(err.message).show();
this.ui.close.prop('disabled', false);
});
}
});

View File

@ -0,0 +1,23 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('dead-hosts', 'delete') %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<%= i18n('dead-hosts', 'delete-confirm', {domains: domain_names.join(', ')}) %>
<% if (certificate_id) { %>
<br><br>
<%- i18n('ssl', 'delete-ssl') %>
<% } %>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-danger save"><%- i18n('str', 'sure') %></button>
</div>
</div>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./delete.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
App.Api.Nginx.DeadHosts.delete(this.model.get('id'))
.then(() => {
App.Controller.showNginxDead();
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -0,0 +1,113 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('dead-hosts', '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">
<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>
<li role="presentation" class="nav-item"><a href="#advanced" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('all-hosts', 'advanced') %></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>
</div>
<!-- SSL -->
<div role="tabpanel" class="tab-pane" id="ssl-options">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('all-hosts', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
</select>
</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' : '' %><%- 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-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="http2_support" value="1"<%- http2_support ? ' checked' : '' %><%- certificate_id ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'http2-support') %></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="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></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="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
</label>
</div>
</div>
<!-- Lets encrypt -->
<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 disabled>
</div>
</div>
<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 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>
</div>
</div>
<!-- Advanced -->
<div role="tabpanel" class="tab-pane" id="advanced">
<div class="row">
<div class="col-md-12">
<div class="form-group mb-0">
<label class="form-label"><%- i18n('all-hosts', 'advanced-config') %></label>
<textarea name="advanced_config" rows="8" class="form-control text-monospace" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,207 @@
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',
ui: {
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"]',
hsts_enabled: 'input[name="hsts_enabled"]',
hsts_subdomains: 'input[name="hsts_subdomains"]',
http2_support: 'input[name="http2_support"]',
letsencrypt: '.letsencrypt'
},
events: {
'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);
}
let enabled = id === 'new' || parseInt(id, 10) > 0;
let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
inputs
.prop('disabled', !enabled)
.parents('.form-group')
.css('opacity', enabled ? 1 : 0.5);
if (!enabled) {
inputs.prop('checked', false);
}
inputs.trigger('change');
},
'change @ui.ssl_forced': function () {
let checked = this.ui.ssl_forced.prop('checked');
this.ui.hsts_enabled
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_enabled.prop('checked', false);
}
this.ui.hsts_enabled.trigger('change');
},
'change @ui.hsts_enabled': function () {
let checked = this.ui.hsts_enabled.prop('checked');
this.ui.hsts_subdomains
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_subdomains.prop('checked', false);
}
},
'click @ui.save': function (e) {
e.preventDefault();
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
// Manipulate
data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains;
data.http2_support = !!data.http2_support;
data.ssl_forced = !!data.ssl_forced;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');
}
// 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, 10);
}
let method = App.Api.Nginx.DeadHosts.create;
let is_new = true;
if (this.model.get('id')) {
// edit
is_new = false;
method = App.Api.Nginx.DeadHosts.update;
data.id = this.model.get('id');
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
method(data)
.then(result => {
view.model.set(result);
App.UI.closeModal(function () {
if (is_new) {
App.Controller.showNginxDead();
}
});
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
templateContext: {
getLetsencryptEmail: function () {
return App.Cache.User.get('email');
}
},
onRender: function () {
let view = this;
// Domain names
this.ui.domain_names.selectize({
delimiter: ',',
persist: false,
maxOptions: 15,
create: function (input) {
return {
value: input,
text: input
};
},
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) {
if (typeof options.model === 'undefined' || !options.model) {
this.model = new DeadHostModel.Model();
}
}
});

View File

@ -0,0 +1,53 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div>
<% domain_names.map(function(host) {
if (host.indexOf('*') === -1) {
%>
<span class="tag host-link hover-red" rel="http<%- certificate_id ? 's' : '' %>://<%- host %>"><%- host %></span>
<%
} else {
%>
<span class="tag"><%- host %></span>
<%
}
});
%>
</div>
<div class="small text-muted">
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
</div>
</td>
<td>
<div><%- certificate ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %></div>
</td>
<td>
<%
var o = isOnline();
if (!enabled) { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'disabled') %>
<% } else if (o === true) { %>
<span class="status-icon bg-success"></span> <%- i18n('str', 'online') %>
<% } else if (o === false) { %>
<span title="<%- getOfflineError() %>"><span class="status-icon bg-danger"></span> <%- i18n('str', 'offline') %></span>
<% } else { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'unknown') %>
<% } %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
<a href="#" class="able dropdown-item"><i class="dropdown-icon fe fe-power"></i> <%- i18n('str', enabled ? 'disable' : 'enable') %></a>
<div class="dropdown-divider"></div>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
</div>
</div>
</td>
<% } %>

View File

@ -0,0 +1,61 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
able: 'a.able',
edit: 'a.edit',
delete: 'a.delete',
host_link: '.host-link'
},
events: {
'click @ui.able': function (e) {
e.preventDefault();
let id = this.model.get('id');
App.Api.Nginx.DeadHosts[this.model.get('enabled') ? 'disable' : 'enable'](id)
.then(() => {
return App.Api.Nginx.DeadHosts.get(id)
.then(row => {
this.model.set(row);
});
});
},
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showNginxDeadForm(this.model);
},
'click @ui.delete': function (e) {
e.preventDefault();
App.Controller.showNginxDeadDeleteConfirm(this.model);
},
'click @ui.host_link': function (e) {
e.preventDefault();
let win = window.open($(e.currentTarget).attr('rel'), '_blank');
win.focus();
}
},
templateContext: {
canManage: App.Cache.User.canManage('dead_hosts'),
isOnline: function () {
return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online;
},
getOfflineError: function () {
return this.meta.nginx_err || '';
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,12 @@
<thead>
<th width="30">&nbsp;</th>
<th><%- i18n('str', 'source') %></th>
<th><%- i18n('str', 'ssl') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
templateContext: {
canManage: App.Cache.User.canManage('dead_hosts')
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,20 @@
<div class="card">
<div class="card-status bg-danger"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('dead-hosts', '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) { %>
<a href="#" class="btn btn-outline-danger btn-sm ml-2 add-item"><%- i18n('dead-hosts', 'add') %></a>
<% } %>
</div>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,81 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const DeadHostModel = require('../../../models/dead-host');
const ListView = require('./list/main');
const ErrorView = require('../../error/main');
const EmptyView = require('../../empty/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'nginx-dead',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
help: '.help',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
events: {
'click @ui.add': function (e) {
e.preventDefault();
App.Controller.showNginxDeadForm();
},
'click @ui.help': function (e) {
e.preventDefault();
App.Controller.showHelp(App.i18n('dead-hosts', 'help-title'), App.i18n('dead-hosts', 'help-content'));
}
},
templateContext: {
showAddButton: App.Cache.User.canManage('dead_hosts')
},
onRender: function () {
let view = this;
App.Api.Nginx.DeadHosts.getAll(['owner', 'certificate'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {
view.showChildView('list_region', new ListView({
collection: new DeadHostModel.Collection(response)
}));
} else {
let manage = App.Cache.User.canManage('dead_hosts');
view.showChildView('list_region', new EmptyView({
title: App.i18n('dead-hosts', 'empty'),
subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}),
link: manage ? App.i18n('dead-hosts', 'add') : null,
btn_color: 'danger',
permission: 'dead_hosts',
action: function () {
App.Controller.showNginxDeadForm();
}
}));
}
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showNginxDead();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

View File

@ -0,0 +1,13 @@
<div>
<% if (id > 0) { %>
<div class="title">
<i class="fe fe-lock text-teal"></i> <%- name %>
</div>
<span class="description"><%- i18n('access-lists', 'item-count', {count: items.length || 0}) %> &ndash; Created: <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %></span>
<% } else { %>
<div class="title">
<i class="fe fe-unlock text-yellow"></i> <%- i18n('access-lists', 'public') %>
</div>
<span class="description"><%- i18n('access-lists', 'public-sub') %></span>
<% } %>
</div>

View File

@ -0,0 +1,23 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('proxy-hosts', 'delete') %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<%= i18n('proxy-hosts', 'delete-confirm', {domains: domain_names.join(', ')}) %>
<% if (certificate_id) { %>
<br><br>
<%- i18n('ssl', 'delete-ssl') %>
<% } %>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-danger save"><%- i18n('str', 'sure') %></button>
</div>
</div>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./delete.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
App.Api.Nginx.ProxyHosts.delete(this.model.get('id'))
.then(() => {
App.Controller.showNginxProxy();
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -0,0 +1,187 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('proxy-hosts', '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">
<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="#locations" aria-controls="tab4" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-layers"></i> <%- i18n('all-hosts', 'locations') %></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>
<li role="presentation" class="nav-item"><a href="#advanced" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('all-hosts', 'advanced') %></a></li>
</ul>
<div class="tab-content">
<!-- Locations -->
<div class="tab-pane" id="locations">
<div class="row">
<div class="col-sm-12">
<button type="button" class="btn btn-secondary add_location"><%- i18n('locations', 'new_location') %></button>
<div class="locations_container mt-3"></div>
</div>
</div>
</div>
<!-- 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-3 col-md-3">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-scheme') %><span class="form-required">*</span></label>
<select name="forward_scheme" class="form-control custom-select" placeholder="http">
<option value="http" <%- forward_scheme === 'http' ? 'selected' : '' %>>http</option>
<option value="https" <%- forward_scheme === 'https' ? 'selected' : '' %>>https</option>
</select>
</div>
</div>
<div class="col-sm-5 col-md-5">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-host') %><span class="form-required">*</span></label>
<input type="text" name="forward_host" class="form-control text-monospace" placeholder="" value="<%- forward_host %>" autocomplete="off" maxlength="50" 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="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="caching_enabled" value="1"<%- caching_enabled ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'caching-enabled') %></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="block_exploits" value="1"<%- block_exploits ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'block-exploits') %></span>
</label>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="allow_websocket_upgrade" value="1"<%- allow_websocket_upgrade ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('proxy-hosts', 'allow-websocket-upgrade') %></span>
</label>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'access-list') %></label>
<select name="access_list_id" class="form-control custom-select" placeholder="<%- i18n('access-lists', 'public') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- access_list_id ? '' : 'selected' %>><%- i18n('access-lists', 'public') %></option>
</select>
</div>
</div>
</div>
</div>
<!-- SSL -->
<div role="tabpanel" class="tab-pane" id="ssl-options">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('all-hosts', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
</select>
</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' : '' %><%- 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-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="http2_support" value="1"<%- http2_support ? ' checked' : '' %><%- certificate_id ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'http2-support') %></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="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></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="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
</label>
</div>
</div>
<!-- Lets encrypt -->
<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 disabled>
</div>
</div>
<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 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>
</div>
</div>
<!-- Advanced -->
<div role="tabpanel" class="tab-pane" id="advanced">
<div class="row">
<div class="col-md-12">
<p>Nginx variables available to you are:</p>
<ul class="text-monospace">
<li>$server # Host/IP</li>
<li>$port # Port Number</li>
<li>$forward_scheme # http or https</li>
</ul>
<div class="form-group mb-0">
<label class="form-label"><%- i18n('all-hosts', 'advanced-config') %></label>
<textarea name="advanced_config" rows="8" class="form-control text-monospace" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,291 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const ProxyHostModel = require('../../../models/proxy-host');
const ProxyLocationModel = require('../../../models/proxy-host-location');
const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs');
const accessListItemTemplate = require('./access-list-item.ejs');
const CustomLocation = require('./location');
const Helpers = require('../../../lib/helpers');
require('jquery-serializejson');
require('selectize');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
locationsCollection: new ProxyLocationModel.Collection(),
ui: {
form: 'form',
domain_names: 'input[name="domain_names"]',
forward_host: 'input[name="forward_host"]',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
add_location_btn: 'button.add_location',
locations_container:'.locations_container',
certificate_select: 'select[name="certificate_id"]',
access_list_select: 'select[name="access_list_id"]',
ssl_forced: 'input[name="ssl_forced"]',
hsts_enabled: 'input[name="hsts_enabled"]',
hsts_subdomains: 'input[name="hsts_subdomains"]',
http2_support: 'input[name="http2_support"]',
forward_scheme: 'select[name="forward_scheme"]',
letsencrypt: '.letsencrypt'
},
regions: {
locations_regions: '@ui.locations_container'
},
events: {
'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);
}
let enabled = id === 'new' || parseInt(id, 10) > 0;
let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
inputs
.prop('disabled', !enabled)
.parents('.form-group')
.css('opacity', enabled ? 1 : 0.5);
if (!enabled) {
inputs.prop('checked', false);
}
inputs.trigger('change');
},
'change @ui.ssl_forced': function () {
let checked = this.ui.ssl_forced.prop('checked');
this.ui.hsts_enabled
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_enabled.prop('checked', false);
}
this.ui.hsts_enabled.trigger('change');
},
'change @ui.hsts_enabled': function () {
let checked = this.ui.hsts_enabled.prop('checked');
this.ui.hsts_subdomains
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_subdomains.prop('checked', false);
}
},
'click @ui.add_location_btn': function (e) {
e.preventDefault();
const model = new ProxyLocationModel.Model();
this.locationsCollection.add(model);
},
'click @ui.save': function (e) {
e.preventDefault();
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
// Add locations
data.locations = [];
this.locationsCollection.models.forEach((location) => {
data.locations.push(location.toJSON());
});
// Serialize collects path from custom locations
// This field must be removed from root object
delete data.path;
// Manipulate
data.forward_port = parseInt(data.forward_port, 10);
data.block_exploits = !!data.block_exploits;
data.caching_enabled = !!data.caching_enabled;
data.allow_websocket_upgrade = !!data.allow_websocket_upgrade;
data.http2_support = !!data.http2_support;
data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains;
data.ssl_forced = !!data.ssl_forced;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');
}
// 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, 10);
}
let method = App.Api.Nginx.ProxyHosts.create;
let is_new = true;
if (this.model.get('id')) {
// edit
is_new = false;
method = App.Api.Nginx.ProxyHosts.update;
data.id = this.model.get('id');
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
method(data)
.then(result => {
view.model.set(result);
App.UI.closeModal(function () {
if (is_new) {
App.Controller.showNginxProxy();
}
});
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
templateContext: {
getLetsencryptEmail: function () {
return App.Cache.User.get('email');
}
},
onRender: function () {
let view = this;
this.ui.ssl_forced.trigger('change');
this.ui.hsts_enabled.trigger('change');
// Domain names
this.ui.domain_names.selectize({
delimiter: ',',
persist: false,
maxOptions: 15,
create: function (input) {
return {
value: input,
text: input
};
},
createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/
});
// Access Lists
this.ui.access_list_select.selectize({
valueField: 'id',
labelField: 'name',
searchField: ['name'],
create: false,
preload: true,
allowEmptyOption: true,
render: {
option: function (item) {
item.i18n = App.i18n;
item.formatDbDate = Helpers.formatDbDate;
return accessListItemTemplate(item);
}
},
load: function (query, callback) {
App.Api.Nginx.AccessLists.getAll(['items'])
.then(rows => {
callback(rows);
})
.catch(err => {
console.error(err);
callback();
});
},
onLoad: function () {
view.ui.access_list_select[0].selectize.setValue(view.model.get('access_list_id'));
}
});
// 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) {
if (typeof options.model === 'undefined' || !options.model) {
this.model = new ProxyHostModel.Model();
}
this.locationsCollection = new ProxyLocationModel.Collection();
// Custom locations
this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({
collection: this.locationsCollection
}));
// Check wether there are any location defined
if (options.model && Array.isArray(options.model.attributes.locations)) {
options.model.attributes.locations.forEach((location) => {
let m = new ProxyLocationModel.Model(location);
this.locationsCollection.add(m);
});
}
}
});

View File

@ -0,0 +1,59 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div class="wrap">
<% domain_names.map(function(host) {
if (host.indexOf('*') === -1) {
%>
<span class="tag host-link hover-green" rel="http<%- certificate_id ? 's' : '' %>://<%- host %>"><%- host %></span>
<%
} else {
%>
<span class="tag"><%- host %></span>
<%
}
});
%>
</div>
<div class="small text-muted">
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
</div>
</td>
<td>
<div class="text-monospace"><%- forward_scheme %>://<%- forward_host %>:<%- forward_port %></div>
</td>
<td>
<div><%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %></div>
</td>
<td>
<div><%- access_list_id ? access_list.name : i18n('str', 'public') %></div>
</td>
<td>
<%
var o = isOnline();
if (!enabled) { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'disabled') %>
<% } else if (o === true) { %>
<span class="status-icon bg-success"></span> <%- i18n('str', 'online') %>
<% } else if (o === false) { %>
<span title="<%- getOfflineError() %>"><span class="status-icon bg-danger"></span> <%- i18n('str', 'offline') %></span>
<% } else { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'unknown') %>
<% } %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
<a href="#" class="able dropdown-item"><i class="dropdown-icon fe fe-power"></i> <%- i18n('str', enabled ? 'disable' : 'enable') %></a>
<div class="dropdown-divider"></div>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
</div>
</div>
</td>
<% } %>

View File

@ -0,0 +1,61 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
able: 'a.able',
edit: 'a.edit',
delete: 'a.delete',
host_link: '.host-link'
},
events: {
'click @ui.able': function (e) {
e.preventDefault();
let id = this.model.get('id');
App.Api.Nginx.ProxyHosts[this.model.get('enabled') ? 'disable' : 'enable'](id)
.then(() => {
return App.Api.Nginx.ProxyHosts.get(id)
.then(row => {
this.model.set(row);
});
});
},
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showNginxProxyForm(this.model);
},
'click @ui.delete': function (e) {
e.preventDefault();
App.Controller.showNginxProxyDeleteConfirm(this.model);
},
'click @ui.host_link': function (e) {
e.preventDefault();
let win = window.open($(e.currentTarget).attr('rel'), '_blank');
win.focus();
}
},
templateContext: {
canManage: App.Cache.User.canManage('proxy_hosts'),
isOnline: function () {
return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online;
},
getOfflineError: function () {
return this.meta.nginx_err || '';
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,14 @@
<thead>
<th width="30">&nbsp;</th>
<th><%- i18n('str', 'source') %></th>
<th><%- i18n('str', 'destination') %></th>
<th><%- i18n('str', 'ssl') %></th>
<th><%- i18n('str', 'access') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
templateContext: {
canManage: App.Cache.User.canManage('proxy_hosts')
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,64 @@
<div class="location-block card">
<div class="card-body">
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="form-label"><%- i18n('locations', 'location_label') %> <span class="form-required">*</span></label>
<div class="row gutter-xs">
<div class="col">
<div class="input-group">
<span class="input-group-prepend">
<span class="input-group-text">location</span>
</span>
<input type="text" name="path" class="form-control model" value="<%- path %>" placeholder="<%- i18n('locations', 'path') %>" required>
</div>
</div>
<div class="col-auto">
<div class="selectgroup">
<label class="selectgroup-item">
<input type="checkbox" class="selectgroup-input">
<span class="selectgroup-button">
<i class="fe fe-settings"></i>
</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-3 col-md-3">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-scheme') %><span class="form-required">*</span></label>
<select name="forward_scheme" class="form-control custom-select model" placeholder="http">
<option value="http" <%- forward_scheme === 'http' ? 'selected' : '' %>>http</option>
<option value="https" <%- forward_scheme === 'https' ? 'selected' : '' %>>https</option>
</select>
</div>
</div>
<div class="col-sm-5 col-md-5">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-host') %><span class="form-required">*</span></label>
<input type="text" name="forward_host" class="form-control text-monospace model" placeholder="" value="<%- forward_host %>" autocomplete="off" maxlength="50" required>
<span style="font-size: 9px;"><%- i18n('proxy-hosts', 'custom-forward-host-help') %></span>
</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 model" placeholder="80" value="<%- forward_port %>" required>
</div>
</div>
</div>
<div class="row config">
<div class="col-md-12">
<div class="form-group">
<textarea name="advanced_config" rows="8" class="form-control text-monospace model" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>
</div>
</div>
</div>
<a href="#" class="card-link location-delete">
<i class="fa fa-trash"></i> <%- i18n('locations', 'delete') %>
</a>
</div>
</div>

View File

@ -0,0 +1,54 @@
const locationItemTemplate = require('./location-item.ejs');
const Mn = require('backbone.marionette');
const App = require('../../main');
const LocationView = Mn.View.extend({
template: locationItemTemplate,
className: 'location_block',
ui: {
toggle: 'input[type="checkbox"]',
config: '.config',
delete: '.location-delete'
},
events: {
'change @ui.toggle': function(el) {
if (el.target.checked) {
this.ui.config.show();
} else {
this.ui.config.hide();
}
},
'change .model': function (e) {
const map = {};
map[e.target.name] = e.target.value;
this.model.set(map);
},
'click @ui.delete': function () {
this.model.destroy();
}
},
onRender: function() {
$(this.ui.config).hide();
},
templateContext: function() {
return {
i18n: App.i18n
}
}
});
const LocationCollectionView = Mn.CollectionView.extend({
className: 'locations_container',
childView: LocationView
});
module.exports = {
LocationCollectionView,
LocationView
}

View File

@ -0,0 +1,20 @@
<div class="card">
<div class="card-status bg-success"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('proxy-hosts', '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) { %>
<a href="#" class="btn btn-outline-success btn-sm ml-2 add-item"><%- i18n('proxy-hosts', 'add') %></a>
<% } %>
</div>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,81 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const ProxyHostModel = require('../../../models/proxy-host');
const ListView = require('./list/main');
const ErrorView = require('../../error/main');
const EmptyView = require('../../empty/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'nginx-proxy',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
help: '.help',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
events: {
'click @ui.add': function (e) {
e.preventDefault();
App.Controller.showNginxProxyForm();
},
'click @ui.help': function (e) {
e.preventDefault();
App.Controller.showHelp(App.i18n('proxy-hosts', 'help-title'), App.i18n('proxy-hosts', 'help-content'));
}
},
templateContext: {
showAddButton: App.Cache.User.canManage('proxy_hosts')
},
onRender: function () {
let view = this;
App.Api.Nginx.ProxyHosts.getAll(['owner', 'access_list', 'certificate'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {
view.showChildView('list_region', new ListView({
collection: new ProxyHostModel.Collection(response)
}));
} else {
let manage = App.Cache.User.canManage('proxy_hosts');
view.showChildView('list_region', new EmptyView({
title: App.i18n('proxy-hosts', 'empty'),
subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}),
link: manage ? App.i18n('proxy-hosts', 'add') : null,
btn_color: 'success',
permission: 'proxy_hosts',
action: function () {
App.Controller.showNginxProxyForm();
}
}));
}
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showNginxProxy();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

View File

@ -0,0 +1,23 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('redirection-hosts', 'delete') %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<%= i18n('redirection-hosts', 'delete-confirm', {domains: domain_names.join(', ')}) %>
<% if (certificate_id) { %>
<br><br>
<%- i18n('ssl', 'delete-ssl') %>
<% } %>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-danger save"><%- i18n('str', 'sure') %></button>
</div>
</div>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./delete.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
App.Api.Nginx.RedirectionHosts.delete(this.model.get('id'))
.then(() => {
App.Controller.showNginxRedirection();
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -0,0 +1,137 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('redirection-hosts', '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">
<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>
<li role="presentation" class="nav-item"><a href="#advanced" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('all-hosts', 'advanced') %></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-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('redirection-hosts', 'forward-domain') %><span class="form-required">*</span></label>
<input type="text" name="forward_domain_name" class="form-control text-monospace" placeholder="" value="<%- forward_domain_name %>" required>
</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="preserve_path" value="1"<%- preserve_path ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('redirection-hosts', 'preserve-path') %></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="block_exploits" value="1"<%- block_exploits ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'block-exploits') %></span>
</label>
</div>
</div>
</div>
</div>
<!-- SSL -->
<div role="tabpanel" class="tab-pane" id="ssl-options">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('all-hosts', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
</select>
</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' : '' %><%- 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-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="http2_support" value="1"<%- http2_support ? ' checked' : '' %><%- certificate_id ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'http2-support') %></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="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></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="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
</label>
</div>
</div>
<!-- Lets encrypt -->
<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 disabled>
</div>
</div>
<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 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>
</div>
</div>
<!-- Advanced -->
<div role="tabpanel" class="tab-pane" id="advanced">
<div class="row">
<div class="col-md-12">
<div class="form-group mb-0">
<label class="form-label"><%- i18n('all-hosts', 'advanced-config') %></label>
<textarea name="advanced_config" rows="8" class="form-control text-monospace" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,209 @@
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',
ui: {
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"]',
hsts_enabled: 'input[name="hsts_enabled"]',
hsts_subdomains: 'input[name="hsts_subdomains"]',
http2_support: 'input[name="http2_support"]',
letsencrypt: '.letsencrypt'
},
events: {
'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);
}
let enabled = id === 'new' || parseInt(id, 10) > 0;
let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
inputs
.prop('disabled', !enabled)
.parents('.form-group')
.css('opacity', enabled ? 1 : 0.5);
if (!enabled) {
inputs.prop('checked', false);
}
inputs.trigger('change');
},
'change @ui.ssl_forced': function () {
let checked = this.ui.ssl_forced.prop('checked');
this.ui.hsts_enabled
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_enabled.prop('checked', false);
}
this.ui.hsts_enabled.trigger('change');
},
'change @ui.hsts_enabled': function () {
let checked = this.ui.hsts_enabled.prop('checked');
this.ui.hsts_subdomains
.prop('disabled', !checked)
.parents('.form-group')
.css('opacity', checked ? 1 : 0.5);
if (!checked) {
this.ui.hsts_subdomains.prop('checked', false);
}
},
'click @ui.save': function (e) {
e.preventDefault();
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
// Manipulate
data.block_exploits = !!data.block_exploits;
data.preserve_path = !!data.preserve_path;
data.http2_support = !!data.http2_support;
data.hsts_enabled = !!data.hsts_enabled;
data.hsts_subdomains = !!data.hsts_subdomains;
data.ssl_forced = !!data.ssl_forced;
if (typeof data.domain_names === 'string' && data.domain_names) {
data.domain_names = data.domain_names.split(',');
}
// 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, 10);
}
let method = App.Api.Nginx.RedirectionHosts.create;
let is_new = true;
if (this.model.get('id')) {
// edit
is_new = false;
method = App.Api.Nginx.RedirectionHosts.update;
data.id = this.model.get('id');
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
method(data)
.then(result => {
view.model.set(result);
App.UI.closeModal(function () {
if (is_new) {
App.Controller.showNginxRedirection();
}
});
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
templateContext: {
getLetsencryptEmail: function () {
return App.Cache.User.get('email');
}
},
onRender: function () {
let view = this;
// Domain names
this.ui.domain_names.selectize({
delimiter: ',',
persist: false,
maxOptions: 15,
create: function (input) {
return {
value: input,
text: input
};
},
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) {
if (typeof options.model === 'undefined' || !options.model) {
this.model = new RedirectionHostModel.Model();
}
}
});

View File

@ -0,0 +1,56 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div>
<% domain_names.map(function(host) {
if (host.indexOf('*') === -1) {
%>
<span class="tag host-link hover-yellow" rel="http<%- certificate_id ? 's' : '' %>://<%- host %>"><%- host %></span>
<%
} else {
%>
<span class="tag"><%- host %></span>
<%
}
});
%>
</div>
<div class="small text-muted">
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
</div>
</td>
<td>
<div class="text-monospace"><%- forward_domain_name %></div>
</td>
<td>
<div><%- certificate ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %></div>
</td>
<td>
<%
var o = isOnline();
if (!enabled) { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'disabled') %>
<% } else if (o === true) { %>
<span class="status-icon bg-success"></span> <%- i18n('str', 'online') %>
<% } else if (o === false) { %>
<span title="<%- getOfflineError() %>"><span class="status-icon bg-danger"></span> <%- i18n('str', 'offline') %></span>
<% } else { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'unknown') %>
<% } %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
<a href="#" class="able dropdown-item"><i class="dropdown-icon fe fe-power"></i> <%- i18n('str', enabled ? 'disable' : 'enable') %></a>
<div class="dropdown-divider"></div>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
</div>
</div>
</td>
<% } %>

View File

@ -0,0 +1,61 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
able: 'a.able',
edit: 'a.edit',
delete: 'a.delete',
host_link: '.host-link'
},
events: {
'click @ui.able': function (e) {
e.preventDefault();
let id = this.model.get('id');
App.Api.Nginx.RedirectionHosts[this.model.get('enabled') ? 'disable' : 'enable'](id)
.then(() => {
return App.Api.Nginx.RedirectionHosts.get(id)
.then(row => {
this.model.set(row);
});
});
},
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showNginxRedirectionForm(this.model);
},
'click @ui.delete': function (e) {
e.preventDefault();
App.Controller.showNginxRedirectionDeleteConfirm(this.model);
},
'click @ui.host_link': function (e) {
e.preventDefault();
let win = window.open($(e.currentTarget).attr('rel'), '_blank');
win.focus();
}
},
templateContext: {
canManage: App.Cache.User.canManage('redirection_hosts'),
isOnline: function () {
return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online;
},
getOfflineError: function () {
return this.meta.nginx_err || '';
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,13 @@
<thead>
<th width="30">&nbsp;</th>
<th><%- i18n('str', 'source') %></th>
<th><%- i18n('str', 'destination') %></th>
<th><%- i18n('str', 'ssl') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
templateContext: {
canManage: App.Cache.User.canManage('redirection_hosts')
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,20 @@
<div class="card">
<div class="card-status bg-yellow"></div>
<div class="card-header">
<h3 class="card-title">Redirection Hosts</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) { %>
<a href="#" class="btn btn-outline-yellow btn-sm ml-2 add-item">Add Redirection Host</a>
<% } %>
</div>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,81 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const RedirectionHostModel = require('../../../models/redirection-host');
const ListView = require('./list/main');
const ErrorView = require('../../error/main');
const EmptyView = require('../../empty/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'nginx-redirection',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
help: '.help',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
events: {
'click @ui.add': function (e) {
e.preventDefault();
App.Controller.showNginxRedirectionForm();
},
'click @ui.help': function (e) {
e.preventDefault();
App.Controller.showHelp(App.i18n('redirection-hosts', 'help-title'), App.i18n('redirection-hosts', 'help-content'));
}
},
templateContext: {
showAddButton: App.Cache.User.canManage('proxy_hosts')
},
onRender: function () {
let view = this;
App.Api.Nginx.RedirectionHosts.getAll(['owner', 'certificate'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {
view.showChildView('list_region', new ListView({
collection: new RedirectionHostModel.Collection(response)
}));
} else {
let manage = App.Cache.User.canManage('redirection_hosts');
view.showChildView('list_region', new EmptyView({
title: App.i18n('redirection-hosts', 'empty'),
subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}),
link: manage ? App.i18n('redirection-hosts', 'add') : null,
btn_color: 'yellow',
permission: 'redirection_hosts',
action: function () {
App.Controller.showNginxRedirectionForm();
}
}));
}
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showNginxRedirection();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

View File

@ -0,0 +1,19 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('streams', 'delete') %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<%= i18n('streams', 'delete-confirm') %>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-danger save"><%- i18n('str', 'sure') %></button>
</div>
</div>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./delete.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
App.Api.Nginx.Streams.delete(this.model.get('id'))
.then(() => {
App.Controller.showNginxStream();
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -0,0 +1,55 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label>
<input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" value="<%- incoming_port %>" required>
</div>
</div>
<div class="col-sm-8 col-md-8">
<div class="form-group">
<label class="form-label"><%- i18n('streams', '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('streams', 'forwarding-port') %> <span class="form-required">*</span></label>
<input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" value="<%- forwarding_port %>" required>
</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="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></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="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span>
</label>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,91 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const StreamModel = require('../../../models/stream');
const template = require('./form.ejs');
require('jquery-serializejson');
require('jquery-mask-plugin');
require('selectize');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
forward_ip: 'input[name="forward_ip"]',
type_error: '.forward-type-error',
buttons: '.modal-footer button',
switches: '.custom-switch-input',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
'change @ui.switches': function () {
this.ui.type_error.hide();
},
'click @ui.save': function (e) {
e.preventDefault();
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
if (!data.tcp_forwarding && !data.udp_forwarding) {
this.ui.type_error.show();
return;
}
// Manipulate
data.incoming_port = parseInt(data.incoming_port, 10);
data.forwarding_port = parseInt(data.forwarding_port, 10);
data.tcp_forwarding = !!data.tcp_forwarding;
data.udp_forwarding = !!data.udp_forwarding;
let method = App.Api.Nginx.Streams.create;
let is_new = true;
if (this.model.get('id')) {
// edit
is_new = false;
method = App.Api.Nginx.Streams.update;
data.id = this.model.get('id');
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
method(data)
.then(result => {
view.model.set(result);
App.UI.closeModal(function () {
if (is_new) {
App.Controller.showNginxStream();
}
});
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
onRender: function () {
this.ui.forward_ip.mask('099.099.099.099', {
clearIfNotMatch: true,
placeholder: '000.000.000.000'
});
},
initialize: function (options) {
if (typeof options.model === 'undefined' || !options.model) {
this.model = new StreamModel.Model();
}
}
});

View File

@ -0,0 +1,52 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div class="text-monospace">
<%- incoming_port %>
</div>
<div class="small text-muted">
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
</div>
</td>
<td>
<div class="text-monospace"><%- forward_ip %>:<%- forwarding_port %></div>
</td>
<td>
<div>
<% if (tcp_forwarding) { %>
<span class="tag"><%- i18n('streams', 'tcp') %></span>
<% }
if (udp_forwarding) { %>
<span class="tag"><%- i18n('streams', 'udp') %></span>
<% } %>
</div>
</td>
<td>
<%
var o = isOnline();
if (!enabled) { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'disabled') %>
<% } else if (o === true) { %>
<span class="status-icon bg-success"></span> <%- i18n('str', 'online') %>
<% } else if (o === false) { %>
<span title="<%- getOfflineError() %>"><span class="status-icon bg-danger"></span> <%- i18n('str', 'offline') %></span>
<% } else { %>
<span class="status-icon bg-warning"></span> <%- i18n('str', 'unknown') %>
<% } %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
<a href="#" class="able dropdown-item"><i class="dropdown-icon fe fe-power"></i> <%- i18n('str', enabled ? 'disable' : 'enable') %></a>
<div class="dropdown-divider"></div>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
</div>
</div>
</td>
<% } %>

View File

@ -0,0 +1,54 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
able: 'a.able',
edit: 'a.edit',
delete: 'a.delete'
},
events: {
'click @ui.able': function (e) {
e.preventDefault();
let id = this.model.get('id');
App.Api.Nginx.Streams[this.model.get('enabled') ? 'disable' : 'enable'](id)
.then(() => {
return App.Api.Nginx.Streams.get(id)
.then(row => {
this.model.set(row);
});
});
},
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showNginxStreamForm(this.model);
},
'click @ui.delete': function (e) {
e.preventDefault();
App.Controller.showNginxStreamDeleteConfirm(this.model);
}
},
templateContext: {
canManage: App.Cache.User.canManage('streams'),
isOnline: function () {
return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online;
},
getOfflineError: function () {
return this.meta.nginx_err || '';
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,13 @@
<thead>
<th width="30">&nbsp;</th>
<th><%- i18n('streams', 'incoming-port') %></th>
<th><%- i18n('str', 'destination') %></th>
<th><%- i18n('streams', 'protocol') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,32 @@
const Mn = require('backbone.marionette');
const App = require('../../../main');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
templateContext: {
canManage: App.Cache.User.canManage('streams')
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,20 @@
<div class="card">
<div class="card-status bg-blue"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('streams', '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) { %>
<a href="#" class="btn btn-outline-blue btn-sm ml-2 add-item"><%- i18n('streams', 'add') %></a>
<% } %>
</div>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,81 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const StreamModel = require('../../../models/stream');
const ListView = require('./list/main');
const ErrorView = require('../../error/main');
const EmptyView = require('../../empty/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'nginx-stream',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
help: '.help',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
events: {
'click @ui.add': function (e) {
e.preventDefault();
App.Controller.showNginxStreamForm();
},
'click @ui.help': function (e) {
e.preventDefault();
App.Controller.showHelp(App.i18n('streams', 'help-title'), App.i18n('streams', 'help-content'));
}
},
templateContext: {
showAddButton: App.Cache.User.canManage('streams')
},
onRender: function () {
let view = this;
App.Api.Nginx.Streams.getAll(['owner'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {
view.showChildView('list_region', new ListView({
collection: new StreamModel.Collection(response)
}));
} else {
let manage = App.Cache.User.canManage('streams');
view.showChildView('list_region', new EmptyView({
title: App.i18n('streams', 'empty'),
subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}),
link: manage ? App.i18n('streams', 'add') : null,
btn_color: 'blue',
permission: 'streams',
action: function () {
App.Controller.showNginxStreamForm();
}
}));
}
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showNginxStream();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

19
frontend/js/app/router.js Normal file
View File

@ -0,0 +1,19 @@
const AppRouter = require('marionette.approuter');
const Controller = require('./controller');
module.exports = AppRouter.default.extend({
controller: Controller,
appRoutes: {
users: 'showUsers',
logout: 'logout',
'nginx/proxy': 'showNginxProxy',
'nginx/redirection': 'showNginxRedirection',
'nginx/404': 'showNginxDead',
'nginx/stream': 'showNginxStream',
'nginx/access': 'showNginxAccess',
'nginx/certificates': 'showNginxCertificates',
'audit-log': 'showAuditLog',
'settings': 'showSettings',
'*default': 'showDashboard'
}
});

View File

@ -0,0 +1,53 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('settings', id) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<div class="form-label"><%- description %></div>
<div class="custom-controls-stacked">
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="congratulations" type="radio" required <%- value === 'congratulations' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-congratulations') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="404" type="radio" required <%- value === '404' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-404') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="redirect" type="radio" required <%- value === 'redirect' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-redirect') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="html" type="radio" required <%- value === 'html' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-html') %></div>
</label>
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 option-item option-redirect">
<div class="form-group">
<div class="form-label">Redirect to</div>
<input class="form-control redirect-input" name="meta[redirect]" placeholder="https://" type="url" value="<%- meta && typeof meta.redirect !== 'undefined' ? meta.redirect : '' %>">
</div>
</div>
<div class="col-sm-12 col-md-12 option-item option-html">
<div class="form-group">
<div class="form-label">HTML Content</div>
<textarea class="form-control text-monospace html-content" name="meta[html]" rows="6" placeholder="<!-- Enter your HTML here -->"><%- meta && typeof meta.html !== 'undefined' ? meta.html : '' %></textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,69 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./main.ejs');
require('jquery-serializejson');
require('selectize');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
options: '.option-item',
value: 'input[name="value"]',
redirect: '.redirect-input',
html: '.html-content'
},
events: {
'change @ui.value': function (e) {
let val = this.ui.value.filter(':checked').val();
this.ui.options.hide();
this.ui.options.filter('.option-' + val).show();
},
'click @ui.save': function (e) {
e.preventDefault();
let val = this.ui.value.filter(':checked').val();
// Clear redirect field before validation
if (val !== 'redirect') {
this.ui.redirect.val('').attr('required', false);
} else {
this.ui.redirect.attr('required', true);
}
this.ui.html.attr('required', val === 'html');
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
data.id = this.model.get('id');
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
App.Api.Settings.update(data)
.then(result => {
view.model.set(result);
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
onRender: function () {
this.ui.value.trigger('change');
}
});

View File

@ -0,0 +1,21 @@
<td>
<div><%- name %></div>
<div class="small text-muted">
<%- description %>
</div>
</td>
<td>
<div>
<% if (id === 'default-site') { %>
<%- i18n('settings', 'default-site-' + value) %>
<% } %>
</div>
</td>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
</div>
</div>
</td>

View File

@ -0,0 +1,23 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
edit: 'a.edit'
},
events: {
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showSettingForm(this.model);
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,8 @@
<thead>
<th><%- i18n('str', 'name') %></th>
<th><%- i18n('str', 'value') %></th>
<th>&nbsp;</th>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,27 @@
const Mn = require('backbone.marionette');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,14 @@
<div class="card">
<div class="card-status bg-teal"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('settings', 'title') %></h3>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,48 @@
const Mn = require('backbone.marionette');
const App = require('../main');
const SettingModel = require('../../models/setting');
const ListView = require('./list/main');
const ErrorView = require('../error/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'settings',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
onRender: function () {
let view = this;
App.Api.Settings.getAll()
.then(response => {
if (!view.isDestroyed() && response && response.length) {
view.showChildView('list_region', new ListView({
collection: new SettingModel.Collection(response)
}));
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showSettings();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

126
frontend/js/app/tokens.js Normal file
View File

@ -0,0 +1,126 @@
const STORAGE_NAME = 'nginx-proxy-manager-tokens';
/**
* @returns {Array}
*/
const getStorageTokens = function () {
let json = window.localStorage.getItem(STORAGE_NAME);
if (json) {
try {
return JSON.parse(json);
} catch (err) {
return [];
}
}
return [];
};
/**
* @param {Array} tokens
*/
const setStorageTokens = function (tokens) {
window.localStorage.setItem(STORAGE_NAME, JSON.stringify(tokens));
};
const Tokens = {
/**
* @returns {Number}
*/
getTokenCount: () => {
return getStorageTokens().length;
},
/**
* @returns {Object} t,n
*/
getTopToken: () => {
let tokens = getStorageTokens();
if (tokens && tokens.length) {
return tokens[0];
}
return null;
},
/**
* @returns {String}
*/
getNextTokenName: () => {
let tokens = getStorageTokens();
if (tokens && tokens.length > 1 && typeof tokens[1] !== 'undefined' && typeof tokens[1].n !== 'undefined') {
return tokens[1].n;
}
return null;
},
/**
*
* @param {String} token
* @param {String} [name]
* @returns {Number}
*/
addToken: (token, name) => {
// Get top token and if it's the same, ignore this call
let top = Tokens.getTopToken();
if (!top || top.t !== token) {
let tokens = getStorageTokens();
tokens.unshift({t: token, n: name || null});
setStorageTokens(tokens);
}
return Tokens.getTokenCount();
},
/**
* @param {String} token
* @returns {Boolean}
*/
setCurrentToken: token => {
let tokens = getStorageTokens();
if (tokens.length) {
tokens[0].t = token;
setStorageTokens(tokens);
return true;
}
return false;
},
/**
* @param {String} name
* @returns {Boolean}
*/
setCurrentName: name => {
let tokens = getStorageTokens();
if (tokens.length) {
tokens[0].n = name;
setStorageTokens(tokens);
return true;
}
return false;
},
/**
* @returns {Number}
*/
dropTopToken: () => {
let tokens = getStorageTokens();
tokens.shift();
setStorageTokens(tokens);
return tokens.length;
},
/**
*
*/
clearTokens: () => {
window.localStorage.removeItem(STORAGE_NAME);
}
};
module.exports = Tokens;

View File

@ -0,0 +1,16 @@
<div class="row align-items-center flex-row-reverse">
<div class="col-auto ml-auto">
<div class="row align-items-center">
<div class="col-auto">
<ul class="list-inline list-inline-dots mb-0">
<li class="list-inline-item"><a href="https://github.com/jc21/nginx-proxy-manager?utm_source=nginx-proxy-manager"><%- i18n('footer', 'fork-me') %></a></li>
</ul>
</div>
</div>
</div>
<div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
<%- i18n('main', 'version', {version: getVersion()}) %>
<%= i18n('footer', 'copy', {url: 'https://jc21.com?utm_source=nginx-proxy-manager'}) %>
<%= i18n('footer', 'theme', {url: 'https://tabler.github.io/?utm_source=nginx-proxy-manager'}) %>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More