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:
Jordan Knott
2020-07-31 20:01:14 -05:00
committed by Jordan Knott
parent 5dbdc20b36
commit e64f6f8569
63 changed files with 3017 additions and 1905 deletions

View File

@ -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 },
);
},
})