Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Edit permissions screen #14950

Merged
merged 12 commits into from
Jul 19, 2019
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