diff --git a/src/backend/internal/user.js b/src/backend/internal/user.js index 9d6abb8..7a487b6 100644 --- a/src/backend/internal/user.js +++ b/src/backend/internal/user.js @@ -1,11 +1,12 @@ 'use strict'; -const _ = require('lodash'); -const error = require('../lib/error'); -const userModel = require('../models/user'); -const authModel = require('../models/auth'); -const gravatar = require('gravatar'); -const internalToken = require('./token'); +const _ = require('lodash'); +const error = require('../lib/error'); +const userModel = require('../models/user'); +const userPermissionModel = require('../models/user_permission'); +const authModel = require('../models/auth'); +const gravatar = require('gravatar'); +const internalToken = require('./token'); function omissions () { return ['is_deleted']; @@ -56,7 +57,23 @@ const internalUser = { } }) .then(user => { - return internalUser.get(access, {id: user.id}); + // Create permissions row as well + let is_admin = data.roles.indexOf('admin') !== -1; + + return userPermissionModel + .query() + .insert({ + user_id: user.id, + visibility: is_admin ? 'all' : 'user', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + dead_hosts: 'manage', + streams: 'manage', + access_lists: 'manage' + }) + .then(() => { + return internalUser.get(access, {id: user.id, expand: ['permissions']}); + }); }); }, @@ -145,6 +162,7 @@ const internalUser = { .query() .where('is_deleted', 0) .andWhere('id', data.id) + .allowEager('[permissions]') .first(); // Custom omissions @@ -377,6 +395,50 @@ const internalUser = { }); }, + /** + * @param {Access} access + * @param {Object} data + * @return {Promise} + */ + setPermissions: (access, data) => { + return access.can('users:permissions', data.id) + .then(() => { + return internalUser.get(access, {id: data.id}); + }) + .then(user => { + if (user.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); + } + + return user; + }) + .then(user => { + // Get perms row, patch if it exists + return userPermissionModel + .query() + .where('user_id', user.id) + .first() + .then(existing_auth => { + if (existing_auth) { + // patch + return userPermissionModel + .query() + .where('user_id', user.id) + .patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data)); + } else { + // insert + return userPermissionModel + .query() + .insertAndFetch(_.assign({user_id: user.id}, data)); + } + }) + .then(permissions => { + return true; + }); + }); + }, + /** * @param {Access} access * @param {Object} data diff --git a/src/backend/lib/access/users-permissions.json b/src/backend/lib/access/users-permissions.json new file mode 100644 index 0000000..d2709fd --- /dev/null +++ b/src/backend/lib/access/users-permissions.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/migrations/20180618015850_initial.js b/src/backend/migrations/20180618015850_initial.js index 9ecb22c..03ee069 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -43,6 +43,124 @@ exports.up = function (knex/*, Promise*/) { }) .then(() => { logger.info('[' + migrate_name + '] user Table created'); + + return knex.schema.createTable('user_permission', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('user_id').notNull().unsigned(); + table.string('visibility').notNull(); + table.string('proxy_hosts').notNull(); + table.string('redirection_hosts').notNull(); + table.string('dead_hosts').notNull(); + table.string('streams').notNull(); + table.string('access_lists').notNull(); + table.unique('user_id'); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] user_permission Table created'); + + return knex.schema.createTable('proxy_host', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('domain_name').notNull(); + table.string('forward_ip').notNull(); + table.integer('forward_port').notNull().unsigned(); + table.integer('access_list_id').notNull().unsigned().defaultTo(0); + table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); + table.string('ssl_provider').notNull().defaultTo(''); + table.integer('ssl_forced').notNull().unsigned().defaultTo(0); + table.integer('caching_enabled').notNull().unsigned().defaultTo(0); + table.integer('block_exploits').notNull().unsigned().defaultTo(0); + table.json('meta').notNull(); + table.unique(['domain_name', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table created'); + + return knex.schema.createTable('redirection_host', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('domain_name').notNull(); + table.string('forward_domain_name').notNull(); + table.integer('preserve_path').notNull().unsigned().defaultTo(0); + table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); + table.string('ssl_provider').notNull().defaultTo(''); + table.integer('block_exploits').notNull().unsigned().defaultTo(0); + table.json('meta').notNull(); + table.unique(['domain_name', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] redirection_host Table created'); + + return knex.schema.createTable('dead_host', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('domain_name').notNull(); + table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); + table.string('ssl_provider').notNull().defaultTo(''); + table.json('meta').notNull(); + table.unique(['domain_name', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] dead_host Table created'); + + return knex.schema.createTable('stream', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.integer('incoming_port').notNull().unsigned(); + table.string('forward_ip').notNull(); + table.integer('forwarding_port').notNull().unsigned(); + table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0); + table.integer('udp_forwarding').notNull().unsigned().defaultTo(0); + table.json('meta').notNull(); + table.unique(['incoming_port', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] stream Table created'); + + return knex.schema.createTable('access_list', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('name').notNull(); + table.json('meta').notNull(); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] access_list Table created'); + + return knex.schema.createTable('access_list_auth', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('access_list_id').notNull().unsigned(); + table.string('username').notNull(); + table.string('password').notNull(); + table.json('meta').notNull(); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] access_list_auth Table created'); }); }; diff --git a/src/backend/models/user.js b/src/backend/models/user.js index b9a9a20..3a2ab10 100644 --- a/src/backend/models/user.js +++ b/src/backend/models/user.js @@ -3,8 +3,9 @@ 'use strict'; -const db = require('../db'); -const Model = require('objection').Model; +const db = require('../db'); +const Model = require('objection').Model; +const UserPermission = require('./user_permission'); Model.knex(db); @@ -30,6 +31,22 @@ class User extends Model { return ['roles']; } + static get relationMappings () { + return { + permissions: { + relation: Model.HasOneRelation, + modelClass: UserPermission, + join: { + from: 'user.id', + to: 'user_permission.user_id' + }, + modify: function (qb) { + qb.omit(['id', 'created_on', 'modified_on', 'user_id']); + } + } + }; + } + } module.exports = User; diff --git a/src/backend/models/user_permission.js b/src/backend/models/user_permission.js new file mode 100644 index 0000000..5848a9e --- /dev/null +++ b/src/backend/models/user_permission.js @@ -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 UserPermission 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 'UserPermission'; + } + + static get tableName () { + return 'user_permission'; + } +} + +module.exports = UserPermission; diff --git a/src/backend/routes/api/users.js b/src/backend/routes/api/users.js index a42d512..070709c 100644 --- a/src/backend/routes/api/users.js +++ b/src/backend/routes/api/users.js @@ -183,12 +183,12 @@ router }); /** - * Specific user service settings + * Specific user permissions * - * /api/users/123/services + * /api/users/123/permissions */ router - .route('/:user_id/services') + .route('/:user_id/permissions') .options((req, res) => { res.sendStatus(204); }) @@ -196,18 +196,18 @@ router .all(userIdFromMe) /** - * POST /api/users/123/services + * PUT /api/users/123/permissions * - * Sets Service Settings for a user + * Set some or all permissions for a user */ - .post((req, res, next) => { + .put((req, res, next) => { apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body) .then(payload => { payload.id = req.params.user_id; - return internalUser.setServiceSettings(res.locals.access, payload); + return internalUser.setPermissions(res.locals.access, payload); }) .then(result => { - res.status(200) + res.status(201) .send(result); }) .catch(next); diff --git a/src/backend/schema/endpoints/users.json b/src/backend/schema/endpoints/users.json index 3d82e63..1202713 100644 --- a/src/backend/schema/endpoints/users.json +++ b/src/backend/schema/endpoints/users.json @@ -206,6 +206,49 @@ "targetSchema": { "type": "boolean" } + }, + { + "title": "Set Permissions", + "description": "Sets Permissions for a User", + "href": "/users/{definitions.identity.example}/permissions", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "properties": { + "visibility": { + "type": "string", + "pattern": "^(all|user)$" + }, + "access_lists": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "dead_hosts": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "proxy_hosts": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "redirection_hosts": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "streams": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + } + } + }, + "targetSchema": { + "type": "boolean" + } } ], "properties": { diff --git a/src/backend/setup.js b/src/backend/setup.js index b6c8f97..8a253c5 100644 --- a/src/backend/setup.js +++ b/src/backend/setup.js @@ -1,11 +1,12 @@ 'use strict'; -const fs = require('fs'); -const NodeRSA = require('node-rsa'); -const config = require('config'); -const logger = require('./logger').global; -const userModel = require('./models/user'); -const authModel = require('./models/auth'); +const fs = require('fs'); +const NodeRSA = require('node-rsa'); +const config = require('config'); +const logger = require('./logger').global; +const userModel = require('./models/user'); +const userPermissionModel = require('./models/user_permission'); +const authModel = require('./models/auth'); module.exports = function () { return new Promise((resolve, reject) => { @@ -54,7 +55,7 @@ module.exports = function () { .select(userModel.raw('COUNT(`id`) as `count`')) .where('is_deleted', 0) .first('count') - .then((row) => { + .then(row => { if (!row.count) { // Create a new user and set password logger.info('Creating a new user: admin@example.com with password: changeme'); @@ -79,6 +80,19 @@ module.exports = function () { type: 'password', secret: 'changeme', meta: {} + }) + .then(() => { + return userPermissionModel + .query() + .insert({ + user_id: user.id, + visibility: 'all', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + dead_hosts: 'manage', + streams: 'manage', + access_lists: 'manage' + }); }); }); } diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index 1f23e16..b689136 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -224,6 +224,16 @@ module.exports = { */ loginAs: function (id) { return fetch('post', 'users/' + id + '/login'); + }, + + /** + * + * @param {Integer} id + * @param {Object} perms + * @returns {Promise} + */ + setPermissions: function (id, perms) { + return fetch('put', 'users/' + id + '/permissions', perms); } }, diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 7e02d9c..d7ea292 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -52,6 +52,19 @@ module.exports = { } }, + /** + * User Permissions Form + * + * @param model + */ + showUserPermissions: function (model) { + if (Cache.User.isAdmin()) { + require(['./main', './user/permissions'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * User Password Form * @@ -65,6 +78,19 @@ module.exports = { } }, + /** + * User Delete Confirm + * + * @param model + */ + showUserDeleteConfirm: function (model) { + if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) { + require(['./main', './user/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Error * diff --git a/src/frontend/js/app/main.js b/src/frontend/js/app/main.js index 3fe2210..b3d80bb 100644 --- a/src/frontend/js/app/main.js +++ b/src/frontend/js/app/main.js @@ -110,7 +110,7 @@ const App = Mn.Application.extend({ * @returns {Promise} */ bootstrap: function () { - return Api.Users.getById('me') + return Api.Users.getById('me', ['permissions']) .then(response => { Cache.User.set(response); Tokens.setCurrentName(response.nickname || response.name); diff --git a/src/frontend/js/app/user/delete.ejs b/src/frontend/js/app/user/delete.ejs new file mode 100644 index 0000000..b743fa9 --- /dev/null +++ b/src/frontend/js/app/user/delete.ejs @@ -0,0 +1,19 @@ +<div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Delete <%- name %></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"> + Are you sure you want to delete <strong><%- name %></strong>? + </div> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary cancel" data-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-danger save">Yes I'm Sure</button> + </div> +</div> diff --git a/src/frontend/js/app/user/delete.js b/src/frontend/js/app/user/delete.js new file mode 100644 index 0000000..a680611 --- /dev/null +++ b/src/frontend/js/app/user/delete.js @@ -0,0 +1,38 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const template = require('./delete.ejs'); +const Controller = require('../controller'); +const Api = require('../api'); +const App = require('../main'); + +require('jquery-serializejson'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + ui: { + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save' + }, + + events: { + + 'click @ui.save': function (e) { + e.preventDefault(); + + Api.Users.delete(this.model.get('id')) + .then(() => { + Controller.showUsers(); + App.UI.closeModal(); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + } +}); diff --git a/src/frontend/js/app/user/form.ejs b/src/frontend/js/app/user/form.ejs index aa6c042..33828a3 100644 --- a/src/frontend/js/app/user/form.ejs +++ b/src/frontend/js/app/user/form.ejs @@ -25,25 +25,27 @@ <div class="invalid-feedback secret-error"></div> </div> </div> - <% if (!isSelf()) { %> <div class="col-sm-12 col-md-12"> + <div class="form-label">Roles</div> + </div> + <div class="col-sm-6 col-md-6"> <div class="form-group"> - <div class="form-label">Switches</div> - <div class="custom-switches-stacked"> - <label class="custom-switch"> - <input type="checkbox" class="custom-switch-input" name="is_admin" value="1"<%- isAdmin() ? ' checked' : '' %>> - <span class="custom-switch-indicator"></span> - <span class="custom-switch-description">Administrator</span> - </label> - <label class="custom-switch"> - <input type="checkbox" class="custom-switch-input" name="is_disabled" value="1"<%- is_disabled ? ' checked' : '' %>> - <span class="custom-switch-indicator"></span> - <span class="custom-switch-description">Disabled</span> - </label> - </div> + <label class="custom-switch"> + <input type="checkbox" class="custom-switch-input" name="is_admin" value="1"<%- isAdmin() ? ' checked' : '' %><%- isSelf() ? ' disabled' : '' %>> + <span class="custom-switch-indicator"></span> + <span class="custom-switch-description">Administrator</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="is_disabled" value="1"<%- is_disabled ? ' checked' : '' %><%- isSelf() ? ' disabled' : '' %>> + <span class="custom-switch-indicator"></span> + <span class="custom-switch-description">Disabled</span> + </label> </div> </div> - <% } %> </div> </form> </div> diff --git a/src/frontend/js/app/user/form.js b/src/frontend/js/app/user/form.js index 5f0b11b..7adc871 100644 --- a/src/frontend/js/app/user/form.js +++ b/src/frontend/js/app/user/form.js @@ -58,7 +58,12 @@ module.exports = Mn.View.extend({ } view.model.set(result); - App.UI.closeModal(); + App.UI.closeModal(function () { + if (method === Api.Users.create) { + // Show permissions dialog immediately + Controller.showUserPermissions(view.model); + } + }); }) .catch(err => { this.ui.error.text(err.message).show(); diff --git a/src/frontend/js/app/user/permissions.ejs b/src/frontend/js/app/user/permissions.ejs new file mode 100644 index 0000000..043e71d --- /dev/null +++ b/src/frontend/js/app/user/permissions.ejs @@ -0,0 +1,140 @@ +<div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Permissions for <%- name %></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"> + + <% if (isAdmin()) { %> + <div class="alert alert-icon alert-secondary" role="alert"> + <i class="fe fe-alert-triangle mr-2" aria-hidden="true"></i> + This user is an Administrator and some items cannot be altered + </div> + <% } %> + + <div class="form-group"> + <label class="form-label">Item Visibility</label> + <div class="selectgroup w-100"> + <label class="selectgroup-item"> + <input type="radio" name="visibility" value="user" class="selectgroup-input"<%- getPerm('visibility') !== 'all' ? ' checked' : '' %>> + <span class="selectgroup-button">Created Items Only</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="visibility" value="all" class="selectgroup-input"<%- getPerm('visibility') === 'all' ? ' checked' : '' %>> + <span class="selectgroup-button">All Items</span> + </label> + </div> + </div> + </div> + + <div class="col-sm-12 col-md-12"> + <div class="form-group"> + <label class="form-label">Proxy Hosts</label> + <div class="selectgroup w-100"> + <label class="selectgroup-item"> + <input type="radio" name="proxy_hosts" value="manage" class="selectgroup-input" <%- getPermProps('proxy_hosts', 'manage', true) %>> + <span class="selectgroup-button">Manage</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="proxy_hosts" value="view" class="selectgroup-input" <%- getPermProps('proxy_hosts', 'view') %>> + <span class="selectgroup-button">View Only</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="proxy_hosts" value="hidden" class="selectgroup-input" <%- getPermProps('proxy_hosts', 'hidden') %>> + <span class="selectgroup-button">Hidden</span> + </label> + </div> + </div> + </div> + + <div class="col-sm-12 col-md-12"> + <div class="form-group"> + <label class="form-label">Redirection Hosts</label> + <div class="selectgroup w-100"> + <label class="selectgroup-item"> + <input type="radio" name="redirection_hosts" value="manage" class="selectgroup-input" <%- getPermProps('redirection_hosts', 'manage', true) %>> + <span class="selectgroup-button">Manage</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="redirection_hosts" value="view" class="selectgroup-input" <%- getPermProps('redirection_hosts', 'view') %>> + <span class="selectgroup-button">View Only</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="redirection_hosts" value="hidden" class="selectgroup-input" <%- getPermProps('redirection_hosts', 'hidden') %>> + <span class="selectgroup-button">Hidden</span> + </label> + </div> + </div> + </div> + + <div class="col-sm-12 col-md-12"> + <div class="form-group"> + <label class="form-label">404 Hosts</label> + <div class="selectgroup w-100"> + <label class="selectgroup-item"> + <input type="radio" name="dead_hosts" value="manage" class="selectgroup-input" <%- getPermProps('dead_hosts', 'manage', true) %>> + <span class="selectgroup-button">Manage</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="dead_hosts" value="view" class="selectgroup-input" <%- getPermProps('dead_hosts', 'view') %>> + <span class="selectgroup-button">View Only</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="dead_hosts" value="hidden" class="selectgroup-input" <%- getPermProps('dead_hosts', 'hidden') %>> + <span class="selectgroup-button">Hidden</span> + </label> + </div> + </div> + </div> + + <div class="col-sm-12 col-md-12"> + <div class="form-group"> + <label class="form-label">Streams</label> + <div class="selectgroup w-100"> + <label class="selectgroup-item"> + <input type="radio" name="streams" value="manage" class="selectgroup-input" <%- getPermProps('streams', 'manage', true) %>> + <span class="selectgroup-button">Manage</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="streams" value="view" class="selectgroup-input" <%- getPermProps('streams', 'view') %>> + <span class="selectgroup-button">View Only</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="streams" value="hidden" class="selectgroup-input" <%- getPermProps('streams', 'hidden') %>> + <span class="selectgroup-button">Hidden</span> + </label> + </div> + </div> + </div> + + <div class="col-sm-12 col-md-12"> + <div class="form-group"> + <label class="form-label">Access Lists</label> + <div class="selectgroup w-100"> + <label class="selectgroup-item"> + <input type="radio" name="access_lists" value="manage" class="selectgroup-input" <%- getPermProps('access_lists', 'manage', true) %>> + <span class="selectgroup-button">Manage</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="access_lists" value="view" class="selectgroup-input" <%- getPermProps('access_lists', 'view') %>> + <span class="selectgroup-button">View Only</span> + </label> + <label class="selectgroup-item"> + <input type="radio" name="access_lists" value="hidden" class="selectgroup-input" <%- getPermProps('access_lists', 'hidden') %>> + <span class="selectgroup-button">Hidden</span> + </label> + </div> + </div> + </div> + + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary cancel" data-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-teal save">Save</button> + </div> +</div> diff --git a/src/frontend/js/app/user/permissions.js b/src/frontend/js/app/user/permissions.js new file mode 100644 index 0000000..267059d --- /dev/null +++ b/src/frontend/js/app/user/permissions.js @@ -0,0 +1,99 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const template = require('./permissions.ejs'); +const Controller = require('../controller'); +const Cache = require('../cache'); +const Api = require('../api'); +const App = require('../main'); +const UserModel = require('../../models/user'); + +require('jquery-serializejson'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + ui: { + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save', + error: '.secret-error' + }, + + events: { + + 'click @ui.save': function (e) { + e.preventDefault(); + + let view = this; + let data = this.ui.form.serializeJSON(); + + // Manipulate + if (view.model.isAdmin()) { + // Force some attributes for admin + data = _.assign({}, data, { + access_lists: 'manage', + dead_hosts: 'manage', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + streams: 'manage' + }); + } + + this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + + Api.Users.setPermissions(view.model.get('id'), data) + .then(() => { + if (view.model.get('id') === Cache.User.get('id')) { + Cache.User.set({permissions: data}); + } + + view.model.set({permissions: data}); + App.UI.closeModal(); + }) + .catch(err => { + this.ui.error.text(err.message).show(); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + }, + + templateContext: function () { + let perms = this.model.get('permissions'); + let is_admin = this.model.isAdmin(); + + return { + getPerm: function (key) { + if (perms !== null && typeof perms[key] !== 'undefined') { + return perms[key]; + } + + return null; + }, + + getPermProps: function (key, item, forced_admin) { + if (forced_admin && is_admin) { + return 'checked disabled'; + } else if (is_admin) { + return 'disabled'; + } else if (perms !== null && typeof perms[key] !== 'undefined' && perms[key] === item) { + return 'checked'; + } + + return ''; + }, + + isAdmin: function () { + return is_admin; + } + }; + }, + + initialize: function (options) { + if (typeof options.model === 'undefined' || !options.model) { + this.model = new UserModel.Model(); + } + } +}); diff --git a/src/frontend/js/app/users/list/item.ejs b/src/frontend/js/app/users/list/item.ejs index 2b1e3dd..bd4d19e 100644 --- a/src/frontend/js/app/users/list/item.ejs +++ b/src/frontend/js/app/users/list/item.ejs @@ -19,8 +19,9 @@ <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 User</a> - <a href="#" class="set-password dropdown-item"><i class="dropdown-icon fe fe-shield"></i> Set Password</a> + <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> diff --git a/src/frontend/js/app/users/list/item.js b/src/frontend/js/app/users/list/item.js index 8b0994b..6766f08 100644 --- a/src/frontend/js/app/users/list/item.js +++ b/src/frontend/js/app/users/list/item.js @@ -12,10 +12,11 @@ module.exports = Mn.View.extend({ tagName: 'tr', ui: { - edit: 'a.edit-user', - password: 'a.set-password', - login: 'a.login', - delete: 'a.delete-user' + edit: 'a.edit-user', + permissions: 'a.edit-permissions', + password: 'a.set-password', + login: 'a.login', + delete: 'a.delete-user' }, events: { @@ -24,6 +25,11 @@ module.exports = Mn.View.extend({ 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); @@ -31,7 +37,7 @@ module.exports = Mn.View.extend({ 'click @ui.delete': function (e) { e.preventDefault(); - //Controller.showUserDeleteConfirm(this.model); + Controller.showUserDeleteConfirm(this.model); }, 'click @ui.login': function (e) { diff --git a/src/frontend/js/app/users/main.js b/src/frontend/js/app/users/main.js index 9f77fa1..548db8d 100644 --- a/src/frontend/js/app/users/main.js +++ b/src/frontend/js/app/users/main.js @@ -31,7 +31,7 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; - Api.Users.getAll() + Api.Users.getAll(['permissions']) .then(response => { if (!view.isDestroyed() && response && response.length) { view.showChildView('list_region', new ListView({ diff --git a/src/frontend/js/models/user.js b/src/frontend/js/models/user.js index 41250d3..5233941 100644 --- a/src/frontend/js/models/user.js +++ b/src/frontend/js/models/user.js @@ -12,7 +12,8 @@ const model = Backbone.Model.extend({ nickname: '', email: '', is_disabled: false, - roles: [] + roles: [], + permissions: null }; }, diff --git a/webpack.config.js b/webpack.config.js index 2732554..0e96aad 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,7 +24,8 @@ module.exports = { 'vector-map': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-2.0.3.min', 'vector-map-de': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-de-merc', 'vector-map-world': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-world-mill', - 'circle-progress': 'tabler-ui/dist/assets/js/vendors/circle-progress.min' + 'circle-progress': 'tabler-ui/dist/assets/js/vendors/circle-progress.min', + 'c3': 'tabler-ui/dist/assets/js/vendors/chart.bundle.min' } }, module: {