diff --git a/client/admin/permissions/EditRolePage.js b/client/admin/permissions/EditRolePage.js index 5c98042b6a42966b5c88ef5170d9096ba6d7540d..0953551a129dfbac124149aa493e5908c5d7f56f 100644 --- a/client/admin/permissions/EditRolePage.js +++ b/client/admin/permissions/EditRolePage.js @@ -27,7 +27,7 @@ const EditRolePage = ({ data }) => { const usersInRoleRouter = useRoute('admin-permissions'); const router = useRoute('admin-permissions'); - const { values, handlers } = useForm({ + const { values, handlers, hasUnsavedChanges } = useForm({ name: data.name, description: data.description || '', scope: data.scope || 'Users', @@ -69,7 +69,7 @@ const EditRolePage = ({ data }) => { <RoleForm values={values} handlers={handlers} editing isProtected={data.protected}/> <Field> <Field.Row> - <Button primary w='full' onClick={handleSave}>{t('Save')}</Button> + <Button primary w='full' disabled={!hasUnsavedChanges} onClick={handleSave}>{t('Save')}</Button> </Field.Row> </Field> {!data.protected && <Field> diff --git a/client/channel/UserCard/index.js b/client/channel/UserCard/index.js index aa496ef6acb57bc76f002e891418e43ba92d88c8..e01358eab9f4dba729eb5edddd0d5b15003972cc 100644 --- a/client/channel/UserCard/index.js +++ b/client/channel/UserCard/index.js @@ -12,10 +12,13 @@ import { LocalTime } from '../../components/basic/UTCClock'; import { useUserInfoActions, useUserInfoActionsSpread } from '../hooks/useUserInfoActions'; import { useComponentDidUpdate } from '../../hooks/useComponentDidUpdate'; import { useCurrentRoute } from '../../contexts/RouterContext'; +import { useRolesDescription } from '../../contexts/AuthorizationContext'; const UserCardWithData = ({ username, onClose, target, open, rid }) => { const ref = useRef(target); + const getRoles = useRolesDescription(); + const t = useTranslation(); const showRealNames = useSetting('UI_Use_Real_Name'); @@ -54,7 +57,7 @@ const UserCardWithData = ({ username, onClose, target, open, rid }) => { _id, name: showRealNames ? name : username, username, - roles: roles && roles.map((role, index) => ( + roles: roles && getRoles(roles).map((role, index) => ( <UserCard.Role key={index}>{role}</UserCard.Role> )), bio, @@ -66,7 +69,7 @@ const UserCardWithData = ({ username, onClose, target, open, rid }) => { customStatus: statusText, nickname, }; - }, [data, username, showRealNames, state]); + }, [data, username, showRealNames, state, getRoles]); const handleOpen = useMutableCallback((e) => { open && open(e); diff --git a/client/channel/UserInfo/index.js b/client/channel/UserInfo/index.js index e8549af9b4e1e9052ac8167e8fbe42859f4cb557..640902f7a1b796649befd5fb0d582891d4a18228 100644 --- a/client/channel/UserInfo/index.js +++ b/client/channel/UserInfo/index.js @@ -13,10 +13,13 @@ import UserCard from '../../components/basic/UserCard'; import { FormSkeleton } from '../../admin/users/Skeleton'; import VerticalBar from '../../components/basic/VerticalBar'; import UserActions from './actions/UserActions'; +import { useRolesDescription } from '../../contexts/AuthorizationContext'; export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, username, tabBar, rid, onClose, video, showBackButton, ...props }) { const t = useTranslation(); + const getRoles = useRolesDescription(); + const showRealNames = useSetting('UI_Use_Real_Name'); const { data, state, error } = useEndpointDataExperimental( @@ -44,7 +47,7 @@ export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, user name: showRealNames ? name : username, username, lastLogin, - roles: roles.map((role, index) => ( + roles: roles && getRoles(roles).map((role, index) => ( <UserCard.Role key={index}>{role}</UserCard.Role> )), bio, @@ -58,7 +61,7 @@ export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, user customStatus: statusText, nickname, }; - }, [data, showRealNames]); + }, [data, showRealNames, getRoles]); return ( <VerticalBar> diff --git a/client/contexts/AuthorizationContext.ts b/client/contexts/AuthorizationContext.ts index e9aa28cdeb42fc77f2819b3b485ad5f26f3e780e..83c6e07c84e34eed360d03b9af1fa64506d08d9c 100644 --- a/client/contexts/AuthorizationContext.ts +++ b/client/contexts/AuthorizationContext.ts @@ -1,5 +1,15 @@ -import { createContext, useContext, useMemo } from 'react'; +import { createContext, useContext, useMemo, useCallback } from 'react'; import { useSubscription, Subscription, Unsubscribe } from 'use-subscription'; +import EventEmitter from 'wolfy87-eventemitter'; + +import { IRole } from '../../definition/IUser'; + +type IRoles = { [_id: string]: IRole } + + +export class RoleStore extends EventEmitter { + roles: IRoles = {}; +} export type AuthorizationContextValue = { queryPermission( @@ -15,8 +25,10 @@ export type AuthorizationContextValue = { scope?: string | Mongo.ObjectID ): Subscription<boolean>; queryRole(role: string | Mongo.ObjectID): Subscription<boolean>; + roleStore: RoleStore; }; + export const AuthorizationContext = createContext<AuthorizationContextValue>({ queryPermission: () => ({ getCurrentValue: (): boolean => false, @@ -34,6 +46,7 @@ export const AuthorizationContext = createContext<AuthorizationContextValue>({ getCurrentValue: (): boolean => false, subscribe: (): Unsubscribe => (): void => undefined, }), + roleStore: new RoleStore(), }); export const usePermission = ( @@ -72,6 +85,28 @@ export const useAllPermissions = ( return useSubscription(subscription); }; +export const useRolesDescription = (): (ids: Array<string>) => [string] => { + const { roleStore } = useContext(AuthorizationContext); + + const subscription = useMemo( + () => ({ + getCurrentValue: (): IRoles => roleStore.roles, + subscribe: (callback: Function): () => void => { + roleStore.on('change', callback); + return (): void => { + roleStore.off('change', callback); + }; + }, + }), + [roleStore], + ); + + const roles = useSubscription<IRoles>(subscription); + + return useCallback((values) => values.map((role: string) => (roles[role] && roles[role].description) || role) + , [roles]) as (ids: Array<string>) => [string]; +}; + export const useRole = (role: string | Mongo.ObjectID): boolean => { const { queryRole } = useContext(AuthorizationContext); const subscription = useMemo(() => queryRole(role), [queryRole, role]); diff --git a/client/providers/AuthorizationProvider.tsx b/client/providers/AuthorizationProvider.tsx index a934bd69af1d9e8c5c24183e64ed1415d8a2e94d..c80c57f9c63e79bebe15d1b5782a0e02ea7a6a40 100644 --- a/client/providers/AuthorizationProvider.tsx +++ b/client/providers/AuthorizationProvider.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback, useEffect } from 'react'; import { Meteor } from 'meteor/meteor'; import { @@ -7,8 +7,11 @@ import { hasAllPermission, hasRole, } from '../../app/authorization/client'; -import { AuthorizationContext } from '../contexts/AuthorizationContext'; +import { AuthorizationContext, RoleStore } from '../contexts/AuthorizationContext'; import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory'; +import { useReactiveValue } from '../hooks/useReactiveValue'; +import { Roles } from '../../app/models/client/models/Roles'; + const contextValue = { queryPermission: createReactiveSubscriptionFactory( @@ -23,9 +26,22 @@ const contextValue = { queryRole: createReactiveSubscriptionFactory( (role) => hasRole(Meteor.userId(), role), ), + roleStore: new RoleStore(), }; -const AuthorizationProvider: FC = ({ children }) => - <AuthorizationContext.Provider children={children} value={contextValue} />; + +const AuthorizationProvider: FC = ({ children }) => { + const roles = useReactiveValue(useCallback(() => Roles.find().fetch().reduce((ret, obj) => { + ret[obj._id] = obj; + return ret; + }, {}), [])); + + useEffect(() => { + contextValue.roleStore.roles = roles; + contextValue.roleStore.emit('change', roles); + }, [roles]); + + return <AuthorizationContext.Provider children={children} value={contextValue} />; +}; export default AuthorizationProvider; diff --git a/definition/IUser.ts b/definition/IUser.ts index fb1628d3b674abc1a0644f1fb9953ea9fab7f229..cb55aa40b9ec5548a5a2ae44e2956ced94c44a28 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -73,6 +73,15 @@ export interface IUserSettings { }; } +export interface IRole { + description: string; + mandatory2fa?: boolean; + name: string; + protected: boolean; + scope?: string; + _id: string; +} + export interface IUser { _id: string; createdAt: Date; diff --git a/package-lock.json b/package-lock.json index b5250204b1f893279b5a516885304ac54ae2ac44..8399ea55cac3ce3d68a0e7abea3637e8927062ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8742,6 +8742,14 @@ } } }, + "@types/wolfy87-eventemitter": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.0.tgz", + "integrity": "sha512-N1moqePknZH927suyuzL9rdGLMwc+Ls7VW5WNlmfG3Zv/ZZhCigPXWiBJ7wy1Up4JVQJTo+S1vCiqYIyQw/Lfw==", + "requires": { + "wolfy87-eventemitter": "*" + } + }, "@types/ws": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz", diff --git a/package.json b/package.json index b362b6b3c020016379118b7073adee5a2465cd57..46e6319991c5a8089098dde5cc291dba56ddbd5d 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "@types/mkdirp": "^1.0.1", "@types/underscore.string": "0.0.38", "@types/use-subscription": "^1.0.0", + "@types/wolfy87-eventemitter": "^5.2.0", "@types/xml-crypto": "^1.4.1", "@types/xmldom": "^0.1.30", "adm-zip": "RocketChat/adm-zip",