mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00:00 
			
		
		
		
	Form design for proxy hosts, audit log base
This commit is contained in:
		
							
								
								
									
										52
									
								
								src/backend/internal/audit-log.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/backend/internal/audit-log.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const auditLogModel = require('../models/audit-log'); | ||||
|  | ||||
| const internalAuditLog = { | ||||
|  | ||||
|     /** | ||||
|      * Internal use only | ||||
|      * | ||||
|      * @param   {Object}  data | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     create: data => { | ||||
|         // TODO | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * All logs | ||||
|      * | ||||
|      * @param   {Access}  access | ||||
|      * @param   {Array}   [expand] | ||||
|      * @param   {String}  [search_query] | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     getAll: (access, expand, search_query) => { | ||||
|         return access.can('auditlog:list') | ||||
|             .then(() => { | ||||
|                 let query = auditLogModel | ||||
|                     .query() | ||||
|                     .orderBy('created_on', 'DESC') | ||||
|                     .limit(100); | ||||
|  | ||||
|                 // Query is used for searching | ||||
|                 if (typeof search_query === 'string') { | ||||
|                     /* | ||||
|                     query.where(function () { | ||||
|                         this.where('name', 'like', '%' + search_query + '%') | ||||
|                             .orWhere('email', 'like', '%' + search_query + '%'); | ||||
|                     }); | ||||
|                     */ | ||||
|                 } | ||||
|  | ||||
|                 if (typeof expand !== 'undefined' && expand !== null) { | ||||
|                     query.eager('[' + expand.join(', ') + ']'); | ||||
|                 } | ||||
|  | ||||
|                 return query; | ||||
|             }); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| module.exports = internalAuditLog; | ||||
							
								
								
									
										7
									
								
								src/backend/lib/access/auditlog-list.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/backend/lib/access/auditlog-list.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -157,6 +157,19 @@ exports.up = function (knex/*, Promise*/) { | ||||
|         }) | ||||
|         .then(() => { | ||||
|             logger.info('[' + migrate_name + '] access_list_auth Table created'); | ||||
|  | ||||
|             return knex.schema.createTable('audit_log', table => { | ||||
|                 table.increments().primary(); | ||||
|                 table.dateTime('created_on').notNull(); | ||||
|                 table.dateTime('modified_on').notNull(); | ||||
|                 table.integer('user_id').notNull().unsigned(); | ||||
|                 // TODO | ||||
|                 table.string('action').notNull(); | ||||
|                 table.json('meta').notNull(); | ||||
|             }); | ||||
|         }) | ||||
|         .then(() => { | ||||
|             logger.info('[' + migrate_name + '] audit_log Table created'); | ||||
|         }); | ||||
|  | ||||
| }; | ||||
|   | ||||
							
								
								
									
										30
									
								
								src/backend/models/audit-log.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/backend/models/audit-log.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| // Objection Docs: | ||||
