Skip to content

Commit

Permalink
[FIX] Edit permissions screen (#14950)
Browse files Browse the repository at this point in the history
* fix layout still missing js

* Change publication to REST call

* Add default fields to exclude

* Use fixed fields

* Deprecate publication instead of removing it

* ui fix

* fix review

* Deprecate publication instead of removing it

* Prevent adding invalid users to role
  • Loading branch information
ggazzo authored and sampaiodiego committed Jul 19, 2019
1 parent 250e398 commit 6a34189
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 97 deletions.
31 changes: 31 additions & 0 deletions app/api/server/v1/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Match, check } from 'meteor/check';

import { Roles } from '../../../models';
import { API } from '../api';
import { getUsersInRole, hasPermission } from '../../../authorization/server';

API.v1.addRoute('roles.list', { authRequired: true }, {
get() {
Expand Down Expand Up @@ -55,3 +56,33 @@ API.v1.addRoute('roles.addUserToRole', { authRequired: true }, {
});
},
});

API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, {
get() {
const { roomId, role } = this.queryParams;
const { offset, count = 50 } = this.getPaginationItems();

const fields = {
name: 1,
username: 1,
emails: 1,
};

if (!role) {
throw new Meteor.Error('error-param-not-provided', 'Query param "role" is required');
}
if (!hasPermission(this.userId, 'access-permissions')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}
if (roomId && !hasPermission(this.userId, 'view-other-user-channels')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}
const users = getUsersInRole(role, roomId, {
limit: count,
sort: { username: 1 },
skip: offset,
fields,
}).fetch();
return API.v1.success({ users });
},
});
5 changes: 5 additions & 0 deletions app/authorization/client/stylesheets/permissions.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
.permissions-manager {
display: flex;
flex-direction: column;

height: 100%;

&.page-container {
padding-bottom: 0 !important;
}
Expand Down
105 changes: 60 additions & 45 deletions app/authorization/client/views/permissionsRole.html
Original file line number Diff line number Diff line change
@@ -1,62 +1,75 @@
<template name="permissionsRole">
<div class="permissions-manager">
{{#if hasPermission}}
<a href="{{pathFor "admin-permissions"}}">{{_ "Back_to_permissions"}}</a>
<br>
<br>
{{#with role}}
<form id="form-role" class="inline form-role">
<label>{{_ "Role"}} :</label>
{{#if editing}}
<span>{{_id}}</span>
{{else}}
<input type="text" name="name" value="">
{{/if}}
<br>
<label>{{_ "Description"}} :</label>
<input type="text" name="description" value="{{description}}">
<br>
<label>{{_ "Scope"}} :</label>
<select name="scope" disabled="{{protected}}">
<option value="Users" selected="{{$eq scope 'Users'}}">{{_ "Global"}}</option>
<option value="Subscriptions" selected="{{$eq scope 'Subscriptions'}}">{{_ "Rooms"}}</option>
</select>
<form id="form-role" class="inline form-role form-inline">
<div class="form-group">

<br/>
<label for="mandatory2fa">{{_ "Users must use Two Factor Authentication"}} :</label>
<input type="checkbox" name="mandatory2fa" checked="{{mandatory2fa}}">
<div class="rc-input">
<div class="rc-input__title">{{_ "Role"}}</div>
{{#if editing}}
<input type="text" class="rc-input__element" name="name" autocomplete="off" value="{{_id}}" disabled>
{{else}}
<input type="text" class="rc-input__element" name="name" autocomplete="off">
{{/if}}
</div>
</div>
<div class="form-group">
<div class="rc-input">
<div class="rc-input__title">{{_ "Description"}}</div>
<input type="text" class="rc-input__element" name="description" autocomplete="off" value="{{description}}">
</div>
</div>
<div class="form-group">
<div class="rc-input__title">{{_ "Scope"}}</div>
<div class="rc-select">
<select name="scope" class="required rc-select__element" disabled="{{protected}}">
<option value="Users" selected="{{$eq scope 'Users'}}">{{_ "Global"}}</option>
<option value="Subscriptions" selected="{{$eq scope 'Subscriptions'}}">{{_ "Rooms"}}</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</div>
<div>
<label for="mandatory2fa">{{_ "Users must use Two Factor Authentication"}} :</label>
<input id="mandatory2fa" type="checkbox" name="mandatory2fa" checked="{{mandatory2fa}}">
</div>

<div class="form-buttons">
<div class="rc-button__group">
{{#if editable}}
<button name="delete" class="button danger delete-role">{{_ "Delete"}}</button>
<button name="delete" class="rc-button rc-button--danger delete-role">{{_ "Delete"}}</button>
{{/if}}
<button name="save" class="button primary save">{{_ "Save"}}</button>
<button name="save" class="rc-button rc-button--primary save">{{_ "Save"}}</button>
<a class="rc-button" href="{{pathFor "admin-permissions"}}">{{_ "Back_to_permissions"}}</a>
</div>
</form>
{{/with}}
{{#if editing}}
<h2 class="border-tertiary-background-color">{{_ "Users_in_role"}}</h2>
{{#if $eq role.scope 'Subscriptions'}}
<form id="form-search-room" class="inline">
<label>{{_ "Choose_a_room"}}</label>
{{> inputAutocomplete settings=autocompleteChannelSettings name="room" class="search" placeholder=(_ "Enter_a_room_name") autocomplete="off"}}
<label class="rc-input">
<div class="rc-input__title">{{_ "Choose_a_room"}}</div>
{{> inputAutocomplete settings=autocompleteChannelSettings name="room" class="search autocomplete rc-input__element" placeholder=(_ "Enter_a_room_name") autocomplete="off"}}
</label>
</form>
{{/if}}
{{#if $or ($eq role.scope 'Users') searchRoom}}
<form id="form-users" class="inline">
<label>{{_ "Add_user"}}</label>
{{> inputAutocomplete settings=autocompleteUsernameSettings name="username" class="search" placeholder=(_ "Enter_a_username") autocomplete="off"}}
<button name="add" class="button primary add">{{_ "Add"}}</button>
<label class="rc-input">
<div class="rc-input__title">{{_ "Add_user"}}</div>
{{> inputAutocomplete settings=autocompleteUsernameSettings name="username" class="search autocomplete rc-input__element" placeholder=(_ "Enter_a_username") autocomplete="off"}}
</label>
<button name="add" class="rc-button rc-button--primary add">{{_ "Add"}}</button>
</form>
<div class="list">
<table>
<div class="rc-table-content">
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
<thead>
<tr>
<th>&nbsp;</th>
<th width="34%">{{_ "Name"}}</th>
<th width="33%">{{_ "Username"}}</th>
<th width="33%">{{_ "Email"}}</th>
<th>&nbsp;</th>
<th width="30%"><div class="table-fake-th">{{_ "Name"}}</div></th>
<th width="25%"><div class="table-fake-th">{{_ "Username"}}</div></th>
<th width="25%"><div class="table-fake-th">{{_ "Email"}}</div></th>
<th width="5%">&nbsp;</th>
</tr>
</thead>
<tbody>
Expand All @@ -67,21 +80,23 @@ <h2 class="border-tertiary-background-color">{{_ "Users_in_role"}}</h2>
{{/unless}}
{{#each userInRole}}
<tr class="user-info" data-id="{{_id}}">
<td>
<div class="user-image status-{{status}}">
{{> avatar username=username}}
<td width="30%">
<div class="rc-table-wrapper">
<div class="rc-table-avatar">{{> avatar username=username}}</div>
<div class="rc-table-info">
<span class="rc-table-title">{{name}}</span>
</div>
</div>
</td>
<td>{{name}}</td>
<td>{{username}}</td>
<td>{{emailAddress}}</td>
<td><div class="rc-table-wrapper">{{username}}</div></td>
<td><div class="rc-table-wrapper">{{emailAddress}}</div></td>
<td><a href="#remove" class="remove-user"><i class="icon-block"></i></a></td>
</tr>
{{/each}}
</tbody>
</table>
{{/table}}
{{#if hasMore}}
<button class="button secondary load-more {{isLoading}}">{{_ "Load_more"}}</button>
<button class="rc-button rc-button--secondary load-more {{isLoading}}">{{_ "Load_more"}}</button>
{{/if}}
</div>
{{/if}}
Expand Down
122 changes: 71 additions & 51 deletions app/authorization/client/views/permissionsRole.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveDict } from 'meteor/reactive-dict';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
Expand All @@ -11,6 +12,32 @@ import { Roles } from '../../../models';
import { hasAllPermission } from '../hasPermission';
import { modal } from '../../../ui-utils/client/lib/modal';
import { SideNav } from '../../../ui-utils/client/lib/SideNav';
import { APIClient } from '../../../utils/client';
import { call } from '../../../ui-utils/client';

const PAGE_SIZE = 50;

const loadUsers = async (instance) => {
const offset = instance.state.get('offset');

const rid = instance.searchRoom.get();

const params = {
role: FlowRouter.getParam('name'),
offset,
count: PAGE_SIZE,
...rid && { roomId: rid },
};

instance.state.set('loading', true);
const { users } = await APIClient.v1.get('roles.getUsersInRole', params);

instance.usersInRole.set(instance.usersInRole.curValue.concat(users));
instance.state.set({
loading: false,
hasMore: users.length === PAGE_SIZE,
});
};

Template.permissionsRole.helpers({
role() {
Expand Down Expand Up @@ -46,19 +73,16 @@ Template.permissionsRole.helpers({
},

hasUsers() {
return Template.instance().usersInRole.get() && Template.instance().usersInRole.get().count() > 0;
return Template.instance().usersInRole.get().length > 0;
},

hasMore() {
const instance = Template.instance();
return instance.limit && instance.limit.get() <= instance.usersInRole.get().count();
return Template.instance().state.get('hasMore');
},

isLoading() {
const instance = Template.instance();
if (!instance.ready || !instance.ready.get()) {
return 'btn-loading';
}
return (!instance.subscription.ready() || instance.state.get('loading')) && 'btn-loading';
},

searchRoom() {
Expand Down Expand Up @@ -100,7 +124,7 @@ Template.permissionsRole.helpers({
noMatchTemplate: Template.userSearchEmpty,
matchAll: true,
filter: {
exceptions: instance.usersInRole.get() && instance.usersInRole.get().fetch(),
exceptions: instance.usersInRole.get(),
},
selector(match) {
return {
Expand All @@ -127,19 +151,15 @@ Template.permissionsRole.events({
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, () => {
Meteor.call('authorization:removeUserFromRole', FlowRouter.getParam('name'), this.username, instance.searchRoom.get(), function(error/* , result*/) {
if (error) {
return handleError(error);
}

modal.open({
title: t('Removed'),
text: t('User_removed'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
}, async () => {
await call('authorization:removeUserFromRole', FlowRouter.getParam('name'), this.username, instance.searchRoom.get());
instance.usersInRole.set(instance.usersInRole.curValue.filter((user) => user.username !== this.username));
modal.open({
title: t('Removed'),
text: t('User_removed'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
});
},
Expand Down Expand Up @@ -176,23 +196,26 @@ Template.permissionsRole.events({
});
},

'submit #form-users'(e, instance) {
async 'submit #form-users'(e, instance) {
e.preventDefault();
if (e.currentTarget.elements.username.value.trim() === '') {
return toastr.error(t('Please_fill_a_username'));
}
const oldBtnValue = e.currentTarget.elements.add.value;
e.currentTarget.elements.add.value = t('Saving');

Meteor.call('authorization:addUserToRole', FlowRouter.getParam('name'), e.currentTarget.elements.username.value, instance.searchRoom.get(), (error/* , result*/) => {
e.currentTarget.elements.add.value = oldBtnValue;
if (error) {
return handleError(error);
}
instance.subscribe('usersInRole', FlowRouter.getParam('name'), instance.searchRoom.get());
try {
await call('authorization:addUserToRole', FlowRouter.getParam('name'), e.currentTarget.elements.username.value, instance.searchRoom.get());
instance.usersInRole.set([]);
instance.state.set({
offset: 0,
cache: Date.now(),
});
toastr.success(t('User_added'));
e.currentTarget.reset();
});
} finally {
e.currentTarget.elements.add.value = oldBtnValue;
}
},

'submit #form-search-room'(e) {
Expand All @@ -215,43 +238,40 @@ Template.permissionsRole.events({
},

'click .load-more'(e, t) {
e.preventDefault();
e.stopPropagation();
t.limit.set(t.limit.get() + 50);
t.state.set('offset', t.state.get('offset') + PAGE_SIZE);
},

'autocompleteselect input[name=room]'(event, template, doc) {
template.searchRoom.set(doc._id);
},
});

Template.permissionsRole.onCreated(function() {
Template.permissionsRole.onCreated(async function() {
this.state = new ReactiveDict({
offset: 0,
loading: false,
hasMore: true,
cache: 0,
});
this.searchRoom = new ReactiveVar();
this.searchUsername = new ReactiveVar();
this.usersInRole = new ReactiveVar();
this.limit = new ReactiveVar(50);
this.ready = new ReactiveVar(true);
this.subscribe('roles', FlowRouter.getParam('name'));

this.autorun(() => {
if (this.searchRoom.get()) {
this.subscribe('roomSubscriptionsByRole', this.searchRoom.get(), FlowRouter.getParam('name'));
}
this.usersInRole = new ReactiveVar([]);

const limit = this.limit.get();
this.subscription = this.subscribe('roles', FlowRouter.getParam('name'));
});

const subscription = this.subscribe('usersInRole', FlowRouter.getParam('name'), this.searchRoom.get(), limit);
this.ready.set(subscription.ready());
Template.permissionsRole.onRendered(function() {
this.autorun(() => {
this.searchRoom.get();
this.usersInRole.set([]);
this.state.set({ offset: 0 });
});

this.usersInRole.set(Roles.findUsersInRole(FlowRouter.getParam('name'), this.searchRoom.get(), {
sort: {
username: 1,
},
}));
this.autorun(() => {
this.state.get('cache');
loadUsers(this);
});
});

Template.permissionsRole.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
Expand Down
Loading

0 comments on commit 6a34189

Please sign in to comment.