Merge branch 'develop' of https://github.com/jc21/nginx-proxy-manager into feature/custom-locations

This commit is contained in:
kolbii 2019-03-04 19:56:01 +01:00
commit b4fd49f3cb
34 changed files with 920 additions and 18 deletions

View file

@ -143,3 +143,23 @@ Password: changeme
``` ```
Immediately after logging in with this default user you will be asked to modify your details and change your password. Immediately after logging in with this default user you will be asked to modify your details and change your password.
### Advanced Options
#### X-FRAME-OPTIONS Header
You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header
value by specifying it as a Docker environment variable. The default if not specified is `deny`.
```yml
...
environment:
X_FRAME_OPTIONS: "sameorigin"
...
```
```
... -e "X_FRAME_OPTIONS=sameorigin" ...
```

View file

@ -22,10 +22,10 @@ server {
} }
} }
# Default 80 Host, which shows a "You are not configured" page # "You are not configured" page, which is the default if another default doesn't exist
server { server {
listen 80 default; listen 80;
server_name localhost; server_name localhost-nginx-proxy-manager;
access_log /data/logs/default.log proxy; access_log /data/logs/default.log proxy;
@ -38,9 +38,9 @@ server {
} }
} }
# Default 443 Host # First 443 Host, which is the default if another default doesn't exist
server { server {
listen 443 ssl default; listen 443 ssl;
server_name localhost; server_name localhost;
access_log /data/logs/default.log proxy; access_log /data/logs/default.log proxy;

View file

@ -70,6 +70,7 @@ http {
# Files generated by NPM # Files generated by NPM
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
include /data/nginx/default_host/*.conf;
include /data/nginx/proxy_host/*.conf; include /data/nginx/proxy_host/*.conf;
include /data/nginx/redirection_host/*.conf; include /data/nginx/redirection_host/*.conf;
include /data/nginx/dead_host/*.conf; include /data/nginx/dead_host/*.conf;

View file

@ -7,6 +7,8 @@ mkdir -p /tmp/nginx/body \
/data/custom_ssl \ /data/custom_ssl \
/data/logs \ /data/logs \
/data/access \ /data/access \
/data/nginx/default_host \
/data/nginx/default_www \
/data/nginx/proxy_host \ /data/nginx/proxy_host \
/data/nginx/redirection_host \ /data/nginx/redirection_host \
/data/nginx/stream \ /data/nginx/stream \

View file

@ -40,11 +40,17 @@ app.use(require('./lib/express/cors'));
// General security/cache related headers + server header // General security/cache related headers + server header
app.use(function (req, res, next) { app.use(function (req, res, next) {
let x_frame_options = 'DENY';
if (typeof process.env.X_FRAME_OPTIONS !== 'undefined' && process.env.X_FRAME_OPTIONS) {
x_frame_options = process.env.X_FRAME_OPTIONS;
}
res.set({ res.set({
'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload', 'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
'X-XSS-Protection': '0', 'X-XSS-Protection': '0',
'X-Content-Type-Options': 'nosniff', 'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY', 'X-Frame-Options': x_frame_options,
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
Pragma: 'no-cache', Pragma: 'no-cache',
Expires: 0 Expires: 0

View file

@ -1,7 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
const logger = require('./logger').global; const logger = require('./logger').global;
function appStart () { function appStart () {

View file

@ -17,9 +17,9 @@ const internalNginx = {
* - IF BAD: update the meta with offline status and remove the config entirely * - IF BAD: update the meta with offline status and remove the config entirely
* - then reload nginx * - then reload nginx
* *
* @param {Object} model * @param {Object|String} model
* @param {String} host_type * @param {String} host_type
* @param {Object} host * @param {Object} host
* @returns {Promise} * @returns {Promise}
*/ */
configure: (model, host_type, host) => { configure: (model, host_type, host) => {
@ -122,6 +122,11 @@ const internalNginx = {
*/ */
getConfigName: (host_type, host_id) => { getConfigName: (host_type, host_id) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_'); host_type = host_type.replace(new RegExp('-', 'g'), '_');
if (host_type === 'default') {
return '/data/nginx/default_host/site.conf';
}
return '/data/nginx/' + host_type + '/' + host_id + '.conf'; return '/data/nginx/' + host_type + '/' + host_id + '.conf';
}, },
@ -185,9 +190,11 @@ const internalNginx = {
let origLocations; let origLocations;
// Manipulate the data a bit before sending it to the template // Manipulate the data a bit before sending it to the template
host.use_default_location = true; if (host_type !== 'default') {
if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { host.use_default_location = true;
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
}
} }
if (host.locations) { if (host.locations) {
@ -306,7 +313,7 @@ const internalNginx = {
/** /**
* @param {String} host_type * @param {String} host_type
* @param {Object} host * @param {Object} [host]
* @param {Boolean} [throw_errors] * @param {Boolean} [throw_errors]
* @returns {Promise} * @returns {Promise}
*/ */
@ -315,7 +322,7 @@ const internalNginx = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
let config_file = internalNginx.getConfigName(host_type, host.id); let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id);
if (debug_mode) { if (debug_mode) {
logger.warn('Deleting nginx config: ' + config_file); logger.warn('Deleting nginx config: ' + config_file);

View file

@ -108,7 +108,7 @@ const internalProxyHost = {
*/ */
update: (access, data) => { update: (access, data) => {
let create_certificate = data.certificate_id === 'new'; let create_certificate = data.certificate_id === 'new';
console.log('PH UPDATE:', data);
if (create_certificate) { if (create_certificate) {
delete data.certificate_id; delete data.certificate_id;
} }

View file

@ -0,0 +1,133 @@
const fs = require('fs');
const error = require('../lib/error');
const settingModel = require('../models/setting');
const internalNginx = require('./nginx');
const internalSetting = {
/**
* @param {Access} access
* @param {Object} data
* @param {String} data.id
* @return {Promise}
*/
update: (access, data) => {
return access.can('settings:update', data.id)
.then(access_data => {
return internalSetting.get(access, {id: data.id});
})
.then(row => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
return settingModel
.query()
.where({id: data.id})
.patch(data);
})
.then(() => {
return internalSetting.get(access, {
id: data.id
});
})
.then(row => {
if (row.id === 'default-site') {
// write the html if we need to
if (row.value === 'html') {
fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'});
}
// Configure nginx
return internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.generateConfig('default', row);
})
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
})
.then(() => {
return row;
})
.catch((err) => {
internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
})
.then(() => {
// I'm being slack here I know..
throw new error.ValidationError('Could not reconfigure Nginx. Please check logs.');
})
});
} else {
return row;
}
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {String} data.id
* @return {Promise}
*/
get: (access, data) => {
return access.can('settings:get', data.id)
.then(() => {
return settingModel
.query()
.where('id', data.id)
.first();
})
.then(row => {
if (row) {
return row;
} else {
throw new error.ItemNotFoundError(data.id);
}
});
},
/**
* This will only count the settings
*
* @param {Access} access
* @returns {*}
*/
getCount: (access) => {
return access.can('settings:list')
.then(() => {
return settingModel
.query()
.count('id as count')
.first();
})
.then(row => {
return parseInt(row.count, 10);
});
},
/**
* All settings
*
* @param {Access} access
* @returns {Promise}
*/
getAll: (access) => {
return access.can('settings:list')
.then(() => {
return settingModel
.query()
.orderBy('description', 'ASC');
});
}
};
module.exports = internalSetting;

View file

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View file

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View file

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View file

@ -0,0 +1,54 @@
const migrate_name = 'settings';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.createTable('setting', table => {
table.string('id').notNull().primary();
table.string('name', 100).notNull();
table.string('description', 255).notNull();
table.string('value', 255).notNull();
table.json('meta').notNull();
})
.then(() => {
logger.info('[' + migrate_name + '] setting Table created');
// TODO: add settings
let settingModel = require('../models/setting');
return settingModel
.query()
.insert({
id: 'default-site',
name: 'Default Site',
description: 'What to show when Nginx is hit with an unknown Host',
value: 'congratulations',
meta: {}
});
})
.then(() => {
logger.info('[' + migrate_name + '] Default settings added');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
return Promise.resolve(true);
};

View file

@ -0,0 +1,30 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
Model.knex(db);
class Setting extends Model {
$beforeInsert () {
// Default for meta
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
static get name () {
return 'Setting';
}
static get tableName () {
return 'setting';
}
static get jsonAttributes () {
return ['meta'];
}
}
module.exports = Setting;

View file

@ -31,6 +31,7 @@ router.use('/tokens', require('./tokens'));
router.use('/users', require('./users')); router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log')); router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports')); router.use('/reports', require('./reports'));
router.use('/settings', require('./settings'));
router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts'));
router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts'));
router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); router.use('/nginx/dead-hosts', require('./nginx/dead_hosts'));

View file

@ -0,0 +1,96 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalSetting = require('../../internal/setting');
const apiValidator = require('../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/settings
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/settings
*
* Retrieve all settings
*/
.get((req, res, next) => {
internalSetting.getAll(res.locals.access)
.then(rows => {
res.status(200)
.send(rows);
})
.catch(next);
});
/**
* Specific setting
*
* /api/settings/something
*/
router
.route('/:setting_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /settings/something
*
* Retrieve a specific setting
*/
.get((req, res, next) => {
validator({
required: ['setting_id'],
additionalProperties: false,
properties: {
setting_id: {
$ref: 'definitions#/definitions/setting_id'
}
}
}, {
setting_id: req.params.setting_id
})
.then(data => {
return internalSetting.get(res.locals.access, {
id: data.setting_id
});
})
.then(row => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/settings/something
*
* Update and existing setting
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/settings#/links/1/schema'}, req.body)
.then(payload => {
payload.id = req.params.setting_id;
return internalSetting.update(res.locals.access, payload);
})
.then(result => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View file

@ -9,6 +9,13 @@
"type": "integer", "type": "integer",
"minimum": 1 "minimum": 1
}, },
"setting_id": {
"description": "Unique identifier for a Setting",
"example": "default-site",
"readOnly": true,
"type": "string",
"minLength": 2
},
"token": { "token": {
"type": "string", "type": "string",
"minLength": 10 "minLength": 10

View file

@ -0,0 +1,99 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "endpoints/settings",
"title": "Settings",
"description": "Endpoints relating to Settings",
"stability": "stable",
"type": "object",
"definitions": {
"id": {
"$ref": "../definitions.json#/definitions/setting_id"
},
"name": {
"description": "Name",
"example": "Default Site",
"type": "string",
"minLength": 2,
"maxLength": 100
},
"description": {
"description": "Description",
"example": "Default Site",
"type": "string",
"minLength": 2,
"maxLength": 255
},
"value": {
"description": "Value",
"example": "404",
"type": "string",
"maxLength": 255
},
"meta": {
"type": "object"
}
},
"links": [
{
"title": "List",
"description": "Returns a list of Settings",
"href": "/settings",
"access": "private",
"method": "GET",
"rel": "self",
"http_header": {
"$ref": "../examples.json#/definitions/auth_header"
},
"targetSchema": {
"type": "array",
"items": {
"$ref": "#/properties"
}
}
},
{
"title": "Update",
"description": "Updates a existing Setting",
"href": "/settings/{definitions.identity.example}",
"access": "private",
"method": "PUT",
"rel": "update",
"http_header": {
"$ref": "../examples.json#/definitions/auth_header"
},
"schema": {
"type": "object",
"properties": {
"value": {
"$ref": "#/definitions/value"
},
"meta": {
"$ref": "#/definitions/meta"
}
}
},
"targetSchema": {
"properties": {
"$ref": "#/properties"
}
}
}
],
"properties": {
"id": {
"$ref": "#/definitions/id"
},
"name": {
"$ref": "#/definitions/description"
},
"description": {
"$ref": "#/definitions/description"
},
"value": {
"$ref": "#/definitions/value"
},
"meta": {
"$ref": "#/definitions/meta"
}
}
}

View file

@ -34,6 +34,9 @@
}, },
"access-lists": { "access-lists": {
"$ref": "endpoints/access-lists.json" "$ref": "endpoints/access-lists.json"
},
"settings": {
"$ref": "endpoints/settings.json"
} }
} }
} }

View file

@ -0,0 +1,32 @@
# ------------------------------------------------------------
# Default Site
# ------------------------------------------------------------
{% if value == "congratulations" %}
# Skipping output, congratulations page configration is baked in.
{%- else %}
server {
listen 80 default;
server_name default-host.localhost;
access_log /data/logs/default_host.log combined;
{% include "_exploits.conf" %}
{%- if value == "404" %}
location / {
return 404;
}
{% endif %}
{%- if value == "redirect" %}
location / {
return 301 {{ meta.redirect }};
}
{%- endif %}
{%- if value == "html" %}
root /data/nginx/default_www;
location / {
try_files $uri /index.html ={{ meta.http_code }};
}
{%- endif %}
}
{% endif %}

View file

@ -662,5 +662,34 @@ module.exports = {
getHostStats: function () { getHostStats: function () {
return fetch('get', 'reports/hosts'); 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

@ -383,6 +383,36 @@ module.exports = {
} }
}, },
/**
* 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
*/ */

View file

@ -15,6 +15,7 @@ module.exports = AppRouter.default.extend({
'nginx/access': 'showNginxAccess', 'nginx/access': 'showNginxAccess',
'nginx/certificates': 'showNginxCertificates', 'nginx/certificates': 'showNginxCertificates',
'audit-log': 'showAuditLog', 'audit-log': 'showAuditLog',
'settings': 'showSettings',
'*default': 'showDashboard' '*default': 'showDashboard'
} }
}); });

View file

@ -0,0 +1,77 @@
<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">
<label class="form-label">HTTP Status Code</label>
<%
var code = meta && typeof meta.http_code !== 'undefined' ? parseInt(meta.http_code, 10) : 200;
var codes = [
[200, 'OK'],
[204, 'No Content'],
[400, 'Bad Request'],
[401, 'Unauthorized'],
[403, 'Forbidden'],
[404, 'Not Found'],
[418, 'I\'m a Teapot'],
[500, 'Internal Server Error'],
[501, 'Not Implemented'],
[502, 'Bad Gateway'],
[503, 'Service Unavailable']
];
%>
<select class="custom-select" name="meta[http_code]">
<% codes.map(function(item) { %>
<option value="<%- item[0] %>"<%- code === item[0] ? ' selected' : '' %>><%- item[0] %> - <%- item[1] %></option>
<% }); %>
</select>
</div>
<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,71 @@
'use strict';
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,25 @@
'use strict';
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,29 @@
'use strict';
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 text-nowrap 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,50 @@
'use strict';
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');
});
}
});

