mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 15:53:33 +00:00 
			
		
		
		
	Default Site customisation and new Settings space (#91)
This commit is contained in:
		| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 \ | ||||
|   | ||||
| @@ -17,7 +17,7 @@ const internalNginx = { | ||||
|      * - IF BAD: update the meta with offline status and remove the config entirely | ||||
|      * - then reload nginx | ||||
|      * | ||||
|      * @param   {Object}  model | ||||
|      * @param   {Object|String}  model | ||||
|      * @param   {String}         host_type | ||||
|      * @param   {Object}         host | ||||
|      * @returns {Promise} | ||||
| @@ -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,10 +158,12 @@ const internalNginx = { | ||||
|             } | ||||
|  | ||||
|             // Manipulate the data a bit before sending it to the template | ||||
|             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 | ||||
|                 .parseAndRender(template, host) | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|   | ||||
							
								
								
									
										133
									
								
								src/backend/internal/setting.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/backend/internal/setting.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										7
									
								
								src/backend/lib/access/settings-get.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/backend/lib/access/settings-get.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/backend/lib/access/settings-list.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/backend/lib/access/settings-list.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/backend/lib/access/settings-update.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/backend/lib/access/settings-update.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										54
									
								
								src/backend/migrations/20190227065017_settings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/backend/migrations/20190227065017_settings.js
									
									
									
									
									
										Normal 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); | ||||
| }; | ||||
							
								
								
									
										30
									
								
								src/backend/models/setting.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/backend/models/setting.js
									
									
									
									
									
										Normal 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; | ||||
| @@ -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')); | ||||
|   | ||||
							
								
								
									
										96
									
								
								src/backend/routes/api/settings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/backend/routes/api/settings.js
									
									
									
									
									
										Normal 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; | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										99
									
								
								src/backend/schema/endpoints/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/backend/schema/endpoints/settings.json
									
									
									
									
									
										Normal 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" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -34,6 +34,9 @@ | ||||
|     }, | ||||
|     "access-lists": { | ||||
|       "$ref": "endpoints/access-lists.json" | ||||
|     }, | ||||
|     "settings": { | ||||
|       "$ref": "endpoints/settings.json" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/backend/templates/default.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/backend/templates/default.conf
									
									
									
									
									
										Normal 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 %} | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -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 | ||||
|      */ | ||||
|   | ||||
| @@ -15,6 +15,7 @@ module.exports = AppRouter.default.extend({ | ||||
|         'nginx/access':       'showNginxAccess', | ||||
|         'nginx/certificates': 'showNginxCertificates', | ||||
|         'audit-log':          'showAuditLog', | ||||
|         'settings':           'showSettings', | ||||
|         '*default':           'showDashboard' | ||||
|     } | ||||
| }); | ||||
|   | ||||
							
								
								
									
										77
									
								
								src/frontend/js/app/settings/default-site/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/frontend/js/app/settings/default-site/main.ejs
									
									
									
									
									
										Normal 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"> </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> | ||||
							
								
								
									
										71
									
								
								src/frontend/js/app/settings/default-site/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/frontend/js/app/settings/default-site/main.js
									
									
									
									
									
										Normal 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'); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										21
									
								
								src/frontend/js/app/settings/list/item.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/frontend/js/app/settings/list/item.ejs
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										25
									
								
								src/frontend/js/app/settings/list/item.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/frontend/js/app/settings/list/item.js
									
									
									
									
									
										Normal 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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										8
									
								
								src/frontend/js/app/settings/list/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/frontend/js/app/settings/list/main.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| <thead> | ||||
|     <th><%- i18n('str', 'name') %></th> | ||||
|     <th><%- i18n('str', 'value') %></th> | ||||
|     <th> </th> | ||||
| </thead> | ||||
| <tbody> | ||||
|     <!-- items --> | ||||
| </tbody> | ||||
							
								
								
									
										29
									
								
								src/frontend/js/app/settings/list/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/frontend/js/app/settings/list/main.js
									
									
									
									
									
										Normal 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 | ||||
|         })); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										14
									
								
								src/frontend/js/app/settings/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/frontend/js/app/settings/main.ejs
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										50
									
								
								src/frontend/js/app/settings/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/frontend/js/app/settings/main.js
									
									
									
									
									
										Normal 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'); | ||||
|             }); | ||||
|     } | ||||
| }); | ||||
| @@ -42,6 +42,9 @@ | ||||
|                 <li class="nav-item"> | ||||
|                     <a href="/audit-log" class="nav-link"><i class="fe fe-book-open"></i> <%- i18n('audit-log', 'title') %></a> | ||||
|                 </li> | ||||
|                 <li class="nav-item"> | ||||
|                     <a href="/settings" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('settings', 'title') %></a> | ||||
|                 </li> | ||||
|                 <% } %> | ||||
|             </ul> | ||||
|         </div> | ||||
|   | ||||
| @@ -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" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/frontend/js/models/setting.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/frontend/js/models/setting.js
									
									
									
									
									
										Normal 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 | ||||
|     }) | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user