diff --git a/app/api/server/v1/roles.js b/app/api/server/v1/roles.js index 7772f80e29d..dbf967b8483 100644 --- a/app/api/server/v1/roles.js +++ b/app/api/server/v1/roles.js @@ -83,6 +83,7 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { name: 1, username: 1, emails: 1, + avatarETag: 1, }; if (!role) { @@ -99,7 +100,7 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { sort: { username: 1 }, skip: offset, fields, - }).fetch(); - return API.v1.success({ users }); + }); + return API.v1.success({ users: users.fetch(), total: users.count() }); }, }); diff --git a/app/authorization/client/index.js b/app/authorization/client/index.js index ca382bacc82..bf484b06455 100644 --- a/app/authorization/client/index.js +++ b/app/authorization/client/index.js @@ -3,9 +3,7 @@ import { hasRole } from './hasRole'; import { AuthorizationUtils } from '../lib/AuthorizationUtils'; import './usersNameChanged'; import './requiresPermission.html'; -import './route'; import './startup'; -import './stylesheets/permissions.css'; export { hasAllPermission, diff --git a/app/authorization/client/route.js b/app/authorization/client/route.js deleted file mode 100644 index 2a09c78da6d..00000000000 --- a/app/authorization/client/route.js +++ /dev/null @@ -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', - }); - }, -}); diff --git a/app/authorization/client/startup.js b/app/authorization/client/startup.js index 2dd50b95fa2..3f4c4637a0b 100644 --- a/app/authorization/client/startup.js +++ b/app/authorization/client/startup.js @@ -27,7 +27,9 @@ Meteor.startup(() => { delete role.type; Roles.upsert({ _id: role.name }, role); }, - removed: (role) => Roles.remove({ _id: role.name }), + removed: (role) => { + Roles.remove({ _id: role.name }); + }, }; Tracker.autorun((c) => { diff --git a/app/authorization/client/stylesheets/permissions.css b/app/authorization/client/stylesheets/permissions.css deleted file mode 100644 index 1561d2ebf53..00000000000 --- a/app/authorization/client/stylesheets/permissions.css +++ /dev/null @@ -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; - } - } -} diff --git a/app/authorization/client/views/index.js b/app/authorization/client/views/index.js deleted file mode 100644 index ba54dffa5c0..00000000000 --- a/app/authorization/client/views/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import './permissions.html'; -import './permissions'; -import './permissionsRole.html'; -import './permissionsRole'; diff --git a/app/authorization/client/views/permissions.html b/app/authorization/client/views/permissions.html deleted file mode 100644 index d332e471849..00000000000 --- a/app/authorization/client/views/permissions.html +++ /dev/null @@ -1,80 +0,0 @@ - - diff --git a/app/authorization/client/views/permissions.js b/app/authorization/client/views/permissions.js deleted file mode 100644 index a01a9d182fa..00000000000 --- a/app/authorization/client/views/permissions.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/authorization/client/views/permissionsRole.html b/app/authorization/client/views/permissionsRole.html deleted file mode 100644 index 74579e16fe5..00000000000 --- a/app/authorization/client/views/permissionsRole.html +++ /dev/null @@ -1,110 +0,0 @@ - diff --git a/app/authorization/client/views/permissionsRole.js b/app/authorization/client/views/permissionsRole.js deleted file mode 100644 index 4e16cd666f5..00000000000 --- a/app/authorization/client/views/permissionsRole.js +++ /dev/null @@ -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(); - }); -}); diff --git a/app/authorization/server/methods/deleteRole.js b/app/authorization/server/methods/deleteRole.js index 0b42263c23f..56ab719fe98 100644 --- a/app/authorization/server/methods/deleteRole.js +++ b/app/authorization/server/methods/deleteRole.js @@ -35,6 +35,7 @@ Meteor.methods({ method: 'authorization:deleteRole', }); } + const removed = Models.Roles.remove(role.name); if (removed) { rolesStreamer.emit('roles', { diff --git a/client/admin/permissions/EditRolePage.js b/client/admin/permissions/EditRolePage.js new file mode 100644 index 00000000000..5c98042b6a4 --- /dev/null +++ b/client/admin/permissions/EditRolePage.js @@ -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 {t('error-invalid-role')}; + } + + return ; +}; + +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 + + + + + + + + + {!data.protected && + + + + } + + + + + + + + ; +}; + +export default EditRolePageContainer; diff --git a/client/admin/permissions/NewRolePage.js b/client/admin/permissions/NewRolePage.js new file mode 100644 index 00000000000..7c17c0a1fbe --- /dev/null +++ b/client/admin/permissions/NewRolePage.js @@ -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 + + + + + + + + + ; +}; + +export default NewRolePage; diff --git a/client/admin/permissions/PermissionsContextBar.js b/client/admin/permissions/PermissionsContextBar.js new file mode 100644 index 00000000000..41225f64bd8 --- /dev/null +++ b/client/admin/permissions/PermissionsContextBar.js @@ -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 && + + {context === 'new' && t('New_role')} + {context === 'edit' && t('Role_Editing')} + + + + {context === 'new' && } + {context === 'edit' && } + + ) || null; +}; + +export default PermissionsContextBar; diff --git a/client/admin/permissions/PermissionsRouter.js b/client/admin/permissions/PermissionsRouter.js new file mode 100644 index 00000000000..5947a9e9b41 --- /dev/null +++ b/client/admin/permissions/PermissionsRouter.js @@ -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 ; + } + + return ; +}; + +export default PermissionsRouter; diff --git a/client/admin/permissions/PermissionsTable.js b/client/admin/permissions/PermissionsTable.js new file mode 100644 index 00000000000..d6f41a688d8 --- /dev/null +++ b/client/admin/permissions/PermissionsTable.js @@ -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 + + + {!loading && + {description || _id} + } + {loading && } + + ; +}); + +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 + {getName(t, permission)} + {roleList.map(({ _id, description }) => )} + ; +}); + +const RoleHeader = React.memo(({ router, _id, description, ...props }) => { + const onClick = useMutableCallback(() => { + router.push({ + context: 'edit', + _id, + }); + }); + + return + + + {description || _id} + + + + ; +}); + +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 ; +}; + +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 + + + + + + + + {t('Permissions')} + + + {t('Settings')} + + + + + + + + {t('Name')} + {roleList.map(({ _id, description }) => )} + } + total={total} + results={permissions} + params={params} + setParams={handleParams} + fixed={false} + > + {useCallback((permission) => , [grantRole, removeRole, roleList, t])} + + + + + + ; +}; + +export default PermissionsTable; diff --git a/client/admin/permissions/RoleForm.js b/client/admin/permissions/RoleForm.js new file mode 100644 index 00000000000..d91e99e5df9 --- /dev/null +++ b/client/admin/permissions/RoleForm.js @@ -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 <> + + {t('Role')} + + + + + + {t('Description')} + + + + {('Leave the description field blank if you dont want to show the role')} + + + {t('Scope')} + +