View file

@ -42,6 +42,9 @@
<li class="nav-item"> <li class="nav-item">
<a href="/audit-log" class="nav-link"><i class="fe fe-book-open"></i> <%- i18n('audit-log', 'title') %></a> <a href="/audit-log" class="nav-link"><i class="fe fe-book-open"></i> <%- i18n('audit-log', 'title') %></a>
</li> </li>
<li class="nav-item">
<a href="/settings" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('settings', 'title') %></a>
</li>
<% } %> <% } %>
</ul> </ul>
</div> </div>

View file

@ -31,7 +31,8 @@
"online": "Online", "online": "Online",
"offline": "Offline", "offline": "Offline",
"unknown": "Unknown", "unknown": "Unknown",
"expires": "Expires" "expires": "Expires",
"value": "Value"
}, },
"login": { "login": {
"title": "Login to your account" "title": "Login to your account"
@ -229,6 +230,14 @@
"meta-title": "Details for Event", "meta-title": "Details for Event",
"view-meta": "View Details", "view-meta": "View Details",
"date": "Date" "date": "Date"
},
"settings": {
"title": "Settings",
"default-site": "Default Site",
"default-site-congratulations": "Congratulations Page",
"default-site-404": "404 Page",
"default-site-html": "Custom Page",
"default-site-redirect": "Redirect"
} }
} }
} }

View file

@ -0,0 +1,25 @@
'use strict';
const _ = require('underscore');
const Backbone = require('backbone');
const model = Backbone.Model.extend({
idAttribute: 'id',
defaults: function () {
return {
id: undefined,
name: '',
description: '',
value: null,
meta: []
};
}
});
module.exports = {
Model: model,
Collection: Backbone.Collection.extend({
model: model
})
};