Refactor: Admin permissions page (#18932)
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>pull/18762/head
parent
ee1001af14
commit
aa6e674bb6
@ -1,39 +0,0 @@ |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { registerAdminRoute } from '../../../client/admin'; |
||||
import { t } from '../../utils/client'; |
||||
|
||||
registerAdminRoute('/permissions', { |
||||
name: 'admin-permissions', |
||||
async action(/* params*/) { |
||||
await import('./views'); |
||||
return BlazeLayout.render('main', { |
||||
center: 'permissions', |
||||
pageTitle: t('Permissions'), |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/permissions/:name?/edit', { |
||||
name: 'admin-permissions-edit', |
||||
async action(/* params*/) { |
||||
await import('./views'); |
||||
return BlazeLayout.render('main', { |
||||
center: 'pageContainer', |
||||
pageTitle: t('Role_Editing'), |
||||
pageTemplate: 'permissionsRole', |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/permissions/new', { |
||||
name: 'admin-permissions-new', |
||||
async action(/* params*/) { |
||||
await import('./views'); |
||||
return BlazeLayout.render('main', { |
||||
center: 'pageContainer', |
||||
pageTitle: t('Role_Editing'), |
||||
pageTemplate: 'permissionsRole', |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,122 +0,0 @@ |
||||
.permissions-manager { |
||||
display: flex; |
||||
flex-direction: column; |
||||
|
||||
height: 100%; |
||||
|
||||
&.page-container { |
||||
padding-bottom: 0 !important; |
||||
} |
||||
|
||||
.permission-edit { |
||||
display: flex; |
||||
flex-direction: column; |
||||
|
||||
height: 100%; |
||||
|
||||
padding: 10px; |
||||
align-items: center; |
||||
} |
||||
|
||||
.permission-label, |
||||
.permission-icon { |
||||
display: flex; |
||||
|
||||
align-items: flex-end; |
||||
flex-grow: 1; |
||||
} |
||||
|
||||
.permission-icon { |
||||
width: 30px; |
||||
|
||||
margin-bottom: 0; |
||||
flex-grow: 0; |
||||
} |
||||
|
||||
.content { |
||||
padding: 0 !important; |
||||
} |
||||
|
||||
.permission-grid { |
||||
overflow-x: scroll; |
||||
|
||||
table-layout: fixed; |
||||
|
||||
border-collapse: collapse; |
||||
|
||||
.id-styler { |
||||
white-space: nowrap; |
||||
|
||||
color: #7f7f7f; |
||||
|
||||
font-size: smaller; |
||||
} |
||||
|
||||
.edit-icon.role-name-edit-icon { |
||||
height: 30px; |
||||
} |
||||
|
||||
.role-name { |
||||
position: sticky; |
||||
|
||||
top: 0; |
||||
|
||||
width: 70px; |
||||
|
||||
text-align: left; |
||||
|
||||
vertical-align: middle; |
||||
|
||||
background: white; |
||||
} |
||||
|
||||
.role-name-edit-icon { |
||||
width: 70px; |
||||
height: 70px; |
||||
|
||||
text-align: center; |
||||
|
||||
vertical-align: middle; |
||||
} |
||||
|
||||
.rotator { |
||||
overflow: hidden; |
||||
|
||||
width: 30px; |
||||
height: 130px; |
||||
|
||||
padding: 10px 0; |
||||
|
||||
transform: rotate(-180deg); |
||||
|
||||
white-space: nowrap; |
||||
|
||||
text-overflow: ellipsis; |
||||
writing-mode: vertical-rl; |
||||
} |
||||
|
||||
.admin-table-row { |
||||
height: 50px; |
||||
} |
||||
|
||||
td { |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.permission-name { |
||||
width: 25%; |
||||
padding-left: 14px; |
||||
|
||||
vertical-align: middle; |
||||
} |
||||
|
||||
.permission-checkbox { |
||||
text-align: center; |
||||
vertical-align: middle; |
||||
} |
||||
|
||||
.icon-edit { |
||||
font-size: 1.5em; |
||||
} |
||||
} |
||||
} |
||||
@ -1,4 +0,0 @@ |
||||
import './permissions.html'; |
||||
import './permissions'; |
||||
import './permissionsRole.html'; |
||||
import './permissionsRole'; |
||||
@ -1,80 +0,0 @@ |
||||
<template name="permissionsTable"> |
||||
<div class="content"> |
||||
<table class="permission-grid secondary-background-color"> |
||||
|
||||
<thead class="content-background-color"> |
||||
<tr> |
||||
<th class=" role-name permission-name border-component-color"></th> |
||||
{{#each role in allRoles}} |
||||
<th class="edit-icon role-name border-component-color" title="{{role.description}}"> |
||||
<a class="permission-edit" href="{{pathFor "admin-permissions-edit" name=role._id}}"> |
||||
<p class="permission-label"> |
||||
<div class = "rotator"> |
||||
{{role._id}} |
||||
</div> |
||||
</p> |
||||
<p class="permission-icon"> |
||||
<i class="icon-edit"></i> |
||||
</p> |
||||
</a> |
||||
</th> |
||||
{{/each}} |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each permission in permissions}} |
||||
<tr class="admin-table-row"> |
||||
<td class="permission-name border-component-color" title="{{permissionDescription permission}}">{{permissionName permission}}<br><span class = "id-styler">[ID: {{permission._id}}]</span></td> |
||||
{{#each role in allRoles}} |
||||
<td class="permission-checkbox border-component-color"> |
||||
{{#if isRolePermissionEnabled role permission}} |
||||
<input type="checkbox" name="perm[{{role._id}}][{{permission._id}}]" class="role-permission" value="1" checked="{{granted permission.roles role}}" data-role="{{role._id}}" data-permission="{{permission._id}}"> |
||||
{{/if}} |
||||
</td> |
||||
{{else}} |
||||
<tr class="table-no-click"> |
||||
<td>{{_ "No_results_found_for"}} {{filter}}</td> |
||||
</tr> |
||||
{{/each}} |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
</template> |
||||
<template name="permissions"> |
||||
<div class="page-container permissions-manager"> |
||||
<section class="permissions-manager page-container page-home page-static page-list"> |
||||
{{#if hasPermission}} |
||||
{{# header sectionName=pageTitle}} |
||||
<div class="rc-header__section-button"> |
||||
<a href="{{pathFor"admin-permissions-new"}}" |
||||
class="rc-button rc-button--primary new-role">{{_ "New_role"}}</a> |
||||
</div> |
||||
{{/header}} |
||||
<form class="search-form" role="form"> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon="magnifier" }} |
||||
</div> |
||||
<input id="permissions-filter" type="text" class="rc-input__element" placeholder="{{_ " Search "}}" autofocus dir="auto"> |
||||
</div> |
||||
</form> |
||||
|
||||
{{>tabs tabs=tabsData}} |
||||
{{#if $eq tab 'settings'}} |
||||
{{> permissionsTable query=filter permissions=settingPermissions allRoles=roles collection='Setting'}} |
||||
{{else}} |
||||
{{> permissionsTable query=filter permissions=permissions allRoles=roles collection='Chat'}} |
||||
{{/if}} |
||||
{{/if}} |
||||
{{#if hasNoPermission}} |
||||
<div class="content"> |
||||
{{#if i18nPageTitle}} {{> header sectionName=i18nPageTitle}} {{else}} {{> header sectionName=pageTitle}} |
||||
{{/if}} |
||||
{{_ "Not_authorized"}} |
||||
</div> |
||||
{{/if}} |
||||
</section> |
||||
</div> |
||||
</template> |
||||
@ -1,205 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import _ from 'underscore'; |
||||
import s from 'underscore.string'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { Roles } from '../../../models/client'; |
||||
import { ChatPermissions } from '../lib/ChatPermissions'; |
||||
import { hasAllPermission } from '../hasPermission'; |
||||
import { t } from '../../../utils/client'; |
||||
import { SideNav } from '../../../ui-utils/client/lib/SideNav'; |
||||
import { CONSTANTS, AuthorizationUtils } from '../../lib'; |
||||
import { hasAtLeastOnePermission } from '..'; |
||||
|
||||
Template.permissions.helpers({ |
||||
tabsData() { |
||||
const { |
||||
state, |
||||
} = Template.instance(); |
||||
|
||||
const permissionsTab = { |
||||
label: t('Permissions'), |
||||
value: 'permissions', |
||||
condition() { |
||||
return true; |
||||
}, |
||||
}; |
||||
|
||||
const settingsTab = { |
||||
label: t('Settings'), |
||||
value: 'settings', |
||||
condition() { |
||||
return true; |
||||
}, |
||||
}; |
||||
|
||||
const tabs = [permissionsTab]; |
||||
|
||||
const settingsPermissions = hasAllPermission('access-setting-permissions'); |
||||
|
||||
if (settingsPermissions) { |
||||
tabs.push(settingsTab); |
||||
} |
||||
switch (settingsPermissions && state.get('tab')) { |
||||
case 'settings': |
||||
settingsTab.active = true; |
||||
break; |
||||
case 'permissions': |
||||
permissionsTab.active = true; |
||||
break; |
||||
default: |
||||
permissionsTab.active = true; |
||||
} |
||||
|
||||
|
||||
return { |
||||
tabs, |
||||
onChange(value) { |
||||
state.set({ |
||||
tab: value, |
||||
size: 50, |
||||
}); |
||||
}, |
||||
}; |
||||
}, |
||||
roles() { |
||||
return Roles.find(); |
||||
}, |
||||
|
||||
permissions() { |
||||
const { state } = Template.instance(); |
||||
const limit = state.get('size'); |
||||
const filter = new RegExp(s.escapeRegExp(state.get('filter')), 'i'); |
||||
|
||||
return ChatPermissions.find( |
||||
{ |
||||
level: { $ne: CONSTANTS.SETTINGS_LEVEL }, |
||||
_id: filter, |
||||
}, |
||||
{ |
||||
sort: { |
||||
_id: 1, |
||||
}, |
||||
limit, |
||||
}, |
||||
); |
||||
}, |
||||
|
||||
settingPermissions() { |
||||
const { state } = Template.instance(); |
||||
const limit = state.get('size'); |
||||
const filter = new RegExp(s.escapeRegExp(state.get('filter')), 'i'); |
||||
return ChatPermissions.find( |
||||
{ |
||||
_id: filter, |
||||
level: CONSTANTS.SETTINGS_LEVEL, |
||||
group: { $exists: true }, |
||||
}, |
||||
{ |
||||
limit, |
||||
sort: { |
||||
group: 1, |
||||
section: 1, |
||||
}, |
||||
}, |
||||
); |
||||
}, |
||||
|
||||
hasPermission() { |
||||
return hasAllPermission('access-permissions'); |
||||
}, |
||||
|
||||
hasNoPermission() { |
||||
return !hasAtLeastOnePermission([ |
||||
'access-permissions', |
||||
'access-setting-permissions', |
||||
]); |
||||
}, |
||||
filter() { |
||||
return Template.instance().state.get('filter'); |
||||
}, |
||||
|
||||
tab() { |
||||
return Template.instance().state.get('tab'); |
||||
}, |
||||
}); |
||||
|
||||
Template.permissions.events({ |
||||
'keyup #permissions-filter'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.state.set('filter', e.currentTarget.value); |
||||
}, |
||||
'scroll .content': _.throttle(({ currentTarget }, i) => { |
||||
if ( |
||||
currentTarget.offsetHeight + currentTarget.scrollTop |
||||
>= currentTarget.scrollHeight - 100 |
||||
) { |
||||
return i.state.set('size', i.state.get('size') + 50); |
||||
} |
||||
}, 300), |
||||
}); |
||||
|
||||
Template.permissions.onCreated(function() { |
||||
this.state = new ReactiveDict({ |
||||
filter: '', |
||||
tab: '', |
||||
size: 50, |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
this.state.get('filter'); |
||||
this.state.set('size', 50); |
||||
}); |
||||
}); |
||||
|
||||
Template.permissionsTable.helpers({ |
||||
granted(roles, role) { |
||||
return (roles && ~roles.indexOf(role._id) && 'checked') || null; |
||||
}, |
||||
|
||||
permissionName(permission) { |
||||
if (permission.level === CONSTANTS.SETTINGS_LEVEL) { |
||||
let path = ''; |
||||
if (permission.group) { |
||||
path = `${ t(permission.group) } > `; |
||||
} |
||||
if (permission.section) { |
||||
path = `${ path }${ t(permission.section) } > `; |
||||
} |
||||
return `${ path }${ t(permission.settingId) }`; |
||||
} |
||||
|
||||
return t(permission._id); |
||||
}, |
||||
|
||||
permissionDescription(permission) { |
||||
return t(`${ permission._id }_description`); |
||||
}, |
||||
|
||||
isRolePermissionEnabled(role, permission) { |
||||
return !AuthorizationUtils.isPermissionRestrictedForRole(permission._id, role._id); |
||||
}, |
||||
}); |
||||
|
||||
Template.permissionsTable.events({ |
||||
'click .role-permission'(e) { |
||||
const permissionId = e.currentTarget.getAttribute('data-permission'); |
||||
const role = e.currentTarget.getAttribute('data-role'); |
||||
|
||||
const permission = permissionId && ChatPermissions.findOne(permissionId); |
||||
|
||||
const action = ~permission.roles.indexOf(role) ? 'authorization:removeRoleFromPermission' : 'authorization:addPermissionToRole'; |
||||
|
||||
return Meteor.call(action, permissionId, role); |
||||
}, |
||||
}); |
||||
|
||||
Template.permissions.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -1,110 +0,0 @@ |
||||
<template name="permissionsRole"> |
||||
<div class="permissions-manager"> |
||||
{{#if hasPermission}} |
||||
{{#with role}} |
||||
<form id="form-role" class="inline form-role form-inline"> |
||||
<div class="form-group"> |
||||
|
||||
<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="alert alert-warning"> |
||||
<p for="mandatory2fa">{{_ "Leave the description field blank if you dont want to show the role"}}.</p> |
||||
</div> |
||||
<div class="rc-button__group"> |
||||
{{#if editable}} |
||||
<button name="delete" class="rc-button rc-button--danger delete-role">{{_ "Delete"}}</button> |
||||
{{/if}} |
||||
<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 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 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="rc-table-content"> |
||||
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}} |
||||
<thead> |
||||
<tr> |
||||
<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%"> </th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#unless hasUsers}} |
||||
<tr> |
||||
<td colspan="5" class="empty-role">{{_ "There_are_no_users_in_this_role"}}</td> |
||||
</tr> |
||||
{{/unless}} |
||||
{{#each userInRole}} |
||||
<tr class="user-info" data-id="{{_id}}"> |
||||
<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><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}} |
||||
{{#if hasMore}} |
||||
<button class="rc-button rc-button--secondary load-more {{isLoading}}">{{_ "Load_more"}}</button> |
||||
{{/if}} |
||||
</div> |
||||
{{/if}} |
||||
{{/if}} |
||||
{{else}} |
||||
{{_ "Not_authorized"}} |
||||
{{/if}} |
||||
</div> |
||||
</template> |
||||
@ -1,277 +0,0 @@ |
||||
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'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { handleError } from '../../../utils/client/lib/handleError'; |
||||
import { t } from '../../../utils/lib/tapi18n'; |
||||
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() { |
||||
return Roles.findOne({ |
||||
_id: FlowRouter.getParam('name'), |
||||
}) || {}; |
||||
}, |
||||
|
||||
userInRole() { |
||||
return Template.instance().usersInRole.get(); |
||||
}, |
||||
|
||||
editing() { |
||||
return FlowRouter.getParam('name') != null; |
||||
}, |
||||
|
||||
emailAddress() { |
||||
if (this.emails && this.emails.length > 0) { |
||||
return this.emails[0].address; |
||||
} |
||||
}, |
||||
|
||||
hasPermission() { |
||||
return hasAllPermission('access-permissions'); |
||||
}, |
||||
|
||||
protected() { |
||||
return this.protected; |
||||
}, |
||||
|
||||
editable() { |
||||
return this._id && !this.protected; |
||||
}, |
||||
|
||||
hasUsers() { |
||||
return Template.instance().usersInRole.get().length > 0; |
||||
}, |
||||
|
||||
hasMore() { |
||||
return Template.instance().state.get('hasMore'); |
||||
}, |
||||
|
||||
isLoading() { |
||||
const instance = Template.instance(); |
||||
return (!instance.subscription.ready() || instance.state.get('loading')) && 'btn-loading'; |
||||
}, |
||||
|
||||
searchRoom() { |
||||
return Template.instance().searchRoom.get(); |
||||
}, |
||||
|
||||
autocompleteChannelSettings() { |
||||
return { |
||||
limit: 10, |
||||
rules: [ |
||||
{ |
||||
collection: 'CachedChannelList', |
||||
endpoint: 'rooms.autocomplete.channelAndPrivate', |
||||
field: 'name', |
||||
template: Template.roomSearch, |
||||
noMatchTemplate: Template.roomSearchEmpty, |
||||
matchAll: true, |
||||
sort: 'name', |
||||
selector(match) { |
||||
return { |
||||
name: match, |
||||
}; |
||||
}, |
||||
}, |
||||
], |
||||
}; |
||||
}, |
||||
|
||||
autocompleteUsernameSettings() { |
||||
const instance = Template.instance(); |
||||
return { |
||||
limit: 10, |
||||
rules: [ |
||||
{ |
||||
collection: 'CachedUserList', |
||||
endpoint: 'users.autocomplete', |
||||
field: 'username', |
||||
template: Template.userSearch, |
||||
noMatchTemplate: Template.userSearchEmpty, |
||||
matchAll: true, |
||||
filter: { |
||||
exceptions: instance.usersInRole.get(), |
||||
}, |
||||
selector(match) { |
||||
return { |
||||
term: match, |
||||
}; |
||||
}, |
||||
sort: 'username', |
||||
}, |
||||
], |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.permissionsRole.events({ |
||||
async 'click .remove-user'(e, instance) { |
||||
e.preventDefault(); |
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('The_user_s_will_be_removed_from_role_s', this.username, FlowRouter.getParam('name')), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: 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, |
||||
}); |
||||
}); |
||||
}, |
||||
|
||||
'submit #form-role'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
const oldBtnValue = e.currentTarget.elements.save.value; |
||||
e.currentTarget.elements.save.value = t('Saving'); |
||||
const roleData = { |
||||
description: e.currentTarget.elements.description.value, |
||||
scope: e.currentTarget.elements.scope.value, |
||||
mandatory2fa: e.currentTarget.elements.mandatory2fa.checked, |
||||
}; |
||||
|
||||
if (this._id) { |
||||
roleData.name = this._id; |
||||
} else { |
||||
roleData.name = e.currentTarget.elements.name.value; |
||||
} |
||||
|
||||
Meteor.call('authorization:saveRole', roleData, (error/* , result*/) => { |
||||
e.currentTarget.elements.save.value = oldBtnValue; |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
|
||||
toastr.success(t('Saved')); |
||||
|
||||
if (!this._id) { |
||||
return FlowRouter.go('admin-permissions-edit', { |
||||
name: roleData.name, |
||||
}); |
||||
} |
||||
}); |
||||
}, |
||||
|
||||
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'); |
||||
|
||||
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) { |
||||
return e.preventDefault(); |
||||
}, |
||||
|
||||
'click .delete-role'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
if (this.protected) { |
||||
return toastr.error(t('error-delete-protected-role')); |
||||
} |
||||
|
||||
Meteor.call('authorization:deleteRole', this._id, function(error/* , result*/) { |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
toastr.success(t('Role_removed')); |
||||
FlowRouter.go('admin-permissions'); |
||||
}); |
||||
}, |
||||
|
||||
'click .load-more'(e, t) { |
||||
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(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([]); |
||||
}); |
||||
|
||||
Template.permissionsRole.onRendered(function() { |
||||
this.autorun(() => { |
||||
this.searchRoom.get(); |
||||
this.usersInRole.set([]); |
||||
this.state.set({ offset: 0 }); |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
this.state.get('cache'); |
||||
loadUsers(this); |
||||
}); |
||||
|
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,90 @@ |
||||
import React from 'react'; |
||||
import { Box, Field, FieldGroup, Button, Margins, Callout } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import RoleForm from './RoleForm'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useRole } from './useRole'; |
||||
|
||||
const EditRolePageContainer = ({ _id }) => { |
||||
const t = useTranslation(); |
||||
const role = useRole(_id); |
||||
|
||||
if (!role) { |
||||
return <Callout type='danger'>{t('error-invalid-role')}</Callout>; |
||||
} |
||||
|
||||
return <EditRolePage key={_id} data={role} />; |
||||
}; |
||||
|
||||
const EditRolePage = ({ data }) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const usersInRoleRouter = useRoute('admin-permissions'); |
||||
const router = useRoute('admin-permissions'); |
||||
|
||||
const { values, handlers } = useForm({ |
||||
name: data.name, |
||||
description: data.description || '', |
||||
scope: data.scope || 'Users', |
||||
mandatory2fa: !!data.mandatory2fa, |
||||
}); |
||||
|
||||
const saveRole = useMethod('authorization:saveRole'); |
||||
const deleteRole = useMethod('authorization:deleteRole'); |
||||
|
||||
const handleManageUsers = useMutableCallback(() => { |
||||
usersInRoleRouter.push({ |
||||
context: 'users-in-role', |
||||
_id: data.name, |
||||
}); |
||||
}); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
try { |
||||
await saveRole(values); |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleDelete = useMutableCallback(async () => { |
||||
try { |
||||
await deleteRole(data.name); |
||||
dispatchToastMessage({ type: 'success', message: t('Role_removed') }); |
||||
router.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
return <Box w='full' alignSelf='center' mb='neg-x8'> |
||||
<Margins block='x8'> |
||||
<FieldGroup> |
||||
<RoleForm values={values} handlers={handlers} editing isProtected={data.protected}/> |
||||
<Field> |
||||
<Field.Row> |
||||
<Button primary w='full' onClick={handleSave}>{t('Save')}</Button> |
||||
</Field.Row> |
||||
</Field> |
||||
{!data.protected && <Field> |
||||
<Field.Row> |
||||
<Button danger w='full' onClick={handleDelete}>{t('Delete')}</Button> |
||||
</Field.Row> |
||||
</Field>} |
||||
<Field> |
||||
<Field.Row> |
||||
<Button w='full' onClick={handleManageUsers}>{t('Users_in_role')}</Button> |
||||
</Field.Row> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Margins> |
||||
</Box>; |
||||
}; |
||||
|
||||
export default EditRolePageContainer; |
||||
@ -0,0 +1,46 @@ |
||||
import React from 'react'; |
||||
import { Box, FieldGroup, ButtonGroup, Button, Margins } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import RoleForm from './RoleForm'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
|
||||
|
||||
const NewRolePage = () => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const { values, handlers } = useForm({ |
||||
name: '', |
||||
description: '', |
||||
scope: 'Users', |
||||
mandatory2fa: false, |
||||
}); |
||||
|
||||
const saveRole = useMethod('authorization:saveRole'); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
try { |
||||
await saveRole(values); |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
return <Box w='full' alignSelf='center' mb='neg-x8'> |
||||
<Margins block='x8'> |
||||
<FieldGroup> |
||||
<RoleForm values={values} handlers={handlers}/> |
||||
</FieldGroup> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button primary onClick={handleSave}>{t('Save')}</Button> |
||||
</ButtonGroup> |
||||
</Margins> |
||||
</Box>; |
||||
}; |
||||
|
||||
export default NewRolePage; |
||||
@ -0,0 +1,34 @@ |
||||
import React from 'react'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useRouteParameter, useRoute } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
import NewRolePage from './NewRolePage'; |
||||
import EditRolePage from './EditRolePage'; |
||||
|
||||
const PermissionsContextBar = () => { |
||||
const t = useTranslation(); |
||||
const _id = useRouteParameter('_id'); |
||||
const context = useRouteParameter('context'); |
||||
|
||||
const router = useRoute('admin-permissions'); |
||||
|
||||
const handleVerticalBarCloseButton = useMutableCallback(() => { |
||||
router.push({}); |
||||
}); |
||||
|
||||
return (context && <VerticalBar className={'contextual-bar'}> |
||||
<VerticalBar.Header> |
||||
{context === 'new' && t('New_role')} |
||||
{context === 'edit' && t('Role_Editing')} |
||||
<VerticalBar.Close onClick={handleVerticalBarCloseButton} /> |
||||
</VerticalBar.Header> |
||||
<VerticalBar.ScrollableContent> |
||||
{context === 'new' && <NewRolePage />} |
||||
{context === 'edit' && <EditRolePage _id={_id} />} |
||||
</VerticalBar.ScrollableContent> |
||||
</VerticalBar>) || null; |
||||
}; |
||||
|
||||
export default PermissionsContextBar; |
||||
@ -0,0 +1,16 @@ |
||||
import React from 'react'; |
||||
|
||||
import { useRouteParameter } from '../../contexts/RouterContext'; |
||||
import UsersInRole from './UsersInRole'; |
||||
import PermissionsTable from './PermissionsTable'; |
||||
|
||||
const PermissionsRouter = () => { |
||||
const context = useRouteParameter('context'); |
||||
if (context === 'users-in-role') { |
||||
return <UsersInRole />; |
||||
} |
||||
|
||||
return <PermissionsTable />; |
||||
}; |
||||
|
||||
export default PermissionsRouter; |
||||
@ -0,0 +1,261 @@ |
||||
import React, { useState, useCallback, useEffect } from 'react'; |
||||
import { TextInput, Table, Margins, Box, Icon, CheckBox, Throbber, Tabs, Button } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import PermissionsContextBar from './PermissionsContextBar'; |
||||
import { GenericTable } from '../../components/GenericTable'; |
||||
import { useReactiveValue } from '../../hooks/useReactiveValue'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { ChatPermissions } from '../../../app/authorization/client/lib/ChatPermissions'; |
||||
import { CONSTANTS, AuthorizationUtils } from '../../../app/authorization/lib'; |
||||
import { Roles } from '../../../app/models/client'; |
||||
|
||||
const useChangeRole = ({ onGrant, onRemove, permissionId }) => { |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
return useMutableCallback(async (roleId, granted) => { |
||||
try { |
||||
if (granted) { |
||||
await onRemove(permissionId, roleId); |
||||
} else { |
||||
await onGrant(permissionId, roleId); |
||||
} |
||||
return !granted; |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
return granted; |
||||
}); |
||||
}; |
||||
|
||||
|
||||
const usePermissionsAndRoles = (type = 'permissions', filter = '', limit = 25, skip = 0) => { |
||||
const getPermissions = useCallback(() => { |
||||
const filterRegExp = new RegExp(filter, 'i'); |
||||
|
||||
return ChatPermissions.find( |
||||
{ |
||||
level: type === 'permissions' ? { $ne: CONSTANTS.SETTINGS_LEVEL } : CONSTANTS.SETTINGS_LEVEL, |
||||
_id: filterRegExp, |
||||
}, |
||||
{ |
||||
sort: { |
||||
_id: 1, |
||||
}, |
||||
skip, |
||||
limit, |
||||
}, |
||||
); |
||||
}, [filter, limit, skip, type]); |
||||
|
||||
const getRoles = useMutableCallback(() => Roles.find().fetch(), []); |
||||
|
||||
const permissions = useReactiveValue(getPermissions); |
||||
const roles = useReactiveValue(getRoles); |
||||
|
||||
return [permissions.fetch(), permissions.count(false), roles]; |
||||
}; |
||||
|
||||
const RoleCell = React.memo(({ grantedRoles = [], _id, description, onChange, lineHovered, permissionId }) => { |
||||
const [granted, setGranted] = useState(() => !!grantedRoles.includes(_id)); |
||||
const [loading, setLoading] = useState(false); |
||||
|
||||
const isRestrictedForRole = AuthorizationUtils.isPermissionRestrictedForRole(permissionId, _id); |
||||
|
||||
const handleChange = useMutableCallback(async () => { |
||||
setLoading(true); |
||||
const result = await onChange(_id, granted); |
||||
setGranted(result); |
||||
setLoading(false); |
||||
}); |
||||
|
||||
const isDisabled = !!loading || !!isRestrictedForRole; |
||||
|
||||
return <Table.Cell withTruncatedText> |
||||
<Margins inline='x2'> |
||||
<CheckBox checked={granted} onChange={handleChange} disabled={isDisabled}/> |
||||
{!loading && <Box display='inline' color='hint' invisible={!lineHovered}> |
||||
{description || _id} |
||||
</Box>} |
||||
{loading && <Throbber size='x12' display='inline-block'/>} |
||||
</Margins> |
||||
</Table.Cell>; |
||||
}); |
||||
|
||||
const getName = (t, permission) => { |
||||
if (permission.level === CONSTANTS.SETTINGS_LEVEL) { |
||||
let path = ''; |
||||
if (permission.group) { |
||||
path = `${ t(permission.group) } > `; |
||||
} |
||||
if (permission.section) { |
||||
path = `${ path }${ t(permission.section) } > `; |
||||
} |
||||
return `${ path }${ t(permission.settingId) }`; |
||||
} |
||||
|
||||
return t(permission._id); |
||||
}; |
||||
|
||||
const PermissionRow = React.memo(({ permission, t, roleList, onGrant, onRemove, ...props }) => { |
||||
const { |
||||
_id, |
||||
roles, |
||||
} = permission; |
||||
|
||||
const [hovered, setHovered] = useState(false); |
||||
|
||||
const onMouseEnter = useMutableCallback(() => setHovered(true)); |
||||
const onMouseLeave = useMutableCallback(() => setHovered(false)); |
||||
|
||||
const changeRole = useChangeRole({ onGrant, onRemove, permissionId: _id }); |
||||
return <Table.Row |
||||
key={_id} |
||||
role='link' |
||||
action |
||||
tabIndex={0} |
||||
onMouseEnter={onMouseEnter} |
||||
onMouseLeave={onMouseLeave} |
||||
{...props} |
||||
> |
||||
<Table.Cell maxWidth='x300' withTruncatedText title={t(`${ _id }_description`)}>{getName(t, permission)}</Table.Cell> |
||||
{roleList.map(({ _id, description }) => <RoleCell |
||||
key={_id} |
||||
_id={_id} |
||||
description={description} |
||||
grantedRoles={roles} |
||||
onChange={changeRole} |
||||
lineHovered={hovered} |
||||
permissionId={_id} |
||||
/>)} |
||||
</Table.Row>; |
||||
}); |
||||
|
||||
const RoleHeader = React.memo(({ router, _id, description, ...props }) => { |
||||
const onClick = useMutableCallback(() => { |
||||
router.push({ |
||||
context: 'edit', |
||||
_id, |
||||
}); |
||||
}); |
||||
|
||||
return <GenericTable.HeaderCell clickable pi='x4' p='x8' onClick={onClick} {...props}> |
||||
<Box |
||||
className={css`white-space: nowrap`} |
||||
pb='x8' |
||||
pi='x12' |
||||
mi='neg-x2' |
||||
borderStyle='solid' |
||||
borderWidth='x2' |
||||
borderRadius='x2' |
||||
borderColor='neutral-300' |
||||
> |
||||
<Margins inline='x2'> |
||||
<span>{description || _id}</span> |
||||
<Icon name='edit' size='x16'/> |
||||
</Margins> |
||||
</Box> |
||||
</GenericTable.HeaderCell>; |
||||
}); |
||||
|
||||
const FilterComponent = ({ onChange }) => { |
||||
const t = useTranslation(); |
||||
const [filter, setFilter] = useState(''); |
||||
|
||||
const debouncedFilter = useDebouncedValue(filter, 500); |
||||
|
||||
useEffect(() => { |
||||
onChange(debouncedFilter); |
||||
}, [debouncedFilter, onChange]); |
||||
|
||||
const handleFilter = useMutableCallback(({ currentTarget: { value } }) => { |
||||
setFilter(value); |
||||
}); |
||||
|
||||
return <TextInput value={filter} onChange={handleFilter} placeholder={t('Search')} flexGrow={0}/>; |
||||
}; |
||||
|
||||
const PermissionsTable = () => { |
||||
const t = useTranslation(); |
||||
const [filter, setFilter] = useState(''); |
||||
const [type, setType] = useState('permissions'); |
||||
const [params, setParams] = useState({ limit: 25, skip: 0 }); |
||||
|
||||
const router = useRoute('admin-permissions'); |
||||
|
||||
const grantRole = useMethod('authorization:addPermissionToRole'); |
||||
const removeRole = useMethod('authorization:removeRoleFromPermission'); |
||||
|
||||
const permissionsData = usePermissionsAndRoles(type, filter, params.limit, params.skip); |
||||
|
||||
const [ |
||||
permissions, |
||||
total, |
||||
roleList, |
||||
] = permissionsData; |
||||
|
||||
const handleParams = useMutableCallback(({ current, itemsPerPage }) => { |
||||
setParams({ skip: current, limit: itemsPerPage }); |
||||
}); |
||||
|
||||
const handlePermissionsTab = useMutableCallback(() => { |
||||
if (type === 'permissions') { return; } |
||||
setType('permissions'); |
||||
}); |
||||
|
||||
const handleSettingsTab = useMutableCallback(() => { |
||||
if (type === 'settings') { return; } |
||||
setType('settings'); |
||||
}); |
||||
|
||||
const handleAdd = useMutableCallback(() => { |
||||
router.push({ |
||||
context: 'new', |
||||
}); |
||||
}); |
||||
|
||||
return <Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={t('Permissions')}> |
||||
<Button small square onClick={handleAdd}> |
||||
<Icon name='plus'/> |
||||
</Button> |
||||
</Page.Header> |
||||
<Margins blockEnd='x8'> |
||||
<Tabs> |
||||
<Tabs.Item selected={type === 'permissions'} onClick={handlePermissionsTab}> |
||||
{t('Permissions')} |
||||
</Tabs.Item> |
||||
<Tabs.Item selected={type === 'settings'} onClick={handleSettingsTab}> |
||||
{t('Settings')} |
||||
</Tabs.Item> |
||||
</Tabs> |
||||
</Margins> |
||||
<Page.Content mb='neg-x8'> |
||||
<Margins block='x8'> |
||||
<FilterComponent onChange={setFilter}/> |
||||
<GenericTable |
||||
header={<> |
||||
<GenericTable.HeaderCell width='x120'>{t('Name')}</GenericTable.HeaderCell> |
||||
{roleList.map(({ _id, description }) => <RoleHeader key={_id} _id={_id} description={description} router={router}/>)} |
||||
</>} |
||||
total={total} |
||||
results={permissions} |
||||
params={params} |
||||
setParams={handleParams} |
||||
fixed={false} |
||||
> |
||||
{useCallback((permission) => <PermissionRow key={permission._id} permission={permission} t={t} roleList={roleList} onGrant={grantRole} onRemove={removeRole} />, [grantRole, removeRole, roleList, t])} |
||||
</GenericTable> |
||||
</Margins> |
||||
</Page.Content> |
||||
</Page> |
||||
<PermissionsContextBar /> |
||||
</Page>; |
||||
}; |
||||
|
||||
export default PermissionsTable; |
||||
@ -0,0 +1,59 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Box, Field, TextInput, Select, ToggleSwitch } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
const RoleForm = ({ values, handlers, className, editing = false, isProtected = false }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const { |
||||
name, |
||||
description, |
||||
scope, |
||||
mandatory2fa, |
||||
} = values; |
||||
|
||||
const { |
||||
handleName, |
||||
handleDescription, |
||||
handleScope, |
||||
handleMandatory2fa, |
||||
} = handlers; |
||||
|
||||
const options = useMemo(() => [ |
||||
['Users', t('Global')], |
||||
['Subscriptions', t('Rooms')], |
||||
], [t]); |
||||
|
||||
return <> |
||||
<Field className={className}> |
||||
<Field.Label>{t('Role')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput disabled={editing} value={name} onChange={handleName} placeholder={t('Role')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field className={className}> |
||||
<Field.Label>{t('Description')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={description} onChange={handleDescription} placeholder={t('Description')}/> |
||||
</Field.Row> |
||||
<Field.Hint>{('Leave the description field blank if you dont want to show the role')}</Field.Hint> |
||||
</Field> |
||||
<Field className={className}> |
||||
<Field.Label>{t('Scope')}</Field.Label> |
||||
<Field.Row> |
||||
<Select disabled={isProtected} options={options} value={scope} onChange={handleScope} placeholder={t('Scope')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field className={className}> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label>{t('Users must use Two Factor Authentication')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={mandatory2fa} onChange={handleMandatory2fa}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
</>; |
||||
}; |
||||
|
||||
export default RoleForm; |
||||
@ -0,0 +1,95 @@ |
||||
import React, { useState, useRef } from 'react'; |
||||
import { Box, Field, Margins, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import UsersInRoleTable from './UsersInRoleTable'; |
||||
import RoomAutoComplete from '../../components/basic/RoomAutoComplete'; |
||||
import { UserAutoComplete } from '../../components/basic/AutoComplete'; |
||||
import { useRoute, useRouteParameter } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useRole } from './useRole'; |
||||
|
||||
const UsersInRolePageContainer = () => { |
||||
const _id = useRouteParameter('_id'); |
||||
|
||||
const role = useRole(_id); |
||||
|
||||
if (!role) { |
||||
return null; |
||||
} |
||||
|
||||
return <UsersInRolePage data={role} />; |
||||
}; |
||||
|
||||
const UsersInRolePage = ({ data }) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const reload = useRef(); |
||||
|
||||
const [user, setUser] = useState(); |
||||
const [rid, setRid] = useState(); |
||||
|
||||
const { name } = data; |
||||
|
||||
const router = useRoute('admin-permissions'); |
||||
|
||||
const addUser = useMethod('authorization:addUserToRole'); |
||||
|
||||
const handleReturn = useMutableCallback(() => { |
||||
router.push({ |
||||
context: 'edit', |
||||
_id: name, |
||||
}); |
||||
}); |
||||
|
||||
const handleAdd = useMutableCallback(async () => { |
||||
try { |
||||
await addUser(name, user, rid); |
||||
dispatchToastMessage({ type: 'success', message: t('User_added') }); |
||||
setUser(); |
||||
reload.current(); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
return <Page> |
||||
<Page.Header title={`${ t('Users_in_role') } "${ name }"`}> |
||||
<ButtonGroup> |
||||
{/* <Button primary onClick={handleSave}>{t('Save')}</Button> */} |
||||
<Button onClick={handleReturn}>{t('Back')}</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.Content> |
||||
<Box display='flex' flexDirection='row' w='full' mi='neg-x4'> |
||||
<Margins inline='x4'> |
||||
{data.scope !== 'Users' && <Field> |
||||
<Field.Label>{t('Choose_a_room')}</Field.Label> |
||||
<Field.Row> |
||||
<RoomAutoComplete value={rid} onChange={setRid} placeholder={t('User')}/> |
||||
</Field.Row> |
||||
</Field>} |
||||
<Field> |
||||
<Field.Label>{t('Add_user')}</Field.Label> |
||||
<Field.Row> |
||||
<UserAutoComplete value={user} onChange={setUser} placeholder={t('User')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Box display='flex' flexGrow={1} flexDirection='column' justifyContent='flex-end'> |
||||
<Button primary onClick={handleAdd}>{t('Add')}</Button> |
||||
</Box> |
||||
</Margins> |
||||
</Box> |
||||
<Margins blockStart='x8'> |
||||
{(data.scope === 'Users' || rid) && <UsersInRoleTable reloadRef={reload} scope={data.scope} rid={rid} roleName={data.name}/>} |
||||
{data.scope !== 'Users' && !rid && <Callout type='info'>{t('Select_a_room')}</Callout>} |
||||
</Margins> |
||||
</Page.Content> |
||||
</Page>; |
||||
}; |
||||
|
||||
export default UsersInRolePageContainer; |
||||
@ -0,0 +1,118 @@ |
||||
import React, { useState, useMemo } from 'react'; |
||||
import { Box, Table, Button, Icon } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import UserAvatar from '../../components/basic/avatar/UserAvatar'; |
||||
import DeleteWarningModal from '../../components/DeleteWarningModal'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { GenericTable } from '../../components/GenericTable'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useSetModal } from '../../contexts/ModalContext'; |
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
|
||||
|
||||
const UserRow = React.memo(({ _id, username, name, avatarETag, emails, onRemove }) => { |
||||
const email = emails?.find(({ address }) => !!address).address; |
||||
|
||||
const handleRemove = useMutableCallback(() => { |
||||
onRemove(username); |
||||
}); |
||||
|
||||
return <Table.Row key={_id} tabIndex={0} role='link'> |
||||
<Table.Cell withTruncatedText> |
||||
<Box display='flex' alignItems='center'> |
||||
<UserAvatar size='x40' title={username} username={username} etag={avatarETag}/> |
||||
<Box display='flex' withTruncatedText mi='x8'> |
||||
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText> |
||||
<Box fontScale='p2' withTruncatedText color='default'>{name || username}</Box> |
||||
{name && <Box fontScale='p1' color='hint' withTruncatedText> {`@${ username }`} </Box>} |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
</Table.Cell> |
||||
<Table.Cell withTruncatedText>{email}</Table.Cell> |
||||
<Table.Cell withTruncatedText> |
||||
<Button small square danger onClick={handleRemove}> |
||||
<Icon name='trash' size='x20' /> |
||||
</Button> |
||||
</Table.Cell> |
||||
</Table.Row>; |
||||
}); |
||||
|
||||
export function UsersInRoleTable({ data, reload, roleName, total, params, setParams, rid }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const setModal = useSetModal(); |
||||
|
||||
const removeUser = useMethod('authorization:removeUserFromRole'); |
||||
|
||||
const closeModal = () => setModal(); |
||||
|
||||
const onRemove = useMutableCallback((username) => { |
||||
const remove = async () => { |
||||
try { |
||||
await removeUser(roleName, username, rid); |
||||
dispatchToastMessage({ type: 'success', message: t('User_removed') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'erroor', message: error }); |
||||
} |
||||
closeModal(); |
||||
reload(); |
||||
}; |
||||
setModal(<DeleteWarningModal onCancel={closeModal} onDelete={remove}> |
||||
{t('The_user_s_will_be_removed_from_role_s', username, roleName)} |
||||
</DeleteWarningModal>); |
||||
}); |
||||
|
||||
return <GenericTable |
||||
header={<> |
||||
<GenericTable.HeaderCell > |
||||
{t('Name')} |
||||
</GenericTable.HeaderCell> |
||||
<GenericTable.HeaderCell> |
||||
{t('Email')} |
||||
</GenericTable.HeaderCell> |
||||
<GenericTable.HeaderCell w='x80'> |
||||
</GenericTable.HeaderCell> |
||||
</>} |
||||
results={data} |
||||
params={params} |
||||
setParams={setParams} |
||||
total={total} |
||||
> |
||||
{(props) => <UserRow onRemove={onRemove} key={props._id} {...props}/>} |
||||
</GenericTable>; |
||||
} |
||||
|
||||
const UsersInRoleTableContainer = ({ rid, roleName, reloadRef }) => { |
||||
const [params, setParams] = useState({ current: 0, itemsPerPage: 25 }); |
||||
|
||||
const debouncedParams = useDebouncedValue(params, 500); |
||||
|
||||
const query = useMemo(() => ({ |
||||
roomId: rid, |
||||
role: roleName, |
||||
...debouncedParams.itemsPerPage && { count: debouncedParams.itemsPerPage }, |
||||
...debouncedParams.current && { offset: debouncedParams.current }, |
||||
}), [debouncedParams, rid, roleName]); |
||||
|
||||
const { data = {}, reload } = useEndpointDataExperimental('roles.getUsersInRole', query); |
||||
|
||||
reloadRef.current = reload; |
||||
|
||||
const tableData = data?.users || []; |
||||
|
||||
return <UsersInRoleTable |
||||
data={tableData} |
||||
total={data?.total} |
||||
reload={reload} |
||||
params={params} |
||||
setParams={setParams} |
||||
roleName={roleName} |
||||
rid={rid} |
||||
/>; |
||||
}; |
||||
|
||||
export default UsersInRoleTableContainer; |
||||
@ -0,0 +1,6 @@ |
||||
import { useCallback } from 'react'; |
||||
|
||||
import { Roles } from '../../../app/models/client'; |
||||
import { useReactiveValue } from '../../hooks/useReactiveValue'; |
||||
|
||||
export const useRole = (_id) => useReactiveValue(useCallback(() => Roles.findOne({ _id }), [_id])); |
||||
@ -0,0 +1,32 @@ |
||||
import React, { useMemo, useState } from 'react'; |
||||
import { AutoComplete, Option, Options } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import RoomAvatar from './avatar/RoomAvatar'; |
||||
|
||||
const query = (term = '') => ({ selector: JSON.stringify({ name: term }) }); |
||||
|
||||
const Avatar = ({ value, type, avatarETag, ...props }) => <RoomAvatar size={Options.AvatarSize} room={{ type, _id: value, avatarETag }} {...props} />; |
||||
|
||||
const RoomAutoComplete = React.memo((props) => { |
||||
const [filter, setFilter] = useState(''); |
||||
const { data } = useEndpointDataExperimental('rooms.autocomplete.channelAndPrivate', useMemo(() => query(filter), [filter])); |
||||
const options = useMemo(() => (data && data.items.map(({ name, _id, avatarETag, t }) => ({ |
||||
value: _id, |
||||
label: { name, avatarETag, type: t }, |
||||
}))) || [], [data]); |
||||
|
||||
return <AutoComplete |
||||
{...props} |
||||
filter={filter} |
||||
setFilter={setFilter} |
||||
renderSelected={({ |
||||
value, |
||||
label, |
||||
}) => <><RoomAvatar size='x20' room={{ type: label?.type || 'c', _id: value, ...label }} /> {label?.name}</>} |
||||
renderItem={({ value, label, ...props }) => <Option key={value} {...props} label={label.name} avatar={<Avatar value={value} {...label} />} />} |
||||
options={ options } |
||||
/>; |
||||
}); |
||||
|
||||
export default RoomAutoComplete; |
||||
Loading…
Reference in new issue