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
@ -8,7 +8,6 @@ import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
useSetProjectOwnerMutation,
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
@ -44,7 +43,7 @@ import SimpleLists from 'shared/components/Lists';
|
||||
import produce from 'immer';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import DueDateManager from 'shared/components/DueDateManager';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser } from 'App/context';
|
||||
import LabelManager from 'shared/components/PopupMenu/LabelManager';
|
||||
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
||||
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||
@ -106,16 +105,48 @@ const initialQuickCardEditorState: QuickCardEditorState = {
|
||||
type ProjectBoardProps = {
|
||||
onCardLabelClick?: () => void;
|
||||
cardLabelVariant?: CardLabelVariant;
|
||||
projectID?: string;
|
||||
loading?: boolean;
|
||||
projectID: string;
|
||||
};
|
||||
|
||||
const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
projectID,
|
||||
onCardLabelClick,
|
||||
cardLabelVariant,
|
||||
loading: isLoading = false,
|
||||
}) => {
|
||||
export const BoardLoading = () => {
|
||||
return (
|
||||
<>
|
||||
<ProjectBar>
|
||||
<ProjectActions>
|
||||
<ProjectAction disabled>
|
||||
<CheckCircle width={13} height={13} />
|
||||
<ProjectActionText>All Tasks</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Filter width={13} height={13} />
|
||||
<ProjectActionText>Filter</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Sort width={13} height={13} />
|
||||
<ProjectActionText>Sort</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
<ProjectActions>
|
||||
<ProjectAction>
|
||||
<Tags width={13} height={13} />
|
||||
<ProjectActionText>Labels</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<ToggleOn width={13} height={13} />
|
||||
<ProjectActionText>Fields</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Bolt width={13} height={13} />
|
||||
<ProjectActionText>Rules</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
</ProjectBar>
|
||||
<EmptyBoard />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick, cardLabelVariant }) => {
|
||||
const [assignTask] = useAssignTaskMutation();
|
||||
const [unassignTask] = useUnassignTaskMutation();
|
||||
const $labelsRef = useRef<HTMLDivElement>(null);
|
||||
@ -124,7 +155,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
|
||||
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user } = useCurrentUser();
|
||||
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
|
||||
const history = useHistory();
|
||||
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
|
||||
@ -138,7 +169,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
|
||||
);
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -156,7 +187,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
||||
}
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -170,14 +201,14 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
|
||||
const { loading, data } = useFindProjectQuery({
|
||||
variables: { projectId: projectID ?? '' },
|
||||
variables: { projectID },
|
||||
});
|
||||
|
||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
|
||||
@ -205,7 +236,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
}
|
||||
}
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -238,7 +269,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
__typename: 'Mutation',
|
||||
createTask: {
|
||||
__typename: 'Task',
|
||||
id: '' + Math.round(Math.random() * -1000000),
|
||||
id: `${Math.round(Math.random() * -1000000)}`,
|
||||
name,
|
||||
complete: false,
|
||||
taskGroup: {
|
||||
@ -248,6 +279,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
position: taskGroup.position,
|
||||
},
|
||||
badges: {
|
||||
__typename: 'TaskBadges',
|
||||
checklist: null,
|
||||
},
|
||||
position,
|
||||
@ -273,42 +305,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || isLoading) {
|
||||
return (
|
||||
<>
|
||||
<ProjectBar>
|
||||
<ProjectActions>
|
||||
<ProjectAction disabled>
|
||||
<CheckCircle width={13} height={13} />
|
||||
<ProjectActionText>All Tasks</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Filter width={13} height={13} />
|
||||
<ProjectActionText>Filter</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Sort width={13} height={13} />
|
||||
<ProjectActionText>Sort</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
<ProjectActions>
|
||||
<ProjectAction>
|
||||
<Tags width={13} height={13} />
|
||||
<ProjectActionText>Labels</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<ToggleOn width={13} height={13} />
|
||||
<ProjectActionText>Fields</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Bolt width={13} height={13} />
|
||||
<ProjectActionText>Rules</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
</ProjectBar>
|
||||
<EmptyBoard />
|
||||
</>
|
||||
);
|
||||
if (loading) {
|
||||
return <BoardLoading />;
|
||||
}
|
||||
if (data) {
|
||||
labelsRef.current = data.findProject.labels;
|
||||
@ -534,7 +532,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
|
||||
}));
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
})
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
FindTaskDocument,
|
||||
FindTaskQuery,
|
||||
} from 'shared/generated/graphql';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser } from 'App/context';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import DueDateManager from 'shared/components/DueDateManager';
|
||||
import produce from 'immer';
|
||||
@ -129,7 +129,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
availableMembers,
|
||||
refreshCache,
|
||||
}) => {
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user } = useCurrentUser();
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
@ -407,7 +407,9 @@ const Details: React.FC<DetailsProps> = ({
|
||||
user={member}
|
||||
bio="None"
|
||||
onRemoveFromTask={() => {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
||||
if (user) {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
@ -422,10 +424,12 @@ const Details: React.FC<DetailsProps> = ({
|
||||
availableMembers={availableMembers}
|
||||
activeMembers={data.findTask.assigned}
|
||||
onMemberChange={(member, isActive) => {
|
||||
if (isActive) {
|
||||
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
||||
} else {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
||||
if (user) {
|
||||
if (isActive) {
|
||||
assignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||
} else {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, {useState} from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import {usePopup, Popup} from 'shared/components/PopupMenu';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
useSetProjectOwnerMutation,
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
@ -50,7 +49,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
taskLabels: taskLabelsRef,
|
||||
}) => {
|
||||
const [currentLabel, setCurrentLabel] = useState('');
|
||||
const {setTab, hidePopup} = usePopup();
|
||||
const { setTab, hidePopup } = usePopup();
|
||||
const [createProjectLabel] = useCreateProjectLabelMutation({
|
||||
update: (client, newLabelData) => {
|
||||
updateApolloCache<FindProjectQuery>(
|
||||
@ -58,10 +57,10 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
FindProjectDocument,
|
||||
cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.labels.push({...newLabelData.data.createProjectLabel});
|
||||
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
||||
}),
|
||||
{
|
||||
projectId: projectID,
|
||||
projectID,
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -78,7 +77,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
label => label.id !== newLabelData.data.deleteProjectLabel.id,
|
||||
);
|
||||
}),
|
||||
{projectId: projectID},
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -108,7 +107,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
if (newProjectLabel) {
|
||||
setCurrentTaskLabels([
|
||||
...currentTaskLabels,
|
||||
{id: '', assignedDate: '', projectLabel: {...newProjectLabel}},
|
||||
{ id: '', assignedDate: '', projectLabel: { ...newProjectLabel } },
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -127,12 +126,12 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
label={labels.find(label => label.id === currentLabel) ?? null}
|
||||
onLabelEdit={(projectLabelID, name, color) => {
|
||||
if (projectLabelID) {
|
||||
updateProjectLabel({variables: {projectLabelID, labelColorID: color.id, name: name ?? ''}});
|
||||
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
|
||||
}
|
||||
setTab(0);
|
||||
}}
|
||||
onLabelDelete={labelID => {
|
||||
deleteProjectLabel({variables: {projectLabelID: labelID}});
|
||||
deleteProjectLabel({ variables: { projectLabelID: labelID } });
|
||||
setTab(0);
|
||||
}}
|
||||
/>
|
||||
@ -142,7 +141,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
labelColors={labelColors}
|
||||
label={null}
|
||||
onLabelEdit={(_labelId, name, color) => {
|
||||
createProjectLabel({variables: {projectID, labelColorID: color.id, name: name ?? ''}});
|
||||
createProjectLabel({ variables: { projectID, labelColorID: color.id, name: name ?? '' } });
|
||||
setTab(0);
|
||||
}}
|
||||
/>
|
||||
@ -151,4 +150,4 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelManagerEditor
|
||||
export default LabelManagerEditor;
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
Redirect,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
useSetProjectOwnerMutation,
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
@ -34,10 +33,10 @@ import {
|
||||
} from 'shared/generated/graphql';
|
||||
|
||||
import produce from 'immer';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser } from 'App/context';
|
||||
import Input from 'shared/components/Input';
|
||||
import Member from 'shared/components/Member';
|
||||
import Board from './Board';
|
||||
import Board, { BoardLoading } from './Board';
|
||||
import Details from './Details';
|
||||
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||
|
||||
@ -140,7 +139,7 @@ const Project = () => {
|
||||
const [updateTaskName] = useUpdateTaskNameMutation();
|
||||
|
||||
const { loading, data } = useFindProjectQuery({
|
||||
variables: { projectId: projectID },
|
||||
variables: { projectID },
|
||||
});
|
||||
|
||||
const [updateProjectName] = useUpdateProjectNameMutation({
|
||||
@ -152,7 +151,7 @@ const Project = () => {
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.name = newName.data.updateProjectName.name;
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -166,11 +165,10 @@ const Project = () => {
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.members.push({ ...response.data.createProjectMember.member });
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
const [setProjectOwner] = useSetProjectOwnerMutation();
|
||||
const [deleteProjectMember] = useDeleteProjectMemberMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<FindProjectQuery>(
|
||||
@ -184,12 +182,12 @@ const Project = () => {
|
||||
m => m.id !== response.data.deleteProjectMember.member.id,
|
||||
);
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user } = useCurrentUser();
|
||||
const location = useLocation();
|
||||
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
@ -205,7 +203,7 @@ const Project = () => {
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar onSaveProjectName={projectName => {}} name="" projectID={null} />
|
||||
<Board loading />
|
||||
<BoardLoading />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -221,7 +219,6 @@ const Project = () => {
|
||||
updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
|
||||
}}
|
||||
onChangeProjectOwner={uid => {
|
||||
setProjectOwner({ variables: { ownerID: uid, projectID } });
|
||||
hidePopup();
|
||||
}}
|
||||
onRemoveFromBoard={userID => {
|
||||
@ -248,6 +245,7 @@ const Project = () => {
|
||||
currentTab={0}
|
||||
projectMembers={data.findProject.members}
|
||||
projectID={projectID}
|
||||
teamID={data.findProject.team.id}
|
||||
name={data.findProject.name}
|
||||
/>
|
||||
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
|
||||
|
Reference in New Issue
Block a user