From 133d66c2fe77f6a96a401c01865cedd2cbcc4ec1 Mon Sep 17 00:00:00 2001 From: jc21 Date: Mon, 4 Mar 2019 21:19:36 +1000 Subject: [PATCH] Default Site customisation and new Settings space (#91) --- rootfs/etc/nginx/conf.d/default.conf | 10 +- rootfs/etc/nginx/nginx.conf | 1 + rootfs/etc/services.d/nginx/run | 2 + src/backend/internal/nginx.js | 23 +-- src/backend/internal/proxy-host.js | 2 +- src/backend/internal/setting.js | 133 ++++++++++++++++++ src/backend/lib/access/settings-get.json | 7 + src/backend/lib/access/settings-list.json | 7 + src/backend/lib/access/settings-update.json | 7 + .../migrations/20190227065017_settings.js | 54 +++++++ src/backend/models/setting.js | 30 ++++ src/backend/routes/api/main.js | 1 + src/backend/routes/api/settings.js | 96 +++++++++++++ src/backend/schema/definitions.json | 7 + src/backend/schema/endpoints/settings.json | 99 +++++++++++++ src/backend/schema/index.json | 3 + src/backend/templates/default.conf | 32 +++++ src/frontend/js/app/api.js | 29 ++++ src/frontend/js/app/controller.js | 30 ++++ src/frontend/js/app/router.js | 1 + .../js/app/settings/default-site/main.ejs | 77 ++++++++++ .../js/app/settings/default-site/main.js | 71 ++++++++++ src/frontend/js/app/settings/list/item.ejs | 21 +++ src/frontend/js/app/settings/list/item.js | 25 ++++ src/frontend/js/app/settings/list/main.ejs | 8 ++ src/frontend/js/app/settings/list/main.js | 29 ++++ src/frontend/js/app/settings/main.ejs | 14 ++ src/frontend/js/app/settings/main.js | 50 +++++++ src/frontend/js/app/ui/menu/main.ejs | 3 + src/frontend/js/i18n/messages.json | 11 +- src/frontend/js/models/setting.js | 25 ++++ 31 files changed, 893 insertions(+), 15 deletions(-) create mode 100644 src/backend/internal/setting.js create mode 100644 src/backend/lib/access/settings-get.json create mode 100644 src/backend/lib/access/settings-list.json create mode 100644 src/backend/lib/access/settings-update.json create mode 100644 src/backend/migrations/20190227065017_settings.js create mode 100644 src/backend/models/setting.js create mode 100644 src/backend/routes/api/settings.js create mode 100644 src/backend/schema/endpoints/settings.json create mode 100644 src/backend/templates/default.conf create mode 100644 src/frontend/js/app/settings/default-site/main.ejs create mode 100644 src/frontend/js/app/settings/default-site/main.js create mode 100644 src/frontend/js/app/settings/list/item.ejs create mode 100644 src/frontend/js/app/settings/list/item.js create mode 100644 src/frontend/js/app/settings/list/main.ejs create mode 100644 src/frontend/js/app/settings/list/main.js create mode 100644 src/frontend/js/app/settings/main.ejs create mode 100644 src/frontend/js/app/settings/main.js create mode 100644 src/frontend/js/models/setting.js diff --git a/rootfs/etc/nginx/conf.d/default.conf b/rootfs/etc/nginx/conf.d/default.conf index 490e286..2530ec2 100644 --- a/rootfs/etc/nginx/conf.d/default.conf +++ b/rootfs/etc/nginx/conf.d/default.conf @@ -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 { - listen 80 default; - server_name localhost; + listen 80; + server_name localhost-nginx-proxy-manager; 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 { - listen 443 ssl default; + listen 443 ssl; server_name localhost; access_log /data/logs/default.log proxy; diff --git a/rootfs/etc/nginx/nginx.conf b/rootfs/etc/nginx/nginx.conf index ad51c87..7d06873 100644 --- a/rootfs/etc/nginx/nginx.conf +++ b/rootfs/etc/nginx/nginx.conf @@ -70,6 +70,7 @@ http { # Files generated by NPM include /etc/nginx/conf.d/*.conf; + include /data/nginx/default_host/*.conf; include /data/nginx/proxy_host/*.conf; include /data/nginx/redirection_host/*.conf; include /data/nginx/dead_host/*.conf; diff --git a/rootfs/etc/services.d/nginx/run b/rootfs/etc/services.d/nginx/run index c7b6181..f6b59fd 100755 --- a/rootfs/etc/services.d/nginx/run +++ b/rootfs/etc/services.d/nginx/run @@ -7,6 +7,8 @@ mkdir -p /tmp/nginx/body \ /data/custom_ssl \ /data/logs \ /data/access \ + /data/nginx/default_host \ + /data/nginx/default_www \ /data/nginx/proxy_host \ /data/nginx/redirection_host \ /data/nginx/stream \ diff --git a/src/backend/internal/nginx.js b/src/backend/internal/nginx.js index fd8fe16..1e99299 100644 --- a/src/backend/internal/nginx.js +++ b/src/backend/internal/nginx.js @@ -17,9 +17,9 @@ const internalNginx = { * - IF BAD: update the meta with offline status and remove the config entirely * - then reload nginx * - * @param {Object} model - * @param {String} host_type - * @param {Object} host + * @param {Object|String} model + * @param {String} host_type + * @param {Object} host * @returns {Promise} */ configure: (model, host_type, host) => { @@ -122,6 +122,11 @@ const internalNginx = { */ getConfigName: (host_type, host_id) => { 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'; }, @@ -153,9 +158,11 @@ const internalNginx = { } // Manipulate the data a bit before sending it to the template - host.use_default_location = true; - if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { - host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); + if (host_type !== 'default') { + host.use_default_location = true; + if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { + host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); + } } renderEngine @@ -260,7 +267,7 @@ const internalNginx = { /** * @param {String} host_type - * @param {Object} host + * @param {Object} [host] * @param {Boolean} [throw_errors] * @returns {Promise} */ @@ -269,7 +276,7 @@ const internalNginx = { return new Promise((resolve, reject) => { 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) { logger.warn('Deleting nginx config: ' + config_file); diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index 882d6dd..9f1d9be 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -108,7 +108,7 @@ const internalProxyHost = { */ update: (access, data) => { let create_certificate = data.certificate_id === 'new'; -console.log('PH UPDATE:', data); + if (create_certificate) { delete data.certificate_id; } diff --git a/src/backend/internal/setting.js b/src/backend/internal/setting.js new file mode 100644 index 0000000..eedb7d3 --- /dev/null +++ b/src/backend/internal/setting.js @@ -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; diff --git a/src/backend/lib/access/settings-get.json b/src/backend/lib/access/settings-get.json new file mode 100644 index 0000000..d2709fd --- /dev/null +++ b/src/backend/lib/access/settings-get.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/lib/access/settings-list.json b/src/backend/lib/access/settings-list.json new file mode 100644 index 0000000..d2709fd --- /dev/null +++ b/src/backend/lib/access/settings-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/lib/access/settings-update.json b/src/backend/lib/access/settings-update.json new file mode 100644 index 0000000..d2709fd --- /dev/null +++ b/src/backend/lib/access/settings-update.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/migrations/20190227065017_settings.js b/src/backend/migrations/20190227065017_settings.js new file mode 100644 index 0000000..6ba3653 --- /dev/null +++ b/src/backend/migrations/20190227065017_settings.js @@ -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); +}; diff --git a/src/backend/models/setting.js b/src/backend/models/setting.js new file mode 100644 index 0000000..2c3e57e --- /dev/null +++ b/src/backend/models/setting.js @@ -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; diff --git a/src/backend/routes/api/main.js b/src/backend/routes/api/main.js index cbc352e..a9c885c 100644 --- a/src/backend/routes/api/main.js +++ b/src/backend/routes/api/main.js @@ -31,6 +31,7 @@ router.use('/tokens', require('./tokens')); router.use('/users', require('./users')); router.use('/audit-log', require('./audit-log')); router.use('/reports', require('./reports')); +router.use('/settings', require('./settings')); router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); diff --git a/src/backend/routes/api/settings.js b/src/backend/routes/api/settings.js new file mode 100644 index 0000000..cc56db8 --- /dev/null +++ b/src/backend/routes/api/settings.js @@ -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; diff --git a/src/backend/schema/definitions.json b/src/backend/schema/definitions.json index eaf5595..2aa538b 100644 --- a/src/backend/schema/definitions.json +++ b/src/backend/schema/definitions.json @@ -9,6 +9,13 @@ "type": "integer", "minimum": 1 }, + "setting_id": { + "description": "Unique identifier for a Setting", + "example": "default-site", + "readOnly": true, + "type": "string", + "minLength": 2 + }, "token": { "type": "string", "minLength": 10 diff --git a/src/backend/schema/endpoints/settings.json b/src/backend/schema/endpoints/settings.json new file mode 100644 index 0000000..29e2865 --- /dev/null +++ b/src/backend/schema/endpoints/settings.json @@ -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" + } + } +} diff --git a/src/backend/schema/index.json b/src/backend/schema/index.json index b61509b..6e7d1c8 100644 --- a/src/backend/schema/index.json +++ b/src/backend/schema/index.json @@ -34,6 +34,9 @@ }, "access-lists": { "$ref": "endpoints/access-lists.json" + }, + "settings": { + "$ref": "endpoints/settings.json" } } } diff --git a/src/backend/templates/default.conf b/src/backend/templates/default.conf new file mode 100644 index 0000000..8660e5b --- /dev/null +++ b/src/backend/templates/default.conf @@ -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 %} diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index cc3b5ce..c8d5719 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -662,5 +662,34 @@ module.exports = { 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); + } } }; diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 3f89474..7e51643 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -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 */ diff --git a/src/frontend/js/app/router.js b/src/frontend/js/app/router.js index f6b686f..790ef81 100644 --- a/src/frontend/js/app/router.js +++ b/src/frontend/js/app/router.js @@ -15,6 +15,7 @@ module.exports = AppRouter.default.extend({ 'nginx/access': 'showNginxAccess', 'nginx/certificates': 'showNginxCertificates', 'audit-log': 'showAuditLog', + 'settings': 'showSettings', '*default': 'showDashboard' } }); diff --git a/src/frontend/js/app/settings/default-site/main.ejs b/src/frontend/js/app/settings/default-site/main.ejs new file mode 100644 index 0000000..d434c90 --- /dev/null +++ b/src/frontend/js/app/settings/default-site/main.ejs @@ -0,0 +1,77 @@ + diff --git a/src/frontend/js/app/settings/default-site/main.js b/src/frontend/js/app/settings/default-site/main.js new file mode 100644 index 0000000..4bd14e5 --- /dev/null +++ b/src/frontend/js/app/settings/default-site/main.js @@ -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()) { + $('').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'); + } +}); diff --git a/src/frontend/js/app/settings/list/item.ejs b/src/frontend/js/app/settings/list/item.ejs new file mode 100644 index 0000000..4f81b45 --- /dev/null +++ b/src/frontend/js/app/settings/list/item.ejs @@ -0,0 +1,21 @@ + +
<%- name %>
+
+ <%- description %> +
+ + +
+ <% if (id === 'default-site') { %> + <%- i18n('settings', 'default-site-' + value) %> + <% } %> +
+ + + + \ No newline at end of file diff --git a/src/frontend/js/app/settings/list/item.js b/src/frontend/js/app/settings/list/item.js new file mode 100644 index 0000000..c79b73b --- /dev/null +++ b/src/frontend/js/app/settings/list/item.js @@ -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); + } +}); diff --git a/src/frontend/js/app/settings/list/main.ejs b/src/frontend/js/app/settings/list/main.ejs new file mode 100644 index 0000000..c96e923 --- /dev/null +++ b/src/frontend/js/app/settings/list/main.ejs @@ -0,0 +1,8 @@ + + <%- i18n('str', 'name') %> + <%- i18n('str', 'value') %> +   + + + + diff --git a/src/frontend/js/app/settings/list/main.js b/src/frontend/js/app/settings/list/main.js new file mode 100644 index 0000000..bbe75be --- /dev/null +++ b/src/frontend/js/app/settings/list/main.js @@ -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 + })); + } +}); diff --git a/src/frontend/js/app/settings/main.ejs b/src/frontend/js/app/settings/main.ejs new file mode 100644 index 0000000..2b02769 --- /dev/null +++ b/src/frontend/js/app/settings/main.ejs @@ -0,0 +1,14 @@ +
+
+
+

