feat: enforce user roles
enforces user admin role requirement for - creating / deleting / setting role for organization users - creating / deleting / setting role for project users - updating project name - deleting project hides action elements based on role for - admin console - team settings if team is only visible through project membership - add project tile if not team admin - project name text editor if not team / project admin - add redirect from team page if settings only visible through project membership - add redirect from admin console if not org admin role enforcement is handled on the api side through a custom GraphQL directive `hasRole`. on the client side, role information is fetched in the TopNavbar's `me` query and stored in the `UserContext`. there is a custom hook, `useCurrentUser`, that provides a user object with two functions, `isVisibile` & `isAdmin` which is used to check roles in order to render/hide relevant UI elements.
This commit is contained in:
committed by
Jordan Knott
parent
5dbdc20b36
commit
e64f6f8569
@ -3,15 +3,18 @@ import Input from 'shared/components/Input';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import produce from 'immer';
|
||||
import Button from 'shared/components/Button';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser, PermissionLevel, PermissionObjectType } from 'App/context';
|
||||
import Select from 'shared/components/Select';
|
||||
import {
|
||||
useGetTeamQuery,
|
||||
RoleCode,
|
||||
useCreateTeamMemberMutation,
|
||||
useDeleteTeamMemberMutation,
|
||||
useUpdateTeamMemberRoleMutation,
|
||||
GetTeamQuery,
|
||||
GetTeamDocument,
|
||||
MeDocument,
|
||||
MeQuery,
|
||||
} from 'shared/generated/graphql';
|
||||
import { UserPlus, Checkmark } from 'shared/icons';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
@ -165,7 +168,6 @@ type TeamRoleManagerPopupProps = {
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
onRemoveFromTeam?: (newOwnerID: string | null) => void;
|
||||
onChangeTeamOwner?: (userID: string) => void;
|
||||
};
|
||||
|
||||
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
@ -175,7 +177,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
currentUserID,
|
||||
canChangeRole,
|
||||
onRemoveFromTeam,
|
||||
onChangeTeamOwner,
|
||||
onChangeRole,
|
||||
}) => {
|
||||
const { hidePopup, setTab } = usePopup();
|
||||
@ -185,15 +186,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
<Popup title={null} tab={0}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{onChangeTeamOwner && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(3);
|
||||
}}
|
||||
>
|
||||
Set as team owner...
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
{subject.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
@ -298,24 +290,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Set as Team Owner?" onClose={() => hidePopup()} tab={3}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
This will change the project owner from you to this subject. They will be able to view and edit cards,
|
||||
remove members, and change all settings for the project. They will also be able to delete the project.
|
||||
</DeleteDescription>
|
||||
<RemoveMemberButton
|
||||
color="warning"
|
||||
onClick={() => {
|
||||
if (onChangeTeamOwner) {
|
||||
onChangeTeamOwner(subject.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Set as Project Owner
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -444,7 +418,7 @@ type MembersProps = {
|
||||
const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user, setUserRoles } = useCurrentUser();
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
const [createTeamMember] = useCreateTeamMemberMutation({
|
||||
@ -454,12 +428,27 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
GetTeamDocument,
|
||||
cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findTeam.members.push({ ...response.data.createTeamMember.teamMember });
|
||||
draftCache.findTeam.members.push({
|
||||
...response.data.createTeamMember.teamMember,
|
||||
member: { __typename: 'MemberList', projects: [], teams: [] },
|
||||
owned: { __typename: 'OwnedList', projects: [], teams: [] },
|
||||
});
|
||||
}),
|
||||
{ teamID },
|
||||
);
|
||||
},
|
||||
});
|
||||
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation({
|
||||
onCompleted: r => {
|
||||
if (user) {
|
||||
setUserRoles(
|
||||
produce(user.roles, draftRoles => {
|
||||
draftRoles.teams.set(r.updateTeamMemberRole.teamID, r.updateTeamMemberRole.member.role.code);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<GetTeamQuery>(
|
||||
@ -479,7 +468,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
return <span>loading</span>;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
if (data && user) {
|
||||
return (
|
||||
<MemberContainer>
|
||||
<FilterTab>
|
||||
@ -497,23 +486,26 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<UserManagementPopup
|
||||
users={data.users}
|
||||
teamMembers={data.findTeam.members}
|
||||
onAddTeamMember={userID => {
|
||||
createTeamMember({ variables: { userID, teamID } });
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
Invite Team Members
|
||||
</InviteMemberButton>
|
||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && (
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<UserManagementPopup
|
||||
users={data.users}
|
||||
teamMembers={data.findTeam.members}
|
||||
onAddTeamMember={userID => {
|
||||
console.log(`team: ${userID}`);
|
||||
createTeamMember({ variables: { userID, teamID } });
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
Invite Team Members
|
||||
</InviteMemberButton>
|
||||
)}
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
@ -532,15 +524,14 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
currentUserID={userID ?? ''}
|
||||
currentUserID={user.id ?? ''}
|
||||
subject={member}
|
||||
members={data.findTeam.members}
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
onChangeTeamOwner={
|
||||
member.role && member.role.code !== 'owner' ? (userID: string) => {} : undefined
|
||||
}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {}}
|
||||
canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)}
|
||||
onChangeRole={roleCode => {
|
||||
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
||||
}}
|
||||
onRemoveFromTeam={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
|
Reference in New Issue
Block a user