| // http://vincit.github.io/objection.js/ | ||||
|  | ||||
| 'use strict'; | ||||
|  | ||||
| const db    = require('../db'); | ||||
| const Model = require('objection').Model; | ||||
|  | ||||
| Model.knex(db); | ||||
|  | ||||
| class AuditLog extends Model { | ||||
|     $beforeInsert () { | ||||
|         this.created_on  = Model.raw('NOW()'); | ||||
|         this.modified_on = Model.raw('NOW()'); | ||||
|     } | ||||
|  | ||||
|     $beforeUpdate () { | ||||
|         this.modified_on = Model.raw('NOW()'); | ||||
|     } | ||||
|  | ||||
|     static get name () { | ||||
|         return 'AuditLog'; | ||||
|     } | ||||
|  | ||||
|     static get tableName () { | ||||
|         return 'audit_log'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = AuditLog; | ||||
							
								
								
									
										54
									
								
								src/backend/routes/api/audit-log.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/backend/routes/api/audit-log.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const express          = require('express'); | ||||
| const validator        = require('../../lib/validator'); | ||||
| const jwtdecode        = require('../../lib/express/jwt-decode'); | ||||
| const internalAuditLog = require('../../internal/audit-log'); | ||||
|  | ||||
| let router = express.Router({ | ||||
|     caseSensitive: true, | ||||
|     strict:        true, | ||||
|     mergeParams:   true | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * /api/audit-log | ||||
|  */ | ||||
| router | ||||
|     .route('/') | ||||
|     .options((req, res) => { | ||||
|         res.sendStatus(204); | ||||
|     }) | ||||
|     .all(jwtdecode()) | ||||
|  | ||||
|     /** | ||||
|      * GET /api/audit-log | ||||
|      * | ||||
|      * Retrieve all logs | ||||
|      */ | ||||
|     .get((req, res, next) => { | ||||
|         validator({ | ||||
|             additionalProperties: false, | ||||
|             properties:           { | ||||
|                 expand: { | ||||
|                     $ref: 'definitions#/definitions/expand' | ||||
|                 }, | ||||
|                 query:  { | ||||
|                     $ref: 'definitions#/definitions/query' | ||||
|                 } | ||||
|             } | ||||
|         }, { | ||||
|             expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), | ||||
|             query:  (typeof req.query.query === 'string' ? req.query.query : null) | ||||
|         }) | ||||
|             .then(data => { | ||||
|                 return internalAuditLog.getAll(res.locals.access, data.expand, data.query); | ||||
|             }) | ||||
|             .then(rows => { | ||||
|                 res.status(200) | ||||
|                     .send(rows); | ||||
|             }) | ||||
|             .catch(next); | ||||
|     }); | ||||
|  | ||||
| module.exports = router; | ||||
| @@ -29,6 +29,7 @@ router.get('/', (req, res/*, next*/) => { | ||||
|  | ||||
| router.use('/tokens', require('./tokens')); | ||||
| router.use('/users', require('./users')); | ||||
| router.use('/audit-log', require('./audit-log')); | ||||
| router.use('/reports', require('./reports')); | ||||
| router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); | ||||
| router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); | ||||
|   | ||||
| @@ -257,6 +257,13 @@ module.exports = { | ||||
|              */ | ||||
|             getAll: function (expand, query) { | ||||
|                 return getAllObjects('nginx/proxy-hosts', expand, query); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param {Object}  data | ||||
|              */ | ||||
|             create: function (data) { | ||||
|                 return fetch('post', 'nginx/proxy-hosts', data); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
| @@ -305,6 +312,17 @@ module.exports = { | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     AuditLog: { | ||||
|         /** | ||||
|          * @param   {Array}    [expand] | ||||
|          * @param   {String}   [query] | ||||
|          * @returns {Promise} | ||||
|          */ | ||||
|         getAll: function (expand, query) { | ||||
|             return getAllObjects('audit-log', expand, query); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     Reports: { | ||||
|  | ||||
|         /** | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/frontend/js/app/audit-log/list/item.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/frontend/js/app/audit-log/list/item.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- avatar || '/images/default-avatar.jpg' %>)"> | ||||
|         <span class="avatar-status <%- is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- name %></div> | ||||
|     <div class="small text-muted"> | ||||
|         Created: <%- formatDbDate(created_on, 'Do MMMM YYYY') %> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- email %></div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- roles.join(', ') %></div> | ||||
| </td> | ||||
| <td class="text-center"> | ||||
|     <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-user dropdown-item"><i class="dropdown-icon fe fe-edit"></i> Edit Details</a> | ||||
|             <a href="#" class="edit-permissions dropdown-item"><i class="dropdown-icon fe fe-shield"></i> Edit Permissions</a> | ||||
|             <a href="#" class="set-password dropdown-item"><i class="dropdown-icon fe fe-lock"></i> Set Password</a> | ||||
|             <% if (!isSelf()) { %> | ||||
|             <a href="#" class="login dropdown-item"><i class="dropdown-icon fe fe-log-in"></i> Sign in as User</a> | ||||
|             <div class="dropdown-divider"></div> | ||||
|             <a href="#" class="delete-user dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> Delete User</a> | ||||
|             <% } %> | ||||
|         </div> | ||||
|     </div> | ||||
| </td> | ||||
							
								
								
									
										72
									
								
								src/frontend/js/app/audit-log/list/item.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/frontend/js/app/audit-log/list/item.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Mn         = require('backbone.marionette'); | ||||
| const Controller = require('../../controller'); | ||||
| const Api        = require('../../api'); | ||||
| const Cache      = require('../../cache'); | ||||
| const Tokens     = require('../../tokens'); | ||||
| const template   = require('./item.ejs'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template: template, | ||||
|     tagName:  'tr', | ||||
|  | ||||
|     ui: { | ||||
|         edit:        'a.edit-user', | ||||
|         permissions: 'a.edit-permissions', | ||||
|         password:    'a.set-password', | ||||
|         login:       'a.login', | ||||
|         delete:      'a.delete-user' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
|         'click @ui.edit': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserForm(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.permissions': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserPermissions(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.password': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserPasswordForm(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.delete': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserDeleteConfirm(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.login': function (e) { | ||||
|             e.preventDefault(); | ||||
|  | ||||
|             if (Cache.User.get('id') !== this.model.get('id')) { | ||||
|                 this.ui.login.prop('disabled', true).addClass('btn-disabled'); | ||||
|  | ||||
|                 Api.Users.loginAs(this.model.get('id')) | ||||
|                     .then(res => { | ||||
|                         Tokens.addToken(res.token, res.user.nickname || res.user.name); | ||||
|                         window.location = '/'; | ||||
|                         window.location.reload(); | ||||
|                     }) | ||||
|                     .catch(err => { | ||||
|                         alert(err.message); | ||||
|                         this.ui.login.prop('disabled', false).removeClass('btn-disabled'); | ||||
|                     }); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         isSelf: function () { | ||||
|             return Cache.User.get('id') === this.id; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     initialize: function () { | ||||
|         this.listenTo(this.model, 'change', this.render); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										10
									
								
								src/frontend/js/app/audit-log/list/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/frontend/js/app/audit-log/list/main.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| <thead> | ||||
| <th width="30"> </th> | ||||
| <th>Name</th> | ||||
| <th>Email</th> | ||||
| <th>Roles</th> | ||||
| <th> </th> | ||||
| </thead> | ||||
| <tbody> | ||||
| <!-- items --> | ||||
| </tbody> | ||||
							
								
								
									
										29
									
								
								src/frontend/js/app/audit-log/list/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/frontend/js/app/audit-log/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/audit-log/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/frontend/js/app/audit-log/main.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| <div class="card"> | ||||
|     <div class="card-header"> | ||||
|         <h3 class="card-title">Audit Log</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> | ||||
							
								
								
									
										56
									
								
								src/frontend/js/app/audit-log/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/frontend/js/app/audit-log/main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Mn            = require('backbone.marionette'); | ||||
| const AuditLogModel = require('../../models/audit-log'); | ||||
| const Api           = require('../api'); | ||||
| const Controller    = require('../controller'); | ||||
| const ListView      = require('./list/main'); | ||||
| const template      = require('./main.ejs'); | ||||
| const ErrorView     = require('../error/main'); | ||||
| const EmptyView     = require('../empty/main'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     id:       'audit-log', | ||||
|     template: template, | ||||
|  | ||||
|     ui: { | ||||
|         list_region: '.list-region', | ||||
|         dimmer:      '.dimmer' | ||||
|     }, | ||||
|  | ||||
|     regions: { | ||||
|         list_region: '@ui.list_region' | ||||
|     }, | ||||
|  | ||||
|     onRender: function () { | ||||
|         let view = this; | ||||
|  | ||||
|         Api.AuditLog.getAll() | ||||
|             .then(response => { | ||||
|                 if (!view.isDestroyed() && response && response.length) { | ||||
|                     view.showChildView('list_region', new ListView({ | ||||
|                         collection: new AuditLogModel.Collection(response) | ||||
|                     })); | ||||
|                 } else { | ||||
|                     view.showChildView('list_region', new EmptyView({ | ||||
|                         title:    'There are no logs.', | ||||
|                         subtitle: 'As soon as you or another user changes something, history of those events will show up here.' | ||||
|                     })); | ||||
|                 } | ||||
|             }) | ||||
|             .catch(err => { | ||||
|                 view.showChildView('list_region', new ErrorView({ | ||||
|                     code:    err.code, | ||||
|                     message: err.message, | ||||
|                     retry:   function () { | ||||
|                         Controller.showAuditLog(); | ||||
|                     } | ||||
|                 })); | ||||
|  | ||||
|                 console.error(err); | ||||
|             }) | ||||
|             .then(() => { | ||||
|                 view.ui.dimmer.removeClass('active'); | ||||
|             }); | ||||
|     } | ||||
| }); | ||||
| @@ -204,15 +204,18 @@ module.exports = { | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Dashboard | ||||
|      * Audit Log | ||||
|      */ | ||||
|     showProfile: function () { | ||||
|     showAuditLog: function () { | ||||
|         let controller = this; | ||||
|  | ||||
|         require(['./main', './profile/main'], (App, View) => { | ||||
|             controller.navigate('/profile'); | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             require(['./main', './audit-log/main'], (App, View) => { | ||||
|                 controller.navigate('/audit-log'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } else { | ||||
|             this.showDashboard(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -47,6 +47,11 @@ const App = Mn.Application.extend({ | ||||
|                 this.UI.on('render', () => { | ||||
|                     new Router(options); | ||||
|                     Backbone.history.start({pushState: true}); | ||||
|  | ||||
|                     // Ask the admin use to change their details | ||||
|                     if (Cache.User.get('email') === 'admin@example.com') { | ||||
|                         Controller.showUserForm(Cache.User); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 this.getRegion().show(this.UI); | ||||
|   | ||||
| @@ -3,13 +3,20 @@ | ||||
|         <h5 class="modal-title"><% if (typeof id !== 'undefined') { %>Edit<% } else { %>New<% } %> Proxy Host</h5> | ||||
|         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|     <div class="modal-body has-tabs"> | ||||
|         <form> | ||||
|             <ul class="nav nav-tabs" role="tablist"> | ||||
|                 <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> Details</a></li> | ||||
|                 <li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> SSL</a></li> | ||||
|             </ul> | ||||
|             <div class="tab-content"> | ||||
|                 <!-- Details --> | ||||
|                 <div role="tabpanel" class="tab-pane active" id="details"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm-12 col-md-12"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label">Domain Name <span class="form-required">*</span></label> | ||||
|                         <input name="domain_name" type="text" class="form-control" placeholder="example.com" value="<%- domain_name %>" required> | ||||
|                                 <input name="domain_name" type="text" class="form-control" placeholder="example.com or *.example.com" value="<%- domain_name %>" pattern="(\*\.)?[a-z0-9\.]+" required title="Please enter a valid domain name. Domain wildcards are allowed: *.yourdomain.com"> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-8 col-md-8"> | ||||
| @@ -25,6 +32,86 @@ | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <!-- SSL --> | ||||
|                 <div role="tabpanel" class="tab-pane" id="ssl-options"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm-6 col-md-6"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="ssl_enabled" value="1"<%- ssl_enabled ? ' checked' : '' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description">Enable SSL</span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-6 col-md-6"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="ssl_forced" value="1"<%- ssl_forced ? ' checked' : '' %><%- ssl_enabled ? '' : ' disabled' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description">Force SSL</span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label">Certificate Provider</label> | ||||
|                                 <div class="selectgroup w-100"> | ||||
|                                     <label class="selectgroup-item"> | ||||
|                                         <input type="radio" name="ssl_provider" value="letsencrypt" class="selectgroup-input"<%- ssl_provider !== 'other' ? ' checked' : '' %>> | ||||
|                                         <span class="selectgroup-button">Let's Encrypt</span> | ||||
|                                     </label> | ||||
|                                     <label class="selectgroup-item"> | ||||
|                                         <input type="radio" name="ssl_provider" value="other" class="selectgroup-input"<%- ssl_provider === 'other' ? ' checked' : '' %>> | ||||
|                                         <span class="selectgroup-button">Other</span> | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- Lets encrypt --> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label">Email Address for Let's Encrypt <span class="form-required">*</span></label> | ||||
|                                 <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required<%- getLetsencryptAgree() ? ' checked' : '' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description">I Agree to the <a href="https://letsencrypt.org/repository/" target="_blank">Let's Encrypt Terms of Service</a> <span class="form-required">*</span></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- Other --> | ||||
|                         <div class="col-sm-12 col-md-12 other-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <div class="form-label">Certificate</div> | ||||
|                                 <div class="custom-file"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]"> | ||||
|                                     <label class="custom-file-label">Choose file</label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12 other-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <div class="form-label">Certificate Key</div> | ||||
|                                 <div class="custom-file"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]"> | ||||
|                                     <label class="custom-file-label">Choose file</label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|         </form> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const _              = require('underscore'); | ||||
| const Mn             = require('backbone.marionette'); | ||||
| const template       = require('./form.ejs'); | ||||
| const Controller     = require('../../controller'); | ||||
| @@ -17,77 +18,109 @@ module.exports = Mn.View.extend({ | ||||
|  | ||||
|     ui: { | ||||
|         form:         'form', | ||||
|         domain_name:  'input[name="domain_name"]', | ||||
|         forward_ip:   'input[name="forward_ip"]', | ||||
|         buttons:      '.modal-footer button', | ||||
|         cancel:       'button.cancel', | ||||
|         save:         'button.save', | ||||
|         error:      '.secret-error' | ||||
|         ssl_enabled:  'input[name="ssl_enabled"]', | ||||
|         ssl_options:  '#ssl-options input', | ||||
|         ssl_provider: 'input[name="ssl_provider"]', | ||||
|  | ||||
|         // SSL hiding and showing | ||||
|         all_ssl:         '.letsencrypt-ssl, .other-ssl', | ||||
|         letsencrypt_ssl: '.letsencrypt-ssl', | ||||
|         other_ssl:       '.other-ssl' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
|         'change @ui.ssl_enabled': function () { | ||||
|             let enabled = this.ui.ssl_enabled.prop('checked'); | ||||
|             this.ui.ssl_options.not(this.ui.ssl_enabled).prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5); | ||||
|             this.ui.ssl_provider.trigger('change'); | ||||
|         }, | ||||
|  | ||||
|         'change @ui.ssl_provider': function () { | ||||
|             let enabled  = this.ui.ssl_enabled.prop('checked'); | ||||
|             let provider = this.ui.ssl_provider.filter(':checked').val(); | ||||
|             this.ui.all_ssl.hide().find('input').prop('disabled', true); | ||||
|             this.ui[provider + '_ssl'].show().find('input').prop('disabled', !enabled); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.save': function (e) { | ||||
|             e.preventDefault(); | ||||
|             return; | ||||
|  | ||||
|             this.ui.error.hide(); | ||||
|             if (!this.ui.form[0].checkValidity()) { | ||||
|                 $('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let view = this; | ||||
|             let data = this.ui.form.serializeJSON(); | ||||
|  | ||||
|             // Manipulate | ||||
|             data.roles = []; | ||||
|             if ((this.model.get('id') === Cache.User.get('id') && this.model.isAdmin()) || (typeof data.is_admin !== 'undefined' && data.is_admin)) { | ||||
|                 data.roles.push('admin'); | ||||
|                 delete data.is_admin; | ||||
|             data.forward_port = parseInt(data.forward_port, 10); | ||||
|             _.map(data, function (item, idx) { | ||||
|                 if (typeof item === 'string' && item === '1') { | ||||
|                     item = true; | ||||
|                 } else if (typeof item === 'object' && item !== null) { | ||||
|                     _.map(item, function (item2, idx2) { | ||||
|                         if (typeof item2 === 'string' && item2 === '1') { | ||||
|                             item[idx2] = true; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|                 data[idx] = item; | ||||
|             }); | ||||
|  | ||||
|             data.is_disabled = typeof data.is_disabled !== 'undefined' ? !!data.is_disabled : false; | ||||
|             // Process | ||||
|             this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); | ||||
|             let method = Api.Users.create; | ||||
|             let method = Api.Nginx.ProxyHosts.create; | ||||
|  | ||||
|             if (this.model.get('id')) { | ||||
|                 // edit | ||||
|                 method  = Api.Users.update; | ||||
|                 method  = Api.Nginx.ProxyHosts.update; | ||||
|                 data.id = this.model.get('id'); | ||||
|             } | ||||
|  | ||||
|             method(data) | ||||
|                 .then(result => { | ||||
|                     if (result.id === Cache.User.get('id')) { | ||||
|                         Cache.User.set(result); | ||||
|                     } | ||||
|  | ||||
|                     if (view.model.get('id') !== Cache.User.get('id')) { | ||||
|                         Controller.showUsers(); | ||||
|                     } | ||||
|  | ||||
|                     view.model.set(result); | ||||
|                     App.UI.closeModal(function () { | ||||
|                         if (method === Api.Users.create) { | ||||
|                             // Show permissions dialog immediately | ||||
|                             Controller.showUserPermissions(view.model); | ||||
|                         if (method === Api.Nginx.ProxyHosts.create) { | ||||
|                             Controller.showNginxProxy(); | ||||
|                         } | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch(err => { | ||||
|                     this.ui.error.text(err.message).show(); | ||||
|                     alert(err.message); | ||||
|                     this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); | ||||
|                 }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         getLetsencryptEmail: function () { | ||||
|             return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : Cache.User.get('email'); | ||||
|         }, | ||||
|  | ||||
|         getLetsencryptAgree: function () { | ||||
|             return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     onRender: function () { | ||||
|         this.ui.forward_ip.mask('099.099.099.099', { | ||||
|             clearIfNotMatch: true, | ||||
|             placeholder:     '000.000.000.000' | ||||
|         }); | ||||
|         /* | ||||
|         this.ui.forward_ip.mask('099.099.099.099', { | ||||
|             reverse:         true, | ||||
|             clearIfNotMatch: true, | ||||
|             placeholder:     '000.000.000.000' | ||||
|         }); | ||||
|         */ | ||||
|  | ||||
|         this.ui.ssl_enabled.trigger('change'); | ||||
|         this.ui.ssl_provider.trigger('change'); | ||||
|  | ||||
|         this.ui.domain_name[0].oninvalid = function () { | ||||
|             this.setCustomValidity('Please enter a valid domain name. Domain wildcards are allowed: *.yourdomain.com'); | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     initialize: function (options) { | ||||
|   | ||||
| @@ -1,33 +0,0 @@ | ||||
| <div class="row"> | ||||
|  | ||||
|     <div class="col-lg-4 col-md-6 col-xs-12"> | ||||
|         <div class="card"> | ||||
|             <div class="card-header"> | ||||
|                 <h3 class="card-title">My Profile</h3> | ||||
|             </div> | ||||
|             <div class="card-body"> | ||||
|                 <form> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-auto"> | ||||
|                             <span class="avatar avatar-xl" style="background-image: url(<%- avatar || '/images/default-avatar.jpg' %>)"></span> | ||||
|                         </div> | ||||
|                         <div class="col"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label">Name</label> | ||||
|                                 <input name="name" class="form-control" value="<%- name %>"> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="form-label">Email-Address</label> | ||||
|                         <input name="email" class="form-control" value="<%- email %>"> | ||||
|                     </div> | ||||
|                     <div class="form-footer"> | ||||
|                         <button class="btn btn-primary btn-block">Save</button> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
| </div> | ||||
| @@ -1,21 +0,0 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Mn       = require('backbone.marionette'); | ||||
| const Cache    = require('../cache'); | ||||
| const template = require('./main.ejs'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template: template, | ||||
|     id:       'profile', | ||||
|  | ||||
|     templateContext: { | ||||
|         getUserField: function (field, default_val) { | ||||
|             return Cache.User.get(field) || default_val; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     initialize: function () { | ||||
|         this.model = Cache.User; | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @@ -6,13 +6,13 @@ const Controller = require('./controller'); | ||||
| module.exports = Mn.AppRouter.extend({ | ||||
|     appRoutes: { | ||||
|         users:               'showUsers', | ||||
|         profile:             'showProfile', | ||||
|         logout:              'logout', | ||||
|         'nginx/proxy':       'showNginxProxy', | ||||
|         'nginx/redirection': 'showNginxRedirection', | ||||
|         'nginx/404':         'showNginxDead', | ||||
|         'nginx/stream':      'showNginxStream', | ||||
|         'nginx/access':      'showNginxAccess', | ||||
|         'audit-log':         'showAuditLog', | ||||
|         '*default':          'showDashboard' | ||||
|     }, | ||||
|  | ||||
|   | ||||
| @@ -14,8 +14,11 @@ | ||||
|                     </span> | ||||
|                 </a> | ||||
|                 <div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow"> | ||||
|                     <a class="dropdown-item profile" href="/profile"> | ||||
|                         <i class="dropdown-icon fe fe-user"></i> Profile | ||||
|                     <a class="dropdown-item edit-details" href="#"> | ||||
|                         <i class="dropdown-icon fe fe-user"></i> Edit Details | ||||
|                     </a> | ||||
|                     <a class="dropdown-item change-password" href="#"> | ||||
|                         <i class="dropdown-icon fe fe-lock"></i> Change Password | ||||
|                     </a> | ||||
|                     <div class="dropdown-divider"></div> | ||||
|                     <a class="dropdown-item logout" href="/logout"> | ||||
|   | ||||
| @@ -13,10 +13,22 @@ module.exports = Mn.View.extend({ | ||||
|     template:  template, | ||||
|  | ||||
|     ui: { | ||||
|         link: 'a' | ||||
|         link:     'a', | ||||
|         details:  'a.edit-details', | ||||
|         password: 'a.change-password' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
|         'click @ui.details': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserForm(Cache.User); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.password': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserPasswordForm(Cache.User); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.link': function (e) { | ||||
|             e.preventDefault(); | ||||
|             let href = $(e.currentTarget).attr('href'); | ||||
| @@ -25,9 +37,6 @@ module.exports = Mn.View.extend({ | ||||
|                 case '/': | ||||
|                     Controller.showDashboard(); | ||||
|                     break; | ||||
|                 case '/profile': | ||||
|                     Controller.showProfile(); | ||||
|                     break; | ||||
|                 case '/logout': | ||||
|                     Controller.logout(); | ||||
|                     break; | ||||
|   | ||||
| @@ -30,10 +30,13 @@ | ||||
|                     <a href="/nginx/access" class="nav-link"><i class="fe fe-lock"></i> Access Lists</a> | ||||
|                 </li> | ||||
|                 <% } %> | ||||
|                 <% if (showUsers()) { %> | ||||
|                 <% if (isAdmin()) { %> | ||||
|                 <li class="nav-item"> | ||||
|                     <a href="/users" class="nav-link"><i class="fe fe-users"></i> Users</a> | ||||
|                 </li> | ||||
|                 <li class="nav-item"> | ||||
|                     <a href="/audit-log" class="nav-link"><i class="fe fe-book-open"></i> Audit Log</a> | ||||
|                 </li> | ||||
|                 <% } %> | ||||
|             </ul> | ||||
|         </div> | ||||
|   | ||||
| @@ -26,7 +26,7 @@ module.exports = Mn.View.extend({ | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         showUsers: function () { | ||||
|         isAdmin: function () { | ||||
|             return Cache.User.isAdmin(); | ||||
|         }, | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ | ||||
|                         <div class="invalid-feedback secret-error"></div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <% if (isAdmin()) { %> | ||||
|                 <div class="col-sm-12 col-md-12"> | ||||
|                     <div class="form-label">Roles</div> | ||||
|                 </div> | ||||
| @@ -46,6 +47,7 @@ | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <% } %> | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
|   | ||||
| @@ -30,6 +30,15 @@ module.exports = Mn.View.extend({ | ||||
|             let view = this; | ||||
|             let data = this.ui.form.serializeJSON(); | ||||
|  | ||||
|             let show_password = this.model.get('email') === 'admin@example.com'; | ||||
|  | ||||
|             // admin@example.com is not allowed | ||||
|             if (data.email === 'admin@example.com') { | ||||
|                 this.ui.error.text('Default email address must be changed').show(); | ||||
|                 this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Manipulate | ||||
|             data.roles = []; | ||||
|             if ((this.model.get('id') === Cache.User.get('id') && this.model.isAdmin()) || (typeof data.is_admin !== 'undefined' && data.is_admin)) { | ||||
| @@ -62,6 +71,8 @@ module.exports = Mn.View.extend({ | ||||
|                         if (method === Api.Users.create) { | ||||
|                             // Show permissions dialog immediately | ||||
|                             Controller.showUserPermissions(view.model); | ||||
|                         } else if (show_password) { | ||||
|                             Controller.showUserPasswordForm(view.model); | ||||
|                         } | ||||
|                     }); | ||||
|                 }) | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const Api        = require('../api'); | ||||
| const Controller = require('../controller'); | ||||
| const ListView   = require('./list/main'); | ||||
| const template   = require('./main.ejs'); | ||||
| const ErrorView  = require('../error/main'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     id:       'users', | ||||
|   | ||||
							
								
								
									
										20
									
								
								src/frontend/js/models/audit-log.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/frontend/js/models/audit-log.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Backbone = require('backbone'); | ||||
|  | ||||
| const model = Backbone.Model.extend({ | ||||
|     idAttribute: 'id', | ||||
|  | ||||
|     defaults: function () { | ||||
|         return { | ||||
|             name: '' | ||||
|         }; | ||||
|     } | ||||
| }); | ||||
|  | ||||
| module.exports = { | ||||
|     Model:      model, | ||||
|     Collection: Backbone.Collection.extend({ | ||||
|         model: model | ||||
|     }) | ||||
| }; | ||||
| @@ -72,3 +72,17 @@ $blue: #467fcf; | ||||
| .dimmer .loader { | ||||
|     margin-top: 50px; | ||||
| } | ||||
|  | ||||
| /* modal tabs */ | ||||
|  | ||||
| .modal-body.has-tabs { | ||||
|     padding: 0; | ||||
|  | ||||
|     .nav-tabs { | ||||
|         margin: 0; | ||||
|     } | ||||
|  | ||||
|     .tab-content { | ||||
|         padding: 1rem; | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user