<%- i18n('settings', 'title') %>

+
+
+
+
+
+ +
+
+
+
diff --git a/src/frontend/js/app/settings/main.js b/src/frontend/js/app/settings/main.js new file mode 100644 index 0000000..348f467 --- /dev/null +++ b/src/frontend/js/app/settings/main.js @@ -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'); + }); + } +}); diff --git a/src/frontend/js/app/ui/menu/main.ejs b/src/frontend/js/app/ui/menu/main.ejs index 3363640..671b4e3 100644 --- a/src/frontend/js/app/ui/menu/main.ejs +++ b/src/frontend/js/app/ui/menu/main.ejs @@ -42,6 +42,9 @@ + <% } %> diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index 8c0dcdf..f4695f7 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -31,7 +31,8 @@ "online": "Online", "offline": "Offline", "unknown": "Unknown", - "expires": "Expires" + "expires": "Expires", + "value": "Value" }, "login": { "title": "Login to your account" @@ -222,6 +223,14 @@ "meta-title": "Details for Event", "view-meta": "View Details", "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" } } } diff --git a/src/frontend/js/models/setting.js b/src/frontend/js/models/setting.js new file mode 100644 index 0000000..4ee198d --- /dev/null +++ b/src/frontend/js/models/setting.js @@ -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 + }) +};