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