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

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useContext } from 'react';
import Admin from 'shared/components/Admin';
import Select from 'shared/components/Select';
import GlobalTopNavbar from 'App/TopNavbar';
@ -16,6 +16,8 @@ import { useForm, Controller } from 'react-hook-form';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import produce from 'immer';
import updateApolloCache from 'shared/utils/cache';
import UserContext, { useCurrentUser } from 'App/context';
import { Redirect } from 'react-router';
const DeleteUserWrapper = styled.div`
display: flex;
@ -170,6 +172,7 @@ const AdminRoute = () => {
}, []);
const { loading, data } = useUsersQuery();
const { showPopup, hidePopup } = usePopup();
const { user } = useCurrentUser();
const [deleteUser] = useDeleteUserAccountMutation({
update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
@ -201,13 +204,17 @@ const AdminRoute = () => {
if (loading) {
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />;
}
if (data) {
if (data && user) {
if (user.roles.org != 'admin') {
return <Redirect to="/" />;
}
return (
<>
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
<Admin
initialTab={0}
users={data.users}
canInviteUser={user.roles.org == 'admin'}
onInviteUser={() => {}}
onUpdateUserPassword={(user, password) => {
console.log(user);

View File

@ -1,31 +0,0 @@
import React, { useContext } from 'react';
import { Home, Stack } from 'shared/icons';
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from 'shared/components/Navbar';
import { Link } from 'react-router-dom';
import UserIDContext from './context';
const GlobalNavbar = () => {
const { userID } = useContext(UserIDContext);
if (!userID) {
return null;
}
return (
<Navbar>
<PrimaryLogo />
<ButtonContainer>
<Link to="/">
<ActionButton name="Home">
<Home width={28} height={28} />
</ActionButton>
</Link>
<Link to="/projects">
<ActionButton name="Projects">
<Stack size={28} color="#c2c6dc" />
</ActionButton>
</Link>
</ButtonContainer>
</Navbar>
);
};
export default GlobalNavbar;

View File

@ -4,7 +4,7 @@ import styled from 'styled-components/macro';
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
import { UserContext, PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
import {
RoleCode,
useMeQuery,
@ -16,6 +16,8 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
import { History } from 'history';
import produce from 'immer';
import { Link } from 'react-router-dom';
import MiniProfile from 'shared/components/MiniProfile';
import cache from 'App/cache';
const TeamContainer = styled.div`
display: flex;
@ -221,6 +223,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
type GlobalTopNavbarProps = {
nameOnly?: boolean;
projectID: string | null;
teamID?: string | null;
onChangeProjectOwner?: (userID: string) => void;
name: string | null;
currentTab?: number;
@ -239,6 +242,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
onSetTab,
menuType,
projectID,
teamID,
onChangeProjectOwner,
onChangeRole,
name,
@ -250,10 +254,27 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
nameOnly,
}) => {
console.log(popupContent);
const { loading, data } = useMeQuery();
const { user, setUserRoles, setUser } = useCurrentUser();
const { loading, data } = useMeQuery({
onCompleted: data => {
console.log('me query has completed!');
if (user && user.roles) {
setUserRoles({
org: user.roles.org,
teams: data.me.teamRoles.reduce((map, obj) => {
map.set(obj.teamID, obj.roleCode);
return map;
}, new Map<string, string>()),
projects: data.me.projectRoles.reduce((map, obj) => {
map.set(obj.projectID, obj.roleCode);
return map;
}, new Map<string, string>()),
});
}
},
});
const { showPopup, hidePopup, setTab } = usePopup();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
const onLogout = () => {
fetch('/auth/logout', {
method: 'POST',
@ -261,8 +282,9 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
}).then(async x => {
const { status } = x;
if (status === 200) {
cache.reset();
history.replace('/login');
setUserID(null);
setUser(null);
hidePopup();
}
});
@ -273,6 +295,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
<Popup title={null} tab={0}>
<ProfileMenu
onLogout={onLogout}
showAdminConsole={user ? user.roles.org === 'admin' : false}
onAdminConsole={() => {
history.push('/admin');
hidePopup();
@ -295,9 +318,41 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
}
};
if (!userID) {
if (!user) {
return null;
}
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
if (member) {
showPopup(
$targetRef,
<MiniProfile
warning={member.role && member.role.code === 'owner' ? warning : null}
canChangeRole={userIsTeamOrProjectAdmin}
onChangeRole={roleCode => {
if (onChangeRole) {
onChangeRole(member.id, roleCode);
}
}}
onRemoveFromBoard={
member.role && member.role.code === 'owner'
? undefined
: () => {
if (onRemoveFromBoard) {
onRemoveFromBoard(member.id);
}
}
}
user={member}
bio=""
/>,
);
}
};
return (
<>
<TopNavbar
@ -312,7 +367,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
);
}}
currentTab={currentTab}
user={data ? data.me : null}
user={data ? data.me.user : null}
canEditProjectName={userIsTeamOrProjectAdmin}
canInviteUser={userIsTeamOrProjectAdmin}
onMemberProfile={onMemberProfile}
onInviteUser={onInviteUser}
onChangeRole={onChangeRole}
onChangeProjectOwner={onChangeProjectOwner}

View File

@ -0,0 +1,5 @@
import { InMemoryCache } from 'apollo-cache-inmemory';
const cache = new InMemoryCache();
export default cache;

View File

@ -1,9 +1,80 @@
import React from 'react';
import React, { useContext } from 'react';
type UserIDContextState = {
userID: string | null;
setUserID: (userID: string | null) => void;
export enum PermissionLevel {
ORG,
TEAM,
PROJECT,
}
export enum PermissionObjectType {
ORG,
TEAM,
PROJECT,
TASK,
}
export type CurrentUserRoles = {
org: string;
teams: Map<string, string>;
projects: Map<string, string>;
};
export const UserIDContext = React.createContext<UserIDContextState>({ userID: null, setUserID: _userID => null });
export default UserIDContext;
export interface CurrentUserRaw {
id: string;
roles: CurrentUserRoles;
}
type UserContextState = {
user: CurrentUserRaw | null;
setUser: (user: CurrentUserRaw | null) => void;
setUserRoles: (roles: CurrentUserRoles) => void;
};
export const UserContext = React.createContext<UserContextState>({
user: null,
setUser: _user => null,
setUserRoles: roles => null,
});
export interface CurrentUser extends CurrentUserRaw {
isAdmin: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
isVisible: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
}
export const useCurrentUser = () => {
const { user, setUser, setUserRoles } = useContext(UserContext);
let currentUser: CurrentUser | null = null;
if (user) {
currentUser = {
...user,
isAdmin(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
if (user.roles.org === 'admin') {
return true;
}
switch (level) {
case PermissionLevel.TEAM:
return subjectID ? this.roles.teams.get(subjectID) === 'admin' : false;
default:
return false;
}
},
isVisible(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
if (user.roles.org === 'admin') {
return true;
}
switch (level) {
case PermissionLevel.TEAM:
return subjectID ? this.roles.teams.get(subjectID) !== null : false;
default:
return false;
}
},
};
}
return {
user: currentUser,
setUser,
setUserRoles,
};
};
export default UserContext;

View File

@ -9,8 +9,7 @@ import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
import { theme } from './ThemeStyles';
import Routes from './Routes';
import { UserIDContext } from './context';
import Navbar from './Navbar';
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context';
const history = createBrowserHistory();
type RefreshTokenResponse = {
@ -20,7 +19,15 @@ type RefreshTokenResponse = {
const App = () => {
const [loading, setLoading] = useState(true);
const [userID, setUserID] = useState<string | null>(null);
const [user, setUser] = useState<CurrentUserRaw | null>(null);
const setUserRoles = (roles: CurrentUserRoles) => {
if (user) {
setUser({
...user,
roles,
});
}
};
useEffect(() => {
fetch('/auth/refresh_token', {
@ -34,7 +41,11 @@ const App = () => {
const response: RefreshTokenResponse = await x.json();
const { accessToken, isInstalled } = response;
const claims: JWTToken = jwtDecode(accessToken);
setUserID(claims.userId);
const currentUser = {
id: claims.userId,
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
};
setUser(currentUser);
setAccessToken(accessToken);
if (!isInstalled) {
history.replace('/install');
@ -46,7 +57,7 @@ const App = () => {
return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
<ThemeProvider theme={theme}>
<NormalizeStyles />
<BaseStyles />
@ -62,7 +73,7 @@ const App = () => {
</PopupProvider>
</Router>
</ThemeProvider>
</UserIDContext.Provider>
</UserContext.Provider>
</>
);
};

View File

@ -6,13 +6,13 @@ import { setAccessToken } from 'shared/utils/accessToken';
import Login from 'shared/components/Login';
import { Container, LoginWrapper } from './Styles';
import UserIDContext from 'App/context';
import UserContext, { PermissionLevel, PermissionObjectType } from 'App/context';
import JwtDecode from 'jwt-decode';
const Auth = () => {
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
const history = useHistory();
const { setUserID } = useContext(UserIDContext);
const { setUser } = useContext(UserContext);
const login = (
data: LoginFormData,
setComplete: (val: boolean) => void,
@ -35,7 +35,11 @@ const Auth = () => {
const response = await x.json();
const { accessToken } = response;
const claims: JWTToken = JwtDecode(accessToken);
setUserID(claims.userId);
const currentUser = {
id: claims.userId,
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
};
setUser(currentUser);
setComplete(true);
setAccessToken(accessToken);

View File

@ -8,13 +8,13 @@ import { getAccessToken, setAccessToken } from 'shared/utils/accessToken';
import updateApolloCache from 'shared/utils/cache';
import produce from 'immer';
import { useApolloClient } from '@apollo/react-hooks';
import UserIDContext from 'App/context';
import UserContext, { PermissionLevel, PermissionObjectType } from 'App/context';
import jwtDecode from 'jwt-decode';
const Install = () => {
const client = useApolloClient();
const history = useHistory();
const { setUserID } = useContext(UserIDContext);
const { setUser } = useContext(UserContext);
useEffect(() => {
fetch('/auth/refresh_token', {
method: 'POST',
@ -65,7 +65,15 @@ const Install = () => {
const response: RefreshTokenResponse = await x.data;
const { accessToken, isInstalled } = response;
const claims: JWTToken = jwtDecode(accessToken);
setUserID(claims.userId);
const currentUser = {
id: claims.userId,
roles: {
org: claims.orgRole,
teams: new Map<string, string>(),
projects: new Map<string, string>(),
},
};
setUser(currentUser);
setAccessToken(accessToken);
if (!isInstalled) {
history.replace('/install');

View File

@ -3,9 +3,7 @@ import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import { Link } from 'react-router-dom';
import { getAccessToken } from 'shared/utils/accessToken';
import Navbar from 'App/Navbar';
import Settings from 'shared/components/Settings';
import UserIDContext from 'App/context';
import { useMeQuery, useClearProfileAvatarMutation } from 'shared/generated/graphql';
import axios from 'axios';
@ -53,7 +51,7 @@ const Projects = () => {
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
{!loading && data && (
<Settings
profile={data.me}
profile={data.me.user}
onProfileAvatarChange={() => {
if ($fileUpload && $fileUpload.current) {
$fileUpload.current.click();

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,174 +105,10 @@ 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,
}) => {
const [assignTask] = useAssignTaskMutation();
const [unassignTask] = useUnassignTaskMutation();
const $labelsRef = useRef<HTMLDivElement>(null);
const match = useRouteMatch();
const labelsRef = useRef<Array<ProjectLabel>>([]);
const { showPopup, hidePopup } = usePopup();
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const { userID } = useContext(UserIDContext);
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
const history = useHistory();
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
update: (client, deletedTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
);
}),
{ projectId: projectID },
);
},
});
const [updateTaskName] = useUpdateTaskNameMutation();
const [createTask] = useCreateTaskMutation({
update: (client, newTaskData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const { taskGroups } = cache.findProject;
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
}
}),
{ projectId: projectID },
);
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({
update: (client, newTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
}),
{ projectId: projectID },
);
},
});
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
const { loading, data } = useFindProjectQuery({
variables: { projectId: projectID ?? '' },
});
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskLocation] = useUpdateTaskLocationMutation({
update: (client, newTask) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) {
const { taskGroups } = cache.findProject;
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id,
);
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
...taskGroups[newTaskGroupIdx].tasks,
{ ...task },
];
}
}
}),
{ projectId: projectID },
);
},
});
const [deleteTask] = useDeleteTaskMutation();
const [toggleTaskLabel] = useToggleTaskLabelMutation({
onCompleted: newTaskLabel => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
console.log(taskLabelsRef.current);
},
});
const onCreateTask = (taskGroupID: string, name: string) => {
if (data) {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
console.log(`taskGroup ${taskGroup}`);
if (taskGroup) {
let position = 65535;
if (taskGroup.tasks.length !== 0) {
const [lastTask] = taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.slice(-1);
position = Math.ceil(lastTask.position) * 2 + 1;
}
console.log(`position ${position}`);
createTask({
variables: { taskGroupID, name, position },
optimisticResponse: {
__typename: 'Mutation',
createTask: {
__typename: 'Task',
id: '' + Math.round(Math.random() * -1000000),
name,
complete: false,
taskGroup: {
__typename: 'TaskGroup',
id: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
},
badges: {
checklist: null,
},
position,
dueDate: null,
description: null,
labels: [],
assigned: [],
},
},
});
}
}
};
const onCreateList = (listName: string) => {
if (data && projectID) {
const [lastColumn] = data.findProject.taskGroups.sort((a, b) => a.position - b.position).slice(-1);
let position = 65535;
if (lastColumn) {
position = lastColumn.position * 2 + 1;
}
createTaskGroup({ variables: { projectID, name: listName, position } });
}
};
if (loading || isLoading) {
export const BoardLoading = () => {
return (
<>
<ProjectBar>
@ -309,6 +144,169 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
<EmptyBoard />
</>
);
};
const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick, cardLabelVariant }) => {
const [assignTask] = useAssignTaskMutation();
const [unassignTask] = useUnassignTaskMutation();
const $labelsRef = useRef<HTMLDivElement>(null);
const match = useRouteMatch();
const labelsRef = useRef<Array<ProjectLabel>>([]);
const { showPopup, hidePopup } = usePopup();
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const { user } = useCurrentUser();
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
const history = useHistory();
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
update: (client, deletedTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
);
}),
{ projectID },
);
},
});
const [updateTaskName] = useUpdateTaskNameMutation();
const [createTask] = useCreateTaskMutation({
update: (client, newTaskData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const { taskGroups } = cache.findProject;
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
}
}),
{ projectID },
);
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({
update: (client, newTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
}),
{ projectID },
);
},
});
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
const { loading, data } = useFindProjectQuery({
variables: { projectID },
});
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskLocation] = useUpdateTaskLocationMutation({
update: (client, newTask) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) {
const { taskGroups } = cache.findProject;
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id,
);
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
...taskGroups[newTaskGroupIdx].tasks,
{ ...task },
];
}
}
}),
{ projectID },
);
},
});
const [deleteTask] = useDeleteTaskMutation();
const [toggleTaskLabel] = useToggleTaskLabelMutation({
onCompleted: newTaskLabel => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
console.log(taskLabelsRef.current);
},
});
const onCreateTask = (taskGroupID: string, name: string) => {
if (data) {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
console.log(`taskGroup ${taskGroup}`);
if (taskGroup) {
let position = 65535;
if (taskGroup.tasks.length !== 0) {
const [lastTask] = taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.slice(-1);
position = Math.ceil(lastTask.position) * 2 + 1;
}
console.log(`position ${position}`);
createTask({
variables: { taskGroupID, name, position },
optimisticResponse: {
__typename: 'Mutation',
createTask: {
__typename: 'Task',
id: `${Math.round(Math.random() * -1000000)}`,
name,
complete: false,
taskGroup: {
__typename: 'TaskGroup',
id: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
},
badges: {
__typename: 'TaskBadges',
checklist: null,
},
position,
dueDate: null,
description: null,
labels: [],
assigned: [],
},
},
});
}
}
};
const onCreateList = (listName: string) => {
if (data && projectID) {
const [lastColumn] = data.findProject.taskGroups.sort((a, b) => a.position - b.position).slice(-1);
let position = 65535;
if (lastColumn) {
position = lastColumn.position * 2 + 1;
}
createTaskGroup({ variables: { projectID, name: listName, position } });
}
};
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 },
);
},
})

View File

@ -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 (user) {
if (isActive) {
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
assignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
} else {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
}
}
}}
/>

View File

@ -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;

View File

@ -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`} />} />

View File

@ -12,9 +12,8 @@ import {
import ProjectGridItem, { AddProjectItem } from 'shared/components/ProjectGridItem';
import { Link } from 'react-router-dom';
import Navbar from 'App/Navbar';
import NewProject from 'shared/components/NewProject';
import UserIDContext from 'App/context';
import UserContext, { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
import Button from 'shared/components/Button';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useForm } from 'react-hook-form';
@ -227,7 +226,7 @@ const ProjectLink = styled(Link)``;
const Projects = () => {
const { showPopup, hidePopup } = usePopup();
const { loading, data } = useGetProjectsQuery();
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
useEffect(() => {
document.title = 'Taskcafé';
}, []);
@ -242,7 +241,7 @@ const Projects = () => {
});
const [showNewProject, setShowNewProject] = useState<ShowNewProject>({ open: false, initialTeamID: null });
const { userID, setUserID } = useContext(UserIDContext);
const { user, setUser } = useCurrentUser();
const [createTeam] = useCreateTeamMutation({
update: (client, createData) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
@ -261,21 +260,36 @@ const Projects = () => {
}
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
if (data) {
if (data && user) {
console.log(user);
const { projects, teams, organizations } = data;
const organizationID = organizations[0].id ?? null;
const projectTeams = teams.map(team => {
const projectTeams = teams
.sort((a, b) => {
const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase();
return textA < textB ? -1 : textA > textB ? 1 : 0;
})
.map(team => {
return {
id: team.id,
name: team.name,
projects: projects.filter(project => project.team.id === team.id),
projects: projects
.filter(project => project.team.id === team.id)
.sort((a, b) => {
const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase();
return textA < textB ? -1 : textA > textB ? 1 : 0;
}),
};
});
console.log(projectTeams);
return (
<>
<GlobalTopNavbar onSaveProjectName={() => {}} projectID={null} name={null} />
<Wrapper>
<ProjectsContainer>
{user.roles.org === 'admin' && (
<AddTeamButton
variant="outline"
onClick={$target => {
@ -302,6 +316,7 @@ const Projects = () => {
>
Add Team
</AddTeamButton>
)}
{projectTeams.length === 0 && (
<EmptyStateContent>
<EmptyState width={425} height={425} />
@ -340,6 +355,7 @@ const Projects = () => {
<div key={team.id}>
<ProjectSectionTitleWrapper>
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
<SectionActions>
<SectionActionLink to={`/teams/${team.id}`}>
<SectionAction variant="outline">Projects</SectionAction>
@ -351,6 +367,7 @@ const Projects = () => {
<SectionAction variant="outline">Settings</SectionAction>
</SectionActionLink>
</SectionActions>
)}
</ProjectSectionTitleWrapper>
<ProjectList>
{team.projects.map((project, idx) => (
@ -363,6 +380,7 @@ const Projects = () => {
</ProjectTile>
</ProjectListItem>
))}
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
<ProjectListItem>
<ProjectAddTile
onClick={() => {
@ -375,6 +393,7 @@ const Projects = () => {
</ProjectAddTileDetails>
</ProjectAddTile>
</ProjectListItem>
)}
</ProjectList>
</div>
);
@ -383,8 +402,8 @@ const Projects = () => {
<NewProject
initialTeamID={showNewProject.initialTeamID}
onCreateProject={(name, teamID) => {
if (userID) {
createProject({ variables: { teamID, name, userID } });
if (user) {
createProject({ variables: { teamID, name, userID: user.id } });
setShowNewProject({ open: false, initialTeamID: null });
}
}}

View File

@ -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 cant 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,6 +486,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
</ListDesc>
<ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && (
<InviteMemberButton
onClick={$target => {
showPopup(
@ -505,6 +495,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
users={data.users}
teamMembers={data.findTeam.members}
onAddTeamMember={userID => {
console.log(`team: ${userID}`);
createTeamMember({ variables: { userID, teamID } });
}}
/>,
@ -514,6 +505,7 @@ const Members: React.FC<MembersProps> = ({ 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

View File

@ -3,7 +3,7 @@ import styled, { css } from 'styled-components/macro';
import { MENU_TYPES } from 'shared/components/TopNavbar';
import GlobalTopNavbar from 'App/TopNavbar';
import updateApolloCache from 'shared/utils/cache';
import { Route, Switch, useRouteMatch } from 'react-router';
import { Route, Switch, useRouteMatch, Redirect } from 'react-router';
import Members from './Members';
import Projects from './Projects';
@ -18,6 +18,7 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
import { History } from 'history';
import produce from 'immer';
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
import UserContext, { PermissionObjectType, PermissionLevel, useCurrentUser } from 'App/context';
const OuterWrapper = styled.div`
display: flex;
@ -87,6 +88,7 @@ const Teams = () => {
const { teamID } = useParams<TeamsRouteProps>();
const history = useHistory();
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
const { user } = useCurrentUser();
const [currentTab, setCurrentTab] = useState(0);
const match = useRouteMatch();
useEffect(() => {
@ -99,7 +101,10 @@ const Teams = () => {
</>
);
}
if (data) {
if (data && user) {
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
return <Redirect to="/" />;
}
return (
<>
<GlobalTopNavbar

View File

@ -4,14 +4,16 @@ import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import { ApolloProvider } from '@apollo/react-hooks';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { enableMapSet } from 'immer';
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
import cache from './App/cache';
import App from './App';
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
enableMapSet();
let forward$;
let isRefreshing = false;
@ -135,7 +137,7 @@ const client = new ApolloClient({
credentials: 'same-origin',
}),
]),
cache: new InMemoryCache(),
cache,
});
ReactDOM.render(

View File

@ -25,6 +25,7 @@ export const Default = () => {
<ThemeProvider theme={theme}>
<Admin
onInviteUser={action('invite user')}
canInviteUser
initialTab={1}
onUpdateUserPassword={action('update user password')}
onDeleteUser={action('delete user')}

View File

@ -55,7 +55,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
position: relative;
text-decoration: none;
${(props) =>
${props =>
props.disabled
? css`
user-select: none;
@ -75,7 +75,7 @@ export const Content = styled.div`
export const CurrentPermission = styled.span`
margin-left: 4px;
color: rgba(${(props) => props.theme.colors.text.secondary}, 0.4);
color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
`;
export const Separator = styled.div`
@ -86,13 +86,13 @@ export const Separator = styled.div`
export const WarningText = styled.span`
display: flex;
color: rgba(${(props) => props.theme.colors.text.primary}, 0.4);
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
padding: 6px;
`;
export const DeleteDescription = styled.div`
font-size: 14px;
color: rgba(${(props) => props.theme.colors.text.primary});
color: rgba(${props => props.theme.colors.text.primary});
`;
export const RemoveMemberButton = styled(Button)`
@ -159,8 +159,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map((perm) => (
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code}
@ -211,9 +211,9 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Choose a new user to take over ownership of this user's teams & projects.
</DeleteDescription>
<UserSelect
onChange={(v) => setDeleteUser(v)}
onChange={v => setDeleteUser(v)}
value={deleteUser}
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
options={users.map(u => ({ label: u.fullName, value: u.id }))}
/>
</>
)}
@ -239,11 +239,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
</DeleteDescription>
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
<UserSelect
onChange={() => {}}
value={null}
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
/>
<UserSelect onChange={() => {}} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} />
<UserPassConfirmButton
onClick={() => {
// onDeleteUser();
@ -334,14 +330,14 @@ const MemberItemOption = styled(Button)`
`;
const MemberList = styled.div`
border-top: 1px solid rgba(${(props) => props.theme.colors.border});
border-top: 1px solid rgba(${props => props.theme.colors.border});
`;
const MemberListItem = styled.div`
display: flex;
flex-flow: row wrap;
justify-content: space-between;
border-bottom: 1px solid rgba(${(props) => props.theme.colors.border});
border-bottom: 1px solid rgba(${props => props.theme.colors.border});
min-height: 40px;
padding: 12px 0 12px 40px;
position: relative;
@ -365,11 +361,11 @@ const MemberProfile = styled(TaskAssignee)`
`;
const MemberItemName = styled.p`
color: rgba(${(props) => props.theme.colors.text.secondary});
color: rgba(${props => props.theme.colors.text.secondary});
`;
const MemberItemUsername = styled.p`
color: rgba(${(props) => props.theme.colors.text.primary});
color: rgba(${props => props.theme.colors.text.primary});
`;
const MemberListHeader = styled.div`
@ -378,12 +374,12 @@ const MemberListHeader = styled.div`
`;
const ListTitle = styled.h3`
font-size: 18px;
color: rgba(${(props) => props.theme.colors.text.secondary});
color: rgba(${props => props.theme.colors.text.secondary});
margin-bottom: 12px;
`;
const ListDesc = styled.span`
font-size: 16px;
color: rgba(${(props) => props.theme.colors.text.primary});
color: rgba(${props => props.theme.colors.text.primary});
`;
const FilterSearch = styled(Input)`
margin: 0;
@ -484,7 +480,7 @@ const ActionButtons = (params: any) => {
<ActionButton onClick={() => {}}>
<EditUserIcon width={16} height={16} />
</ActionButton>
<ActionButton onClick={($target) => params.onDeleteUser($target, params.value)}>
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
<DeleteUserIcon width={16} height={16} />
</ActionButton>
</>
@ -541,7 +537,7 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
width: 100%;
position: relative;
color: ${(props) => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
&:hover {
color: rgba(115, 103, 240);
}
@ -562,7 +558,7 @@ const TabNavLine = styled.span<{ top: number }>`
width: 2px;
height: 48px;
transform: scaleX(1);
top: ${(props) => props.top}px;
top: ${props => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
box-shadow: 0 0 8px 0 rgba(115, 103, 240);
@ -624,6 +620,7 @@ type AdminProps = {
onDeleteUser: (userID: string, newOwnerID: string | null) => void;
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
users: Array<User>;
canInviteUser: boolean;
onUpdateUserPassword: (user: TaskUser, password: string) => void;
};
@ -631,6 +628,7 @@ const Admin: React.FC<AdminProps> = ({
initialTab,
onAddUser,
onUpdateUserPassword,
canInviteUser,
onDeleteUser,
onInviteUser,
users,
@ -675,18 +673,20 @@ const Admin: React.FC<AdminProps> = ({
</ListDesc>
<ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
{canInviteUser && (
<InviteMemberButton
onClick={($target) => {
onClick={$target => {
onAddUser($target);
}}
>
<InviteIcon width={16} height={16} />
New Member
</InviteMemberButton>
)}
</ListActions>
</MemberListHeader>
<MemberList>
{users.map((member) => {
{users.map(member => {
const projectTotal = member.owned.projects.length + member.member.projects.length;
return (
<MemberListItem>
@ -699,7 +699,7 @@ const Admin: React.FC<AdminProps> = ({
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
<MemberItemOption
variant="outline"
onClick={($target) => {
onClick={$target => {
showPopup(
$target,
<TeamRoleManagerPopup
@ -710,7 +710,7 @@ const Admin: React.FC<AdminProps> = ({
onUpdateUserPassword(user, password);
}}
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
onChangeRole={(roleCode) => {
onChangeRole={roleCode => {
updateUserRole({ variables: { userID: member.id, roleCode } });
}}
onDeleteUser={onDeleteUser}

View File

@ -37,17 +37,22 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onClos
type ProfileMenuProps = {
onProfile: () => void;
onLogout: () => void;
showAdminConsole: boolean;
onAdminConsole: () => void;
};
const ProfileMenu: React.FC<ProfileMenuProps> = ({ onAdminConsole, onProfile, onLogout }) => {
const ProfileMenu: React.FC<ProfileMenuProps> = ({ showAdminConsole, onAdminConsole, onProfile, onLogout }) => {
return (
<>
{showAdminConsole && (
<>
<ActionItem onClick={onAdminConsole}>
<Cog size={16} color="#c2c6dc" />
<ActionTitle>Admin Console</ActionTitle>
</ActionItem>
<Separator />
</>
)}
<ActionItem onClick={onProfile}>
<User size={16} color="#c2c6dc" />
<ActionTitle>Profile</ActionTitle>

View File

@ -50,7 +50,6 @@ type MiniProfileProps = {
onRemoveFromTask?: () => void;
onChangeRole?: (roleCode: RoleCode) => void;
onRemoveFromBoard?: () => void;
onChangeProjectOwner?: (userID: string) => void;
warning?: string | null;
canChangeRole?: boolean;
};
@ -58,7 +57,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
user,
bio,
canChangeRole,
onChangeProjectOwner,
onRemoveFromTask,
onChangeRole,
onRemoveFromBoard,
@ -91,15 +89,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
Remove from card
</MiniProfileActionItem>
)}
{onChangeProjectOwner && (
<MiniProfileActionItem
onClick={() => {
setTab(3);
}}
>
Set as project owner
</MiniProfileActionItem>
)}
{onChangeRole && user.role && (
<MiniProfileActionItem
onClick={() => {
@ -193,24 +182,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
</RemoveMemberButton>
</Content>
</Popup>
<Popup title="Set as Project Owner?" onClose={() => hidePopup()} tab={3}>
<Content>
<DeleteDescription>
This will change the project owner from you to this user. 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 (onChangeProjectOwner) {
onChangeProjectOwner(user.id);
}
}}
>
Set as Project Owner
</RemoveMemberButton>
</Content>
</Popup>
</>
);
};

View File

@ -38,6 +38,7 @@ const HomeDashboard = styled(Home)``;
type ProjectHeadingProps = {
onFavorite?: () => void;
name: string;
canEditProjectName: boolean;
onSaveProjectName?: (projectName: string) => void;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
};
@ -46,6 +47,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
onFavorite,
name: initialProjectName,
onSaveProjectName,
canEditProjectName,
onOpenSettings,
}) => {
const [isEditProjectName, setEditProjectName] = useState(false);
@ -94,7 +96,9 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
) : (
<ProjectName
onClick={() => {
if (canEditProjectName) {
setEditProjectName(true);
}
}}
>
{projectName}
@ -142,19 +146,25 @@ type NavBarProps = {
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
onSaveName?: (name: string) => void;
onNotificationClick: () => void;
canEditProjectName?: boolean;
canInviteUser?: boolean;
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
onDashboardClick: () => void;
user: TaskUser | null;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
projectMembers?: Array<TaskUser> | null;
onRemoveFromBoard?: (userID: string) => void;
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
};
const NavBar: React.FC<NavBarProps> = ({
menuType,
canInviteUser = false,
onInviteUser,
onChangeProjectOwner,
currentTab,
onMemberProfile,
canEditProjectName = false,
onOpenProjectFinder,
onFavorite,
onSetTab,
@ -175,47 +185,6 @@ const NavBar: React.FC<NavBarProps> = ({
}
};
const { showPopup } = usePopup();
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
if (member) {
console.log(member);
showPopup(
$targetRef,
<MiniProfile
warning={member.role && member.role.code === 'owner' ? warning : null}
onChangeProjectOwner={
member.role && member.role.code !== 'owner'
? (userID: string) => {
if (user && onChangeProjectOwner) {
onChangeProjectOwner(userID);
}
}
: undefined
}
canChangeRole={member.role && member.role.code !== 'owner'}
onChangeRole={roleCode => {
if (onChangeRole) {
onChangeRole(member.id, roleCode);
}
}}
onRemoveFromBoard={
member.role && member.role.code === 'owner'
? undefined
: () => {
if (onRemoveFromBoard) {
onRemoveFromBoard(member.id);
}
}
}
user={member}
bio=""
/>,
);
}
};
return (
<NavbarWrapper>
<NavbarHeader>
@ -226,6 +195,7 @@ const NavBar: React.FC<NavBarProps> = ({
onFavorite={onFavorite}
onOpenSettings={onOpenSettings}
name={name}
canEditProjectName={canEditProjectName}
onSaveProjectName={onSaveName}
/>
)}
@ -255,7 +225,7 @@ const NavBar: React.FC<NavBarProps> = ({
<TaskcafeTitle>Taskcafé</TaskcafeTitle>
</LogoContainer>
<GlobalActions>
{projectMembers && (
{projectMembers && onMemberProfile && (
<>
<ProjectMembers>
{projectMembers.map((member, idx) => (
@ -268,6 +238,7 @@ const NavBar: React.FC<NavBarProps> = ({
onMemberProfile={onMemberProfile}
/>
))}
{canInviteUser && (
<InviteButton
onClick={$target => {
if (onInviteUser) {
@ -278,6 +249,7 @@ const NavBar: React.FC<NavBarProps> = ({
>
Invite
</InviteButton>
)}
</ProjectMembers>
<NavSeparator />
</>

View File

@ -17,6 +17,7 @@ export type Scalars = {
export enum RoleCode {
Owner = 'owner',
Admin = 'admin',
@ -125,7 +126,6 @@ export type Project = {
createdAt: Scalars['Time'];
name: Scalars['String'];
team: Team;
owner: Member;
taskGroups: Array<TaskGroup>;
members: Array<Member>;
labels: Array<ProjectLabel>;
@ -192,6 +192,24 @@ export type TaskChecklist = {
items: Array<TaskChecklistItem>;
};
export enum RoleLevel {
Admin = 'ADMIN',
Member = 'MEMBER'
}
export enum ActionLevel {
Org = 'ORG',
Team = 'TEAM',
Project = 'PROJECT'
}
export enum ObjectType {
Org = 'ORG',
Team = 'TEAM',
Project = 'PROJECT',
Task = 'TASK'
}
export type Query = {
__typename?: 'Query';
organizations: Array<Organization>;
@ -204,7 +222,7 @@ export type Query = {
teams: Array<Team>;
labelColors: Array<LabelColor>;
taskGroups: Array<TaskGroup>;
me: UserAccount;
me: MePayload;
};
@ -260,10 +278,8 @@ export type Mutation = {
deleteUserAccount: DeleteUserAccountPayload;
logoutUser: Scalars['Boolean'];
removeTaskLabel: Task;
setProjectOwner: SetProjectOwnerPayload;
setTaskChecklistItemComplete: TaskChecklistItem;
setTaskComplete: Task;
setTeamOwner: SetTeamOwnerPayload;
toggleTaskLabel: ToggleTaskLabelPayload;
unassignTask: Task;
updateProjectLabel: ProjectLabel;
@ -412,11 +428,6 @@ export type MutationRemoveTaskLabelArgs = {
};
export type MutationSetProjectOwnerArgs = {
input: SetProjectOwner;
};
export type MutationSetTaskChecklistItemCompleteArgs = {
input: SetTaskChecklistItemComplete;
};
@ -427,11 +438,6 @@ export type MutationSetTaskCompleteArgs = {
};
export type MutationSetTeamOwnerArgs = {
input: SetTeamOwner;
};
export type MutationToggleTaskLabelArgs = {
input: ToggleTaskLabelInput;
};
@ -531,6 +537,25 @@ export type MutationUpdateUserRoleArgs = {
input: UpdateUserRole;
};
export type TeamRole = {
__typename?: 'TeamRole';
teamID: Scalars['UUID'];
roleCode: RoleCode;
};
export type ProjectRole = {
__typename?: 'ProjectRole';
projectID: Scalars['UUID'];
roleCode: RoleCode;
};
export type MePayload = {
__typename?: 'MePayload';
user: UserAccount;
teamRoles: Array<TeamRole>;
projectRoles: Array<ProjectRole>;
};
export type ProjectsFilter = {
teamID?: Maybe<Scalars['UUID']>;
};
@ -540,7 +565,7 @@ export type FindUser = {
};
export type FindProject = {
projectId: Scalars['String'];
projectID: Scalars['UUID'];
};
export type FindTask = {
@ -633,18 +658,6 @@ export type UpdateProjectMemberRolePayload = {
member: Member;
};
export type SetProjectOwner = {
projectID: Scalars['UUID'];
ownerID: Scalars['UUID'];
};
export type SetProjectOwnerPayload = {
__typename?: 'SetProjectOwnerPayload';
ok: Scalars['Boolean'];
prevOwner: Member;
newOwner: Member;
};
export type NewTask = {
taskGroupID: Scalars['String'];
name: Scalars['String'];
@ -868,19 +881,8 @@ export type UpdateTeamMemberRole = {
export type UpdateTeamMemberRolePayload = {
__typename?: 'UpdateTeamMemberRolePayload';
ok: Scalars['Boolean'];
member: Member;
};
export type SetTeamOwner = {
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
};
export type SetTeamOwnerPayload = {
__typename?: 'SetTeamOwnerPayload';
ok: Scalars['Boolean'];
prevOwner: Member;
newOwner: Member;
member: Member;
};
export type UpdateUserPassword = {
@ -1066,7 +1068,7 @@ export type DeleteTaskGroupMutation = (
);
export type FindProjectQueryVariables = {
projectId: Scalars['String'];
projectID: Scalars['UUID'];
};
@ -1075,7 +1077,10 @@ export type FindProjectQuery = (
& { findProject: (
{ __typename?: 'Project' }
& Pick<Project, 'name'>
& { members: Array<(
& { team: (
{ __typename?: 'Team' }
& Pick<Team, 'id'>
), members: Array<(
{ __typename?: 'Member' }
& Pick<Member, 'id' | 'fullName' | 'username'>
& { role: (
@ -1242,12 +1247,21 @@ export type MeQueryVariables = {};
export type MeQuery = (
{ __typename?: 'Query' }
& { me: (
{ __typename?: 'MePayload' }
& { user: (
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
) }
), teamRoles: Array<(
{ __typename?: 'TeamRole' }
& Pick<TeamRole, 'teamID' | 'roleCode'>
)>, projectRoles: Array<(
{ __typename?: 'ProjectRole' }
& Pick<ProjectRole, 'projectID' | 'roleCode'>
)> }
) }
);
@ -1311,35 +1325,6 @@ export type DeleteProjectMemberMutation = (
) }
);
export type SetProjectOwnerMutationVariables = {
projectID: Scalars['UUID'];
ownerID: Scalars['UUID'];
};
export type SetProjectOwnerMutation = (
{ __typename?: 'Mutation' }
& { setProjectOwner: (
{ __typename?: 'SetProjectOwnerPayload' }
& Pick<SetProjectOwnerPayload, 'ok'>
& { newOwner: (
{ __typename?: 'Member' }
& Pick<Member, 'id'>
& { role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
) }
), prevOwner: (
{ __typename?: 'Member' }
& Pick<Member, 'id'>
& { role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
) }
) }
) }
);
export type UpdateProjectMemberRoleMutationVariables = {
projectID: Scalars['UUID'];
userID: Scalars['UUID'];
@ -1706,6 +1691,29 @@ export type GetTeamQuery = (
)> }
);
export type UpdateTeamMemberRoleMutationVariables = {
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
roleCode: RoleCode;
};
export type UpdateTeamMemberRoleMutation = (
{ __typename?: 'Mutation' }
& { updateTeamMemberRole: (
{ __typename?: 'UpdateTeamMemberRolePayload' }
& Pick<UpdateTeamMemberRolePayload, 'teamID'>
& { member: (
{ __typename?: 'Member' }
& Pick<Member, 'id'>
& { role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
) }
) }
) }
);
export type ToggleTaskLabelMutationVariables = {
taskID: Scalars['UUID'];
projectLabelID: Scalars['UUID'];
@ -2325,9 +2333,12 @@ export type DeleteTaskGroupMutationHookResult = ReturnType<typeof useDeleteTaskG
export type DeleteTaskGroupMutationResult = ApolloReactCommon.MutationResult<DeleteTaskGroupMutation>;
export type DeleteTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>;
export const FindProjectDocument = gql`
query findProject($projectId: String!) {
findProject(input: {projectId: $projectId}) {
query findProject($projectID: UUID!) {
findProject(input: {projectID: $projectID}) {
name
team {
id
}
members {
id
fullName
@ -2418,7 +2429,7 @@ export const FindProjectDocument = gql`
* @example
* const { data, loading, error } = useFindProjectQuery({
* variables: {
* projectId: // value for 'projectId'
* projectID: // value for 'projectID'
* },
* });
*/
@ -2563,6 +2574,7 @@ export type GetProjectsQueryResult = ApolloReactCommon.QueryResult<GetProjectsQu
export const MeDocument = gql`
query me {
me {
user {
id
fullName
profileIcon {
@ -2571,6 +2583,15 @@ export const MeDocument = gql`
url
}
}
teamRoles {
teamID
roleCode
}
projectRoles {
projectID
roleCode
}
}
}
`;
@ -2717,53 +2738,6 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu
export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>;
export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>;
export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMemberMutation, DeleteProjectMemberMutationVariables>;
export const SetProjectOwnerDocument = gql`
mutation setProjectOwner($projectID: UUID!, $ownerID: UUID!) {
setProjectOwner(input: {projectID: $projectID, ownerID: $ownerID}) {
ok
newOwner {
id
role {
code
name
}
}
prevOwner {
id
role {
code
name
}
}
}
}
`;
export type SetProjectOwnerMutationFn = ApolloReactCommon.MutationFunction<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>;
/**
* __useSetProjectOwnerMutation__
*
* To run a mutation, you first call `useSetProjectOwnerMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSetProjectOwnerMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [setProjectOwnerMutation, { data, loading, error }] = useSetProjectOwnerMutation({
* variables: {
* projectID: // value for 'projectID'
* ownerID: // value for 'ownerID'
* },
* });
*/
export function useSetProjectOwnerMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>) {
return ApolloReactHooks.useMutation<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>(SetProjectOwnerDocument, baseOptions);
}
export type SetProjectOwnerMutationHookResult = ReturnType<typeof useSetProjectOwnerMutation>;
export type SetProjectOwnerMutationResult = ApolloReactCommon.MutationResult<SetProjectOwnerMutation>;
export type SetProjectOwnerMutationOptions = ApolloReactCommon.BaseMutationOptions<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>;
export const UpdateProjectMemberRoleDocument = gql`
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) {
@ -3510,6 +3484,47 @@ export function useGetTeamLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHook
export type GetTeamQueryHookResult = ReturnType<typeof useGetTeamQuery>;
export type GetTeamLazyQueryHookResult = ReturnType<typeof useGetTeamLazyQuery>;
export type GetTeamQueryResult = ApolloReactCommon.QueryResult<GetTeamQuery, GetTeamQueryVariables>;
export const UpdateTeamMemberRoleDocument = gql`
mutation updateTeamMemberRole($teamID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
updateTeamMemberRole(input: {teamID: $teamID, userID: $userID, roleCode: $roleCode}) {
member {
id
role {
code
name
}
}
teamID
}
}
`;
export type UpdateTeamMemberRoleMutationFn = ApolloReactCommon.MutationFunction<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>;
/**
* __useUpdateTeamMemberRoleMutation__
*
* To run a mutation, you first call `useUpdateTeamMemberRoleMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTeamMemberRoleMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateTeamMemberRoleMutation, { data, loading, error }] = useUpdateTeamMemberRoleMutation({
* variables: {
* teamID: // value for 'teamID'
* userID: // value for 'userID'
* roleCode: // value for 'roleCode'
* },
* });
*/
export function useUpdateTeamMemberRoleMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>(UpdateTeamMemberRoleDocument, baseOptions);
}
export type UpdateTeamMemberRoleMutationHookResult = ReturnType<typeof useUpdateTeamMemberRoleMutation>;
export type UpdateTeamMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateTeamMemberRoleMutation>;
export type UpdateTeamMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>;
export const ToggleTaskLabelDocument = gql`
mutation toggleTaskLabel($taskID: UUID!, $projectLabelID: UUID!) {
toggleTaskLabel(input: {taskID: $taskID, projectLabelID: $projectLabelID}) {

View File

@ -2,9 +2,12 @@ import gql from 'graphql-tag';
import TASK_FRAGMENT from './fragments/task';
const FIND_PROJECT_QUERY = gql`
query findProject($projectId: String!) {
findProject(input: { projectId: $projectId }) {
query findProject($projectID: UUID!) {
findProject(input: { projectID: $projectID }) {
name
team {
id
}
members {
id
fullName

View File

@ -1,5 +1,6 @@
query me {
me {
user {
id
fullName
profileIcon {
@ -8,4 +9,13 @@ query me {
url
}
}
teamRoles {
teamID
roleCode
}
projectRoles {
projectID
roleCode
}
}
}

View File

@ -1,25 +0,0 @@
import gql from 'graphql-tag';
export const SET_PROJECT_OWNER_MUTATION = gql`
mutation setProjectOwner($projectID: UUID!, $ownerID: UUID!) {
setProjectOwner(input: { projectID: $projectID, ownerID: $ownerID }) {
ok
newOwner {
id
role {
code
name
}
}
prevOwner {
id
role {
code
name
}
}
}
}
`;
export default SET_PROJECT_OWNER_MUTATION;

View File

@ -0,0 +1,18 @@
import gql from 'graphql-tag';
export const UPDATE_TEAM_MEMBER_ROLE_MUTATION = gql`
mutation updateTeamMemberRole($teamID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
updateTeamMemberRole(input: { teamID: $teamID, userID: $userID, roleCode: $roleCode }) {
member {
id
role {
code
name
}
}
teamID
}
}
`;
export default UPDATE_TEAM_MEMBER_ROLE_MUTATION;

View File

@ -0,0 +1,5 @@
import { PermissionObjectType, PermissionLevel } from 'App/context';
export default function userCan(level: PermissionLevel, objectType: PermissionObjectType) {
return false;
}

View File

@ -1,5 +1,6 @@
interface JWTToken {
userId: string;
orgRole: string;
iat: string;
exp: string;
}

1
go.mod
View File

@ -17,7 +17,6 @@ require (
github.com/google/uuid v1.1.1
github.com/jmoiron/sqlx v1.2.0
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
github.com/jordanknott/project-citadel v0.0.0-20200801000017-7ffd0ff6b895
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/lib/pq v1.3.0
github.com/magefile/mage v1.9.0

View File

@ -16,9 +16,17 @@ const (
InstallOnly = "install_only"
)
type Role string
const (
RoleAdmin Role = "admin"
RoleMember Role = "member"
)
type AccessTokenClaims struct {
UserID string `json:"userId"`
Restricted RestrictedMode `json:"restricted"`
OrgRole Role `json:"orgRole"`
jwt.StandardClaims
}
@ -39,11 +47,16 @@ func (r *ErrMalformedToken) Error() string {
return "token is malformed"
}
func NewAccessToken(userID string, restrictedMode RestrictedMode) (string, error) {
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string) (string, error) {
role := RoleMember
if orgRole == "admin" {
role = RoleAdmin
}
accessExpirationTime := time.Now().Add(5 * time.Second)
accessClaims := &AccessTokenClaims{
UserID: userID,
Restricted: restrictedMode,
OrgRole: role,
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
}
@ -60,6 +73,7 @@ func NewAccessTokenCustomExpiration(userID string, dur time.Duration) (string, e
accessClaims := &AccessTokenClaims{
UserID: userID,
Restricted: Unrestricted,
OrgRole: RoleMember,
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
}

View File

@ -35,6 +35,6 @@ var rootCmd = &cobra.Command{
func Execute() {
rootCmd.SetVersionTemplate(versionTemplate)
rootCmd.AddCommand(newWebCmd(), newMigrateCmd())
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd())
rootCmd.Execute()
}

View File

@ -2,6 +2,8 @@ package commands
import (
"fmt"
"net/http"
"github.com/spf13/cobra"
"github.com/golang-migrate/migrate/v4"
@ -10,7 +12,6 @@ import (
"github.com/golang-migrate/migrate/v4/source/httpfs"
"github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/migrations"
log "github.com/sirupsen/logrus"
)
@ -27,6 +28,12 @@ func (l *MigrateLog) Verbose() bool {
return l.verbose
}
var migration http.FileSystem
func init() {
migration = http.Dir("./migrations")
}
func newMigrateCmd() *cobra.Command {
return &cobra.Command{
Use: "migrate",
@ -53,7 +60,7 @@ func newMigrateCmd() *cobra.Command {
return err
}
src, err := httpfs.New(migrations.Migrations, "./")
src, err := httpfs.New(migration, "./")
if err != nil {
return err
}

View File

@ -0,0 +1,13 @@
// +build prod
package commands
import (
"fmt"
"github.com/jordanknott/taskcafe/internal/migrations"
)
func init() {
migration = migrations.Migrations
}

View File

@ -0,0 +1,27 @@
package commands
import (
"fmt"
"time"
"github.com/jordanknott/taskcafe/internal/auth"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func newTokenCmd() *cobra.Command {
return &cobra.Command{
Use: "token",
Short: "Create a long lived JWT token for dev purposes",
Long: "Create a long lived JWT token for dev purposes",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
token, err := auth.NewAccessTokenCustomExpiration(args[0], time.Hour*24)
if err != nil {
log.WithError(err).Error("issue while creating access token")
return
}
fmt.Println(token)
},
}
}

View File

@ -27,7 +27,6 @@ type Project struct {
TeamID uuid.UUID `json:"team_id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Owner uuid.UUID `json:"owner"`
}
type ProjectLabel struct {
@ -120,7 +119,6 @@ type Team struct {
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
OrganizationID uuid.UUID `json:"organization_id"`
Owner uuid.UUID `json:"owner"`
}
type TeamMember struct {

View File

@ -11,30 +11,23 @@ import (
)
const createProject = `-- name: CreateProject :one
INSERT INTO project(owner, team_id, created_at, name) VALUES ($1, $2, $3, $4) RETURNING project_id, team_id, created_at, name, owner
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING project_id, team_id, created_at, name
`
type CreateProjectParams struct {
Owner uuid.UUID `json:"owner"`
TeamID uuid.UUID `json:"team_id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
}
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
row := q.db.QueryRowContext(ctx, createProject,
arg.Owner,
arg.TeamID,
arg.CreatedAt,
arg.Name,
)
row := q.db.QueryRowContext(ctx, createProject, arg.TeamID, arg.CreatedAt, arg.Name)
var i Project
err := row.Scan(
&i.ProjectID,
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
)
return i, err
}
@ -93,7 +86,7 @@ func (q *Queries) DeleteProjectMember(ctx context.Context, arg DeleteProjectMemb
}
const getAllProjects = `-- name: GetAllProjects :many
SELECT project_id, team_id, created_at, name, owner FROM project
SELECT project_id, team_id, created_at, name FROM project
`
func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
@ -110,7 +103,6 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
); err != nil {
return nil, err
}
@ -126,7 +118,7 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
}
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many
SELECT project_id, team_id, created_at, name, owner FROM project WHERE team_id = $1
SELECT project_id, team_id, created_at, name FROM project WHERE team_id = $1
`
func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) {
@ -143,7 +135,39 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) (
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllVisibleProjectsForUserID = `-- name: GetAllVisibleProjectsForUserID :many
SELECT project.project_id, project.team_id, project.created_at, project.name FROM project LEFT JOIN
project_member ON project_member.project_id = project.project_id WHERE project_member.user_id = $1
`
func (q *Queries) GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error) {
rows, err := q.db.QueryContext(ctx, getAllVisibleProjectsForUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Project
for rows.Next() {
var i Project
if err := rows.Scan(
&i.ProjectID,
&i.TeamID,
&i.CreatedAt,
&i.Name,
); err != nil {
return nil, err
}
@ -185,73 +209,8 @@ func (q *Queries) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.
return items, nil
}
const getOwnedProjectsForUserID = `-- name: GetOwnedProjectsForUserID :many
SELECT project_id, team_id, created_at, name, owner FROM project WHERE owner = $1
`
func (q *Queries) GetOwnedProjectsForUserID(ctx context.Context, owner uuid.UUID) ([]Project, error) {
rows, err := q.db.QueryContext(ctx, getOwnedProjectsForUserID, owner)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Project
for rows.Next() {
var i Project
if err := rows.Scan(
&i.ProjectID,
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOwnedTeamProjectsForUserID = `-- name: GetOwnedTeamProjectsForUserID :many
SELECT project_id FROM project WHERE owner = $1 AND team_id = $2
`
type GetOwnedTeamProjectsForUserIDParams struct {
Owner uuid.UUID `json:"owner"`
TeamID uuid.UUID `json:"team_id"`
}
func (q *Queries) GetOwnedTeamProjectsForUserID(ctx context.Context, arg GetOwnedTeamProjectsForUserIDParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, getOwnedTeamProjectsForUserID, arg.Owner, arg.TeamID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var project_id uuid.UUID
if err := rows.Scan(&project_id); err != nil {
return nil, err
}
items = append(items, project_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProjectByID = `-- name: GetProjectByID :one
SELECT project_id, team_id, created_at, name, owner FROM project WHERE project_id = $1
SELECT project_id, team_id, created_at, name FROM project WHERE project_id = $1
`
func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) {
@ -262,7 +221,6 @@ func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Proj
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
)
return i, err
}
@ -300,6 +258,38 @@ func (q *Queries) GetProjectMembersForProjectID(ctx context.Context, projectID u
return items, nil
}
const getProjectRolesForUserID = `-- name: GetProjectRolesForUserID :many
SELECT project_id, role_code FROM project_member WHERE user_id = $1
`
type GetProjectRolesForUserIDRow struct {
ProjectID uuid.UUID `json:"project_id"`
RoleCode string `json:"role_code"`
}
func (q *Queries) GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error) {
rows, err := q.db.QueryContext(ctx, getProjectRolesForUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetProjectRolesForUserIDRow
for rows.Next() {
var i GetProjectRolesForUserIDRow
if err := rows.Scan(&i.ProjectID, &i.RoleCode); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getRoleForProjectMemberByUserID = `-- name: GetRoleForProjectMemberByUserID :one
SELECT code, role.name FROM project_member INNER JOIN role ON role.code = project_member.role_code
WHERE user_id = $1 AND project_id = $2
@ -317,25 +307,29 @@ func (q *Queries) GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRo
return i, err
}
const setProjectOwner = `-- name: SetProjectOwner :one
UPDATE project SET owner = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name, owner
const getUserRolesForProject = `-- name: GetUserRolesForProject :one
SELECT p.team_id, COALESCE(tm.role_code, '') AS team_role, COALESCE(pm.role_code, '') AS project_role
FROM project AS p
LEFT JOIN project_member AS pm ON pm.project_id = p.project_id AND pm.user_id = $1
LEFT JOIN team_member AS tm ON tm.team_id = p.team_id AND tm.user_id = $1
WHERE p.project_id = $2
`
type SetProjectOwnerParams struct {
type GetUserRolesForProjectParams struct {
UserID uuid.UUID `json:"user_id"`
ProjectID uuid.UUID `json:"project_id"`
Owner uuid.UUID `json:"owner"`
}
func (q *Queries) SetProjectOwner(ctx context.Context, arg SetProjectOwnerParams) (Project, error) {
row := q.db.QueryRowContext(ctx, setProjectOwner, arg.ProjectID, arg.Owner)
var i Project
err := row.Scan(
&i.ProjectID,
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
)
type GetUserRolesForProjectRow struct {
TeamID uuid.UUID `json:"team_id"`
TeamRole string `json:"team_role"`
ProjectRole string `json:"project_role"`
}
func (q *Queries) GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error) {
row := q.db.QueryRowContext(ctx, getUserRolesForProject, arg.UserID, arg.ProjectID)
var i GetUserRolesForProjectRow
err := row.Scan(&i.TeamID, &i.TeamRole, &i.ProjectRole)
return i, err
}
@ -364,7 +358,7 @@ func (q *Queries) UpdateProjectMemberRole(ctx context.Context, arg UpdateProject
}
const updateProjectNameByID = `-- name: UpdateProjectNameByID :one
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name, owner
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name
`
type UpdateProjectNameByIDParams struct {
@ -380,39 +374,6 @@ func (q *Queries) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNa
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
)
return i, err
}
const updateProjectOwnerByOwnerID = `-- name: UpdateProjectOwnerByOwnerID :many
UPDATE project SET owner = $2 WHERE owner = $1 RETURNING project_id
`
type UpdateProjectOwnerByOwnerIDParams struct {
Owner uuid.UUID `json:"owner"`
Owner_2 uuid.UUID `json:"owner_2"`
}
func (q *Queries) UpdateProjectOwnerByOwnerID(ctx context.Context, arg UpdateProjectOwnerByOwnerIDParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, updateProjectOwnerByOwnerID, arg.Owner, arg.Owner_2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var project_id uuid.UUID
if err := rows.Scan(&project_id); err != nil {
return nil, err
}
items = append(items, project_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -49,19 +49,18 @@ type Querier interface {
GetAllTasks(ctx context.Context) ([]Task, error)
GetAllTeams(ctx context.Context) ([]Team, error)
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
GetLabelColors(ctx context.Context) ([]LabelColor, error)
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
GetOwnedProjectsForUserID(ctx context.Context, owner uuid.UUID) ([]Project, error)
GetOwnedTeamProjectsForUserID(ctx context.Context, arg GetOwnedTeamProjectsForUserIDParams) ([]uuid.UUID, error)
GetOwnedTeamsForUserID(ctx context.Context, owner uuid.UUID) ([]Team, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
GetProjectMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectMember, error)
GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (Role, error)
@ -81,21 +80,22 @@ type Querier interface {
GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error)
GetTeamMemberByID(ctx context.Context, arg GetTeamMemberByIDParams) (TeamMember, error)
GetTeamMembersForTeamID(ctx context.Context, teamID uuid.UUID) ([]TeamMember, error)
GetTeamRoleForUserID(ctx context.Context, arg GetTeamRoleForUserIDParams) (GetTeamRoleForUserIDRow, error)
GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error)
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
SetProjectOwner(ctx context.Context, arg SetProjectOwnerParams) (Project, error)
GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error)
SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
SetTeamOwner(ctx context.Context, arg SetTeamOwnerParams) (Team, error)
SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error)
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error)
UpdateProjectOwnerByOwnerID(ctx context.Context, arg UpdateProjectOwnerByOwnerIDParams) ([]uuid.UUID, error)
UpdateTaskChecklistItemLocation(ctx context.Context, arg UpdateTaskChecklistItemLocationParams) (TaskChecklistItem, error)
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
@ -106,7 +106,6 @@ type Querier interface {
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
UpdateTeamOwnerByOwnerID(ctx context.Context, arg UpdateTeamOwnerByOwnerIDParams) ([]uuid.UUID, error)
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
}

View File

@ -8,10 +8,7 @@ SELECT * FROM project WHERE team_id = $1;
SELECT * FROM project WHERE project_id = $1;
-- name: CreateProject :one
INSERT INTO project(owner, team_id, created_at, name) VALUES ($1, $2, $3, $4) RETURNING *;
-- name: SetProjectOwner :one
UPDATE project SET owner = $2 WHERE project_id = $1 RETURNING *;
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING *;
-- name: UpdateProjectNameByID :one
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING *;
@ -37,14 +34,19 @@ DELETE FROM project_member WHERE user_id = $1 AND project_id = $2;
UPDATE project_member SET role_code = $3 WHERE project_id = $1 AND user_id = $2
RETURNING *;
-- name: GetOwnedTeamProjectsForUserID :many
SELECT project_id FROM project WHERE owner = $1 AND team_id = $2;
-- name: GetOwnedProjectsForUserID :many
SELECT * FROM project WHERE owner = $1;
-- name: GetProjectRolesForUserID :many
SELECT project_id, role_code FROM project_member WHERE user_id = $1;
-- name: GetMemberProjectIDsForUserID :many
SELECT project_id FROM project_member WHERE user_id = $1;
-- name: UpdateProjectOwnerByOwnerID :many
UPDATE project SET owner = $2 WHERE owner = $1 RETURNING project_id;
-- name: GetAllVisibleProjectsForUserID :many
SELECT project.* FROM project LEFT JOIN
project_member ON project_member.project_id = project.project_id WHERE project_member.user_id = $1;
-- name: GetUserRolesForProject :one
SELECT p.team_id, COALESCE(tm.role_code, '') AS team_role, COALESCE(pm.role_code, '') AS project_role
FROM project AS p
LEFT JOIN project_member AS pm ON pm.project_id = p.project_id AND pm.user_id = $1
LEFT JOIN team_member AS tm ON tm.team_id = p.team_id AND tm.user_id = $1
WHERE p.project_id = $2;

View File

@ -5,7 +5,7 @@ SELECT * FROM team;
SELECT * FROM team WHERE team_id = $1;
-- name: CreateTeam :one
INSERT INTO team (organization_id, created_at, name, owner) VALUES ($1, $2, $3, $4) RETURNING *;
INSERT INTO team (organization_id, created_at, name) VALUES ($1, $2, $3) RETURNING *;
-- name: DeleteTeamByID :exec
DELETE FROM team WHERE team_id = $1;
@ -13,14 +13,15 @@ DELETE FROM team WHERE team_id = $1;
-- name: GetTeamsForOrganization :many
SELECT * FROM team WHERE organization_id = $1;
-- name: SetTeamOwner :one
UPDATE team SET owner = $2 WHERE team_id = $1 RETURNING *;
-- name: GetOwnedTeamsForUserID :many
SELECT * FROM team WHERE owner = $1;
-- name: GetMemberTeamIDsForUserID :many
SELECT team_id FROM team_member WHERE user_id = $1;
-- name: UpdateTeamOwnerByOwnerID :many
UPDATE team SET owner = $2 WHERE owner = $1 RETURNING team_id;
-- name: GetTeamRoleForUserID :one
SELECT team_id, role_code FROM team_member WHERE user_id = $1 AND team_id = $2;
-- name: GetTeamRolesForUserID :many
SELECT team_id, role_code FROM team_member WHERE user_id = $1;
-- name: GetTeamsForUserIDWhereAdmin :many
SELECT team.* FROM team_member INNER JOIN team
ON team.team_id = team_member.team_id WHERE (role_code = 'admin' OR role_code = 'member') AND user_id = $1;

View File

@ -11,30 +11,23 @@ import (
)
const createTeam = `-- name: CreateTeam :one
INSERT INTO team (organization_id, created_at, name, owner) VALUES ($1, $2, $3, $4) RETURNING team_id, created_at, name, organization_id, owner
INSERT INTO team (organization_id, created_at, name) VALUES ($1, $2, $3) RETURNING team_id, created_at, name, organization_id
`
type CreateTeamParams struct {
OrganizationID uuid.UUID `json:"organization_id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Owner uuid.UUID `json:"owner"`
}
func (q *Queries) CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error) {
row := q.db.QueryRowContext(ctx, createTeam,
arg.OrganizationID,
arg.CreatedAt,
arg.Name,
arg.Owner,
)
row := q.db.QueryRowContext(ctx, createTeam, arg.OrganizationID, arg.CreatedAt, arg.Name)
var i Team
err := row.Scan(
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.OrganizationID,
&i.Owner,
)
return i, err
}
@ -49,7 +42,7 @@ func (q *Queries) DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error {
}
const getAllTeams = `-- name: GetAllTeams :many
SELECT team_id, created_at, name, organization_id, owner FROM team
SELECT team_id, created_at, name, organization_id FROM team
`
func (q *Queries) GetAllTeams(ctx context.Context) ([]Team, error) {
@ -66,7 +59,6 @@ func (q *Queries) GetAllTeams(ctx context.Context) ([]Team, error) {
&i.CreatedAt,
&i.Name,
&i.OrganizationID,
&i.Owner,
); err != nil {
return nil, err
}
@ -108,26 +100,62 @@ func (q *Queries) GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUI
return items, nil
}
const getOwnedTeamsForUserID = `-- name: GetOwnedTeamsForUserID :many
SELECT team_id, created_at, name, organization_id, owner FROM team WHERE owner = $1
const getTeamByID = `-- name: GetTeamByID :one
SELECT team_id, created_at, name, organization_id FROM team WHERE team_id = $1
`
func (q *Queries) GetOwnedTeamsForUserID(ctx context.Context, owner uuid.UUID) ([]Team, error) {
rows, err := q.db.QueryContext(ctx, getOwnedTeamsForUserID, owner)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Team
for rows.Next() {
func (q *Queries) GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error) {
row := q.db.QueryRowContext(ctx, getTeamByID, teamID)
var i Team
if err := rows.Scan(
err := row.Scan(
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.OrganizationID,
&i.Owner,
); err != nil {
)
return i, err
}
const getTeamRoleForUserID = `-- name: GetTeamRoleForUserID :one
SELECT team_id, role_code FROM team_member WHERE user_id = $1 AND team_id = $2
`
type GetTeamRoleForUserIDParams struct {
UserID uuid.UUID `json:"user_id"`
TeamID uuid.UUID `json:"team_id"`
}
type GetTeamRoleForUserIDRow struct {
TeamID uuid.UUID `json:"team_id"`
RoleCode string `json:"role_code"`
}
func (q *Queries) GetTeamRoleForUserID(ctx context.Context, arg GetTeamRoleForUserIDParams) (GetTeamRoleForUserIDRow, error) {
row := q.db.QueryRowContext(ctx, getTeamRoleForUserID, arg.UserID, arg.TeamID)
var i GetTeamRoleForUserIDRow
err := row.Scan(&i.TeamID, &i.RoleCode)
return i, err
}
const getTeamRolesForUserID = `-- name: GetTeamRolesForUserID :many
SELECT team_id, role_code FROM team_member WHERE user_id = $1
`
type GetTeamRolesForUserIDRow struct {
TeamID uuid.UUID `json:"team_id"`
RoleCode string `json:"role_code"`
}
func (q *Queries) GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error) {
rows, err := q.db.QueryContext(ctx, getTeamRolesForUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTeamRolesForUserIDRow
for rows.Next() {
var i GetTeamRolesForUserIDRow
if err := rows.Scan(&i.TeamID, &i.RoleCode); err != nil {
return nil, err
}
items = append(items, i)
@ -141,25 +169,8 @@ func (q *Queries) GetOwnedTeamsForUserID(ctx context.Context, owner uuid.UUID) (
return items, nil
}
const getTeamByID = `-- name: GetTeamByID :one
SELECT team_id, created_at, name, organization_id, owner FROM team WHERE team_id = $1
`
func (q *Queries) GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error) {
row := q.db.QueryRowContext(ctx, getTeamByID, teamID)
var i Team
err := row.Scan(
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.OrganizationID,
&i.Owner,
)
return i, err
}
const getTeamsForOrganization = `-- name: GetTeamsForOrganization :many
SELECT team_id, created_at, name, organization_id, owner FROM team WHERE organization_id = $1
SELECT team_id, created_at, name, organization_id FROM team WHERE organization_id = $1
`
func (q *Queries) GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error) {
@ -176,7 +187,6 @@ func (q *Queries) GetTeamsForOrganization(ctx context.Context, organizationID uu
&i.CreatedAt,
&i.Name,
&i.OrganizationID,
&i.Owner,
); err != nil {
return nil, err
}
@ -191,50 +201,29 @@ func (q *Queries) GetTeamsForOrganization(ctx context.Context, organizationID uu
return items, nil
}
const setTeamOwner = `-- name: SetTeamOwner :one
UPDATE team SET owner = $2 WHERE team_id = $1 RETURNING team_id, created_at, name, organization_id, owner
const getTeamsForUserIDWhereAdmin = `-- name: GetTeamsForUserIDWhereAdmin :many
SELECT team.team_id, team.created_at, team.name, team.organization_id FROM team_member INNER JOIN team
ON team.team_id = team_member.team_id WHERE (role_code = 'admin' OR role_code = 'member') AND user_id = $1
`
type SetTeamOwnerParams struct {
TeamID uuid.UUID `json:"team_id"`
Owner uuid.UUID `json:"owner"`
}
func (q *Queries) SetTeamOwner(ctx context.Context, arg SetTeamOwnerParams) (Team, error) {
row := q.db.QueryRowContext(ctx, setTeamOwner, arg.TeamID, arg.Owner)
var i Team
err := row.Scan(
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.OrganizationID,
&i.Owner,
)
return i, err
}
const updateTeamOwnerByOwnerID = `-- name: UpdateTeamOwnerByOwnerID :many
UPDATE team SET owner = $2 WHERE owner = $1 RETURNING team_id
`
type UpdateTeamOwnerByOwnerIDParams struct {
Owner uuid.UUID `json:"owner"`
Owner_2 uuid.UUID `json:"owner_2"`
}
func (q *Queries) UpdateTeamOwnerByOwnerID(ctx context.Context, arg UpdateTeamOwnerByOwnerIDParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, updateTeamOwnerByOwnerID, arg.Owner, arg.Owner_2)
func (q *Queries) GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error) {
rows, err := q.db.QueryContext(ctx, getTeamsForUserIDWhereAdmin, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
var items []Team
for rows.Next() {
var team_id uuid.UUID
if err := rows.Scan(&team_id); err != nil {
var i Team
if err := rows.Scan(
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.OrganizationID,
); err != nil {
return nil, err
}
items = append(items, team_id)
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,13 @@ package graph
import (
"context"
"errors"
"net/http"
"os"
"reflect"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/lru"
@ -15,16 +18,71 @@ import (
"github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// NewHandler returns a new graphql endpoint handler.
func NewHandler(config config.AppConfig, repo db.Repository) http.Handler {
srv := handler.New(NewExecutableSchema(Config{
c := Config{
Resolvers: &Resolver{
Config: config,
Repository: repo,
},
}))
}
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
role, ok := GetUserRole(ctx)
if !ok {
return nil, errors.New("user ID is missing")
}
if role == "admin" {
return next(ctx)
} else if level == ActionLevelOrg {
return nil, errors.New("must be an org admin")
}
var subjectID uuid.UUID
in := graphql.GetResolverContext(ctx).Args["input"]
if typeArg == ObjectTypeProject || typeArg == ObjectTypeTeam {
val := reflect.ValueOf(in) // could be any underlying type
fieldName := "ProjectID"
if typeArg == ObjectTypeTeam {
fieldName = "TeamID"
}
subjectID, ok = val.FieldByName(fieldName).Interface().(uuid.UUID)
if !ok {
return nil, errors.New("error while casting subject uuid")
}
}
if level == ActionLevelProject {
roles, err := GetProjectRoles(ctx, repo, subjectID)
if err != nil {
return nil, err
}
if roles.TeamRole == "admin" || roles.ProjectRole == "admin" {
log.WithFields(log.Fields{"teamRole": roles.TeamRole, "projectRole": roles.ProjectRole}).Info("is team or project role")
return next(ctx)
}
return nil, errors.New("must be a team or project admin")
} else if level == ActionLevelTeam {
userID, ok := GetUserID(ctx)
if !ok {
return nil, errors.New("user id is missing")
}
role, err := repo.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: subjectID})
if err != nil {
return nil, err
}
if role.RoleCode == "admin" {
return next(ctx)
}
return nil, errors.New("must be a team admin")
}
return nil, errors.New("invalid path")
}
srv := handler.New(NewExecutableSchema(c))
srv.AddTransport(transport.Websocket{
KeepAlivePingInterval: 10 * time.Second,
})
@ -55,8 +113,80 @@ func GetUserID(ctx context.Context) (uuid.UUID, bool) {
userID, ok := ctx.Value("userID").(uuid.UUID)
return userID, ok
}
func GetUserRole(ctx context.Context) (auth.Role, bool) {
role, ok := ctx.Value("org_role").(auth.Role)
return role, ok
}
func GetUser(ctx context.Context) (uuid.UUID, auth.Role, bool) {
userID, userOK := GetUserID(ctx)
role, roleOK := GetUserRole(ctx)
return userID, role, userOK && roleOK
}
func GetRestrictedMode(ctx context.Context) (auth.RestrictedMode, bool) {
restricted, ok := ctx.Value("restricted_mode").(auth.RestrictedMode)
return restricted, ok
}
func GetProjectRoles(ctx context.Context, r db.Repository, projectID uuid.UUID) (db.GetUserRolesForProjectRow, error) {
userID, ok := GetUserID(ctx)
if !ok {
return db.GetUserRolesForProjectRow{}, errors.New("user ID is not found")
}
return r.GetUserRolesForProject(ctx, db.GetUserRolesForProjectParams{UserID: userID, ProjectID: projectID})
}
func ConvertToRoleCode(r string) RoleCode {
if r == RoleCodeAdmin.String() {
return RoleCodeAdmin
}
if r == RoleCodeMember.String() {
return RoleCodeMember
}
return RoleCodeObserver
}
func RequireTeamAdmin(ctx context.Context, r db.Repository, teamID uuid.UUID) error {
userID, role, ok := GetUser(ctx)
if !ok {
return errors.New("internal: user id is not set")
}
teamRole, err := r.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: teamID})
isAdmin := role == auth.RoleAdmin
isTeamAdmin := err == nil && ConvertToRoleCode(teamRole.RoleCode) == RoleCodeAdmin
if !(isAdmin || isTeamAdmin) {
return &gqlerror.Error{
Message: "organization or team admin role required",
Extensions: map[string]interface{}{
"code": "2-400",
},
}
} else if err != nil {
return err
}
return nil
}
func RequireProjectOrTeamAdmin(ctx context.Context, r db.Repository, projectID uuid.UUID) error {
role, ok := GetUserRole(ctx)
if !ok {
return errors.New("user ID is not set")
}
if role == auth.RoleAdmin {
return nil
}
roles, err := GetProjectRoles(ctx, r, projectID)
if err != nil {
return err
}
if !(roles.ProjectRole == "admin" || roles.TeamRole == "admin") {
return &gqlerror.Error{
Message: "You must be a team or project admin",
Extensions: map[string]interface{}{
"code": "4-400",
},
}
}
return nil
}

View File

@ -8,15 +8,7 @@ import (
)
func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) {
ownedTeams, err := r.GetOwnedTeamsForUserID(ctx, user.UserID)
if err != sql.ErrNoRows && err != nil {
return &OwnedList{}, err
}
ownedProjects, err := r.GetOwnedProjectsForUserID(ctx, user.UserID)
if err != sql.ErrNoRows && err != nil {
return &OwnedList{}, err
}
return &OwnedList{Teams: ownedTeams, Projects: ownedProjects}, nil
return &OwnedList{}, nil
}
func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) {
projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID)

View File

@ -152,7 +152,7 @@ type DeleteUserAccountPayload struct {
}
type FindProject struct {
ProjectID string `json:"projectId"`
ProjectID uuid.UUID `json:"projectID"`
}
type FindTask struct {
@ -171,6 +171,12 @@ type LogoutUser struct {
UserID string `json:"userID"`
}
type MePayload struct {
User *db.UserAccount `json:"user"`
TeamRoles []TeamRole `json:"teamRoles"`
ProjectRoles []ProjectRole `json:"projectRoles"`
}
type Member struct {
ID uuid.UUID `json:"id"`
Role *db.Role `json:"role"`
@ -255,6 +261,11 @@ type ProfileIcon struct {
BgColor *string `json:"bgColor"`
}
type ProjectRole struct {
ProjectID uuid.UUID `json:"projectID"`
RoleCode RoleCode `json:"roleCode"`
}
type ProjectsFilter struct {
TeamID *uuid.UUID `json:"teamID"`
}
@ -263,17 +274,6 @@ type RemoveTaskLabelInput struct {
TaskLabelID uuid.UUID `json:"taskLabelID"`
}
type SetProjectOwner struct {
ProjectID uuid.UUID `json:"projectID"`
OwnerID uuid.UUID `json:"ownerID"`
}
type SetProjectOwnerPayload struct {
Ok bool `json:"ok"`
PrevOwner *Member `json:"prevOwner"`
NewOwner *Member `json:"newOwner"`
}
type SetTaskChecklistItemComplete struct {
TaskChecklistItemID uuid.UUID `json:"taskChecklistItemID"`
Complete bool `json:"complete"`
@ -284,21 +284,15 @@ type SetTaskComplete struct {
Complete bool `json:"complete"`
}
type SetTeamOwner struct {
TeamID uuid.UUID `json:"teamID"`
UserID uuid.UUID `json:"userID"`
}
type SetTeamOwnerPayload struct {
Ok bool `json:"ok"`
PrevOwner *Member `json:"prevOwner"`
NewOwner *Member `json:"newOwner"`
}
type TaskBadges struct {
Checklist *ChecklistBadge `json:"checklist"`
}
type TeamRole struct {
TeamID uuid.UUID `json:"teamID"`
RoleCode RoleCode `json:"roleCode"`
}
type ToggleTaskLabelInput struct {
TaskID uuid.UUID `json:"taskID"`
ProjectLabelID uuid.UUID `json:"projectLabelID"`
@ -410,6 +404,7 @@ type UpdateTeamMemberRole struct {
type UpdateTeamMemberRolePayload struct {
Ok bool `json:"ok"`
TeamID uuid.UUID `json:"teamID"`
Member *Member `json:"member"`
}
@ -432,6 +427,94 @@ type UpdateUserRolePayload struct {
User *db.UserAccount `json:"user"`
}
type ActionLevel string
const (
ActionLevelOrg ActionLevel = "ORG"
ActionLevelTeam ActionLevel = "TEAM"
ActionLevelProject ActionLevel = "PROJECT"
)
var AllActionLevel = []ActionLevel{
ActionLevelOrg,
ActionLevelTeam,
ActionLevelProject,
}
func (e ActionLevel) IsValid() bool {
switch e {
case ActionLevelOrg, ActionLevelTeam, ActionLevelProject:
return true
}
return false
}
func (e ActionLevel) String() string {
return string(e)
}
func (e *ActionLevel) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ActionLevel(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ActionLevel", str)
}
return nil
}
func (e ActionLevel) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type ObjectType string
const (
ObjectTypeOrg ObjectType = "ORG"
ObjectTypeTeam ObjectType = "TEAM"
ObjectTypeProject ObjectType = "PROJECT"
ObjectTypeTask ObjectType = "TASK"
)
var AllObjectType = []ObjectType{
ObjectTypeOrg,
ObjectTypeTeam,
ObjectTypeProject,
ObjectTypeTask,
}
func (e ObjectType) IsValid() bool {
switch e {
case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask:
return true
}
return false
}
func (e ObjectType) String() string {
return string(e)
}
func (e *ObjectType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ObjectType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ObjectType", str)
}
return nil
}
func (e ObjectType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type RoleCode string
const (
@ -476,3 +559,44 @@ func (e *RoleCode) UnmarshalGQL(v interface{}) error {
func (e RoleCode) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type RoleLevel string
const (
RoleLevelAdmin RoleLevel = "ADMIN"
RoleLevelMember RoleLevel = "MEMBER"
)
var AllRoleLevel = []RoleLevel{
RoleLevelAdmin,
RoleLevelMember,
}
func (e RoleLevel) IsValid() bool {
switch e {
case RoleLevelAdmin, RoleLevelMember:
return true
}
return false
}
func (e RoleLevel) String() string {
return string(e)
}
func (e *RoleLevel) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = RoleLevel(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid RoleLevel", str)
}
return nil
}
func (e RoleLevel) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

View File

@ -97,7 +97,6 @@ type Project {
createdAt: Time!
name: String!
team: Team!
owner: Member!
taskGroups: [TaskGroup!]!
members: [Member!]!
labels: [ProjectLabel!]!
@ -157,6 +156,26 @@ type TaskChecklist {
items: [TaskChecklistItem!]!
}
enum RoleLevel {
ADMIN
MEMBER
}
enum ActionLevel {
ORG
TEAM
PROJECT
}
enum ObjectType {
ORG
TEAM
PROJECT
TASK
}
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
type Query {
organizations: [Organization!]!
users: [UserAccount!]!
@ -168,11 +187,27 @@ type Query {
teams: [Team!]!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]!
me: UserAccount!
me: MePayload!
}
type Mutation
type TeamRole {
teamID: UUID!
roleCode: RoleCode!
}
type ProjectRole {
projectID: UUID!
roleCode: RoleCode!
}
type MePayload {
user: UserAccount!
teamRoles: [TeamRole!]!
projectRoles: [ProjectRole!]!
}
input ProjectsFilter {
teamID: UUID
}
@ -182,7 +217,7 @@ input FindUser {
}
input FindProject {
projectId: String!
projectID: UUID!
}
input FindTask {
@ -194,9 +229,11 @@ input FindTeam {
}
extend type Mutation {
createProject(input: NewProject!): Project!
deleteProject(input: DeleteProject!): DeleteProjectPayload!
updateProjectName(input: UpdateProjectName): Project!
createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
deleteProject(input: DeleteProject!):
DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectName(input: UpdateProjectName):
Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input NewProject {
@ -221,11 +258,16 @@ type DeleteProjectPayload {
extend type Mutation {
createProjectLabel(input: NewProjectLabel!): ProjectLabel!
deleteProjectLabel(input: DeleteProjectLabel!): ProjectLabel!
updateProjectLabel(input: UpdateProjectLabel!): ProjectLabel!
updateProjectLabelName(input: UpdateProjectLabelName!): ProjectLabel!
updateProjectLabelColor(input: UpdateProjectLabelColor!): ProjectLabel!
createProjectLabel(input: NewProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteProjectLabel(input: DeleteProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectLabel(input: UpdateProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectLabelName(input: UpdateProjectLabelName!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectLabelColor(input: UpdateProjectLabelColor!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input NewProjectLabel {
@ -255,10 +297,12 @@ input UpdateProjectLabelColor {
}
extend type Mutation {
createProjectMember(input: CreateProjectMember!): CreateProjectMemberPayload!
deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload!
updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload!
setProjectOwner(input: SetProjectOwner!): SetProjectOwnerPayload!
createProjectMember(input: CreateProjectMember!):
CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteProjectMember(input: DeleteProjectMember!):
DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectMemberRole(input: UpdateProjectMemberRole!):
UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input CreateProjectMember {
@ -293,16 +337,6 @@ type UpdateProjectMemberRolePayload {
member: Member!
}
input SetProjectOwner {
projectID: UUID!
ownerID: UUID!
}
type SetProjectOwnerPayload {
ok: Boolean!
prevOwner: Member!
newOwner: Member!
}
extend type Mutation {
createTask(input: NewTask!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
@ -506,8 +540,10 @@ extend type Mutation {
}
extend type Mutation {
deleteTeam(input: DeleteTeam!): DeleteTeamPayload!
createTeam(input: NewTeam!): Team!
deleteTeam(input: DeleteTeam!):
DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM)
createTeam(input: NewTeam!):
Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
input NewTeam {
@ -526,10 +562,11 @@ type DeleteTeamPayload {
}
extend type Mutation {
setTeamOwner(input: SetTeamOwner!): SetTeamOwnerPayload!
createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload!
updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload!
deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload!
createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
updateTeamMemberRole(input: UpdateTeamMemberRole!):
UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
}
input DeleteTeamMember {
@ -562,29 +599,23 @@ input UpdateTeamMemberRole {
type UpdateTeamMemberRolePayload {
ok: Boolean!
member: Member!
}
input SetTeamOwner {
teamID: UUID!
userID: UUID!
}
type SetTeamOwnerPayload {
ok: Boolean!
prevOwner: Member!
newOwner: Member!
member: Member!
}
extend type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount!
deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
createUserAccount(input: NewUserAccount!):
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
deleteUserAccount(input: DeleteUserAccount!):
DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount!
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
updateUserRole(input: UpdateUserRole!):
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
input UpdateUserPassword {

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/db"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
@ -23,7 +24,8 @@ func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.U
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*db.Project, error) {
createdAt := time.Now().UTC()
project, err := r.Repository.CreateProject(ctx, db.CreateProjectParams{input.UserID, input.TeamID, createdAt, input.Name})
log.WithFields(log.Fields{"userID": input.UserID, "name": input.Name, "teamID": input.TeamID}).Info("creating new project")
project, err := r.Repository.CreateProject(ctx, db.CreateProjectParams{input.TeamID, createdAt, input.Name})
return &project, err
}
@ -146,9 +148,6 @@ func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input Delete
}
func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) {
if input.RoleCode == RoleCodeOwner {
return &UpdateProjectMemberRolePayload{Ok: false}, errors.New("can not set project owner through this mutation")
}
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
if err != nil {
log.WithError(err).Error("get user account")
@ -179,64 +178,6 @@ func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input Up
return &UpdateProjectMemberRolePayload{Ok: true, Member: &member}, err
}
func (r *mutationResolver) SetProjectOwner(ctx context.Context, input SetProjectOwner) (*SetProjectOwnerPayload, error) {
project, err := r.Repository.GetProjectByID(ctx, input.ProjectID)
if project.Owner == input.OwnerID {
return &SetProjectOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
}
_, err = r.Repository.SetProjectOwner(ctx, db.SetProjectOwnerParams{Owner: input.OwnerID, ProjectID: input.ProjectID})
if err != nil {
return &SetProjectOwnerPayload{Ok: false}, err
}
err = r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{ProjectID: input.ProjectID, UserID: input.OwnerID})
if err != nil {
return &SetProjectOwnerPayload{Ok: false}, err
}
addedAt := time.Now().UTC()
_, err = r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID,
UserID: project.Owner, RoleCode: RoleCodeAdmin.String(), AddedAt: addedAt})
if err != nil {
return &SetProjectOwnerPayload{Ok: false}, err
}
oldUser, err := r.Repository.GetUserAccountByID(ctx, project.Owner)
var url *string
if oldUser.ProfileAvatarUrl.Valid {
url = &oldUser.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &oldUser.Initials, &oldUser.ProfileBgColor}
oldUserRole := db.Role{Code: "admin", Name: "Admin"}
oldMember := &Member{
ID: oldUser.UserID,
Username: oldUser.Username,
FullName: oldUser.FullName,
ProfileIcon: profileIcon,
Role: &oldUserRole,
}
newUser, err := r.Repository.GetUserAccountByID(ctx, input.OwnerID)
if newUser.ProfileAvatarUrl.Valid {
url = &newUser.ProfileAvatarUrl.String
}
profileIcon = &ProfileIcon{url, &newUser.Initials, &newUser.ProfileBgColor}
newUserRole := db.Role{Code: "owner", Name: "Owner"}
newMember := &Member{
ID: newUser.UserID,
Username: newUser.Username,
FullName: newUser.FullName,
ProfileIcon: profileIcon,
Role: &newUserRole,
}
return &SetProjectOwnerPayload{
Ok: true,
PrevOwner: oldMember,
NewOwner: newMember,
}, nil
}
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
taskGroupID, err := uuid.Parse(input.TaskGroupID)
createdAt := time.Now().UTC()
@ -574,71 +515,21 @@ func (r *mutationResolver) DeleteTeam(ctx context.Context, input DeleteTeam) (*D
}
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
userID, ok := GetUserID(ctx)
_, role, ok := GetUser(ctx)
if !ok {
return &db.Team{}, fmt.Errorf("internal server error")
return &db.Team{}, nil
}
if role == auth.RoleAdmin {
createdAt := time.Now().UTC()
team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name, Owner: userID})
team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
return &team, err
}
func (r *mutationResolver) SetTeamOwner(ctx context.Context, input SetTeamOwner) (*SetTeamOwnerPayload, error) {
team, err := r.Repository.GetTeamByID(ctx, input.TeamID)
if team.Owner == input.UserID {
return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
}
_, err = r.Repository.SetTeamOwner(ctx, db.SetTeamOwnerParams{Owner: input.UserID, TeamID: input.TeamID})
if err != nil {
return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
return &db.Team{}, &gqlerror.Error{
Message: "You must be an organization admin to create new teams",
Extensions: map[string]interface{}{
"code": "1-400",
},
}
err = r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{TeamID: input.TeamID, UserID: input.UserID})
if err != nil {
return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
}
addedAt := time.Now().UTC()
_, err = r.Repository.CreateTeamMember(ctx, db.CreateTeamMemberParams{TeamID: input.TeamID,
UserID: team.Owner, RoleCode: RoleCodeAdmin.String(), Addeddate: addedAt})
if err != nil {
return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
}
oldUser, err := r.Repository.GetUserAccountByID(ctx, team.Owner)
var url *string
if oldUser.ProfileAvatarUrl.Valid {
url = &oldUser.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &oldUser.Initials, &oldUser.ProfileBgColor}
oldUserRole := db.Role{Code: "admin", Name: "Admin"}
oldMember := &Member{
ID: oldUser.UserID,
Username: oldUser.Username,
FullName: oldUser.FullName,
ProfileIcon: profileIcon,
Role: &oldUserRole,
}
newUser, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
if newUser.ProfileAvatarUrl.Valid {
url = &newUser.ProfileAvatarUrl.String
}
profileIcon = &ProfileIcon{url, &newUser.Initials, &newUser.ProfileBgColor}
newUserRole := db.Role{Code: "owner", Name: "Owner"}
newMember := &Member{
ID: newUser.UserID,
Username: newUser.Username,
FullName: newUser.FullName,
ProfileIcon: profileIcon,
Role: &newUserRole,
}
return &SetTeamOwnerPayload{
Ok: true,
PrevOwner: oldMember,
NewOwner: newMember,
}, nil
}
func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTeamMember) (*CreateTeamMemberPayload, error) {
@ -669,9 +560,6 @@ func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTea
}
func (r *mutationResolver) UpdateTeamMemberRole(ctx context.Context, input UpdateTeamMemberRole) (*UpdateTeamMemberRolePayload, error) {
if input.RoleCode == RoleCodeOwner || input.RoleCode == RoleCodeObserver {
return &UpdateTeamMemberRolePayload{Ok: false}, errors.New("can not set project owner through this mutation")
}
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
if err != nil {
log.WithError(err).Error("get user account")
@ -699,29 +587,12 @@ func (r *mutationResolver) UpdateTeamMemberRole(ctx context.Context, input Updat
member := Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
Role: &db.Role{Code: role.Code, Name: role.Name},
}
return &UpdateTeamMemberRolePayload{Ok: true, Member: &member}, err
return &UpdateTeamMemberRolePayload{Ok: true, Member: &member, TeamID: input.TeamID}, err
}
func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTeamMember) (*DeleteTeamMemberPayload, error) {
ownedProjects, err := r.Repository.GetOwnedTeamProjectsForUserID(ctx, db.GetOwnedTeamProjectsForUserIDParams{TeamID: input.TeamID, Owner: input.UserID})
if err != nil {
return &DeleteTeamMemberPayload{}, err
}
_, err = r.Repository.GetTeamMemberByID(ctx, db.GetTeamMemberByIDParams{TeamID: input.TeamID, UserID: input.UserID})
if err != nil {
return &DeleteTeamMemberPayload{}, err
}
err = r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{TeamID: input.TeamID, UserID: input.UserID})
if err != nil {
return &DeleteTeamMemberPayload{}, err
}
if input.NewOwnerID != nil {
for _, projectID := range ownedProjects {
_, err = r.Repository.SetProjectOwner(ctx, db.SetProjectOwnerParams{ProjectID: projectID, Owner: *input.NewOwnerID})
}
}
return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, nil
err := r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{TeamID: input.TeamID, UserID: input.UserID})
return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, err
}
func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*db.RefreshToken, error) {
@ -733,6 +604,18 @@ func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefr
}
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
_, role, ok := GetUser(ctx)
if !ok {
return &db.UserAccount{}, nil
}
if role != auth.RoleAdmin {
return &db.UserAccount{}, &gqlerror.Error{
Message: "Must be an organization admin",
Extensions: map[string]interface{}{
"code": "0-400",
},
}
}
createdAt := time.Now().UTC()
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
if err != nil {
@ -751,35 +634,25 @@ func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserA
}
func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
_, role, ok := GetUser(ctx)
if !ok {
return &DeleteUserAccountPayload{Ok: false}, nil
}
if role != auth.RoleAdmin {
return &DeleteUserAccountPayload{Ok: false}, &gqlerror.Error{
Message: "User not found",
Extensions: map[string]interface{}{
"code": "0-401",
},
}
}
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
var newOwnerID uuid.UUID
if input.NewOwnerID == nil {
sysUser, err := r.Repository.GetUserAccountByUsername(ctx, "system")
if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
newOwnerID = sysUser.UserID
} else {
newOwnerID = *input.NewOwnerID
}
projectIDs, err := r.Repository.UpdateProjectOwnerByOwnerID(ctx, db.UpdateProjectOwnerByOwnerIDParams{Owner: user.UserID, Owner_2: newOwnerID})
if err != sql.ErrNoRows && err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
for _, projectID := range projectIDs {
r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{UserID: newOwnerID, ProjectID: projectID})
}
teamIDs, err := r.Repository.UpdateTeamOwnerByOwnerID(ctx, db.UpdateTeamOwnerByOwnerIDParams{Owner: user.UserID, Owner_2: newOwnerID})
if err != sql.ErrNoRows && err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
for _, teamID := range teamIDs {
r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{UserID: newOwnerID, TeamID: teamID})
}
// TODO(jordanknott) migrate admin ownership
err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
@ -822,6 +695,18 @@ func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateU
}
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
_, role, ok := GetUser(ctx)
if !ok {
return &UpdateUserRolePayload{}, nil
}
if role != auth.RoleAdmin {
return &UpdateUserRolePayload{}, &gqlerror.Error{
Message: "User not found",
Extensions: map[string]interface{}{
"code": "0-401",
},
}
}
user, err := r.Repository.UpdateUserRole(ctx, db.UpdateUserRoleParams{RoleCode: input.RoleCode.String(), UserID: input.UserID})
if err != nil {
return &UpdateUserRolePayload{}, err
@ -839,26 +724,11 @@ func (r *projectResolver) ID(ctx context.Context, obj *db.Project) (uuid.UUID, e
func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team, error) {
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
return &team, err
}
func (r *projectResolver) Owner(ctx context.Context, obj *db.Project) (*Member, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
if err != nil {
return &Member{}, err
log.WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
return &team, err
}
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: obj.ProjectID})
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
return &Member{ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon,
Role: &db.Role{Code: role.Code, Name: role.Name},
}, nil
return &team, nil
}
func (r *projectResolver) TaskGroups(ctx context.Context, obj *db.Project) ([]db.TaskGroup, error) {
@ -866,24 +736,7 @@ func (r *projectResolver) TaskGroups(ctx context.Context, obj *db.Project) ([]db
}
func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Member, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
members := []Member{}
if err == sql.ErrNoRows {
return members, nil
}
if err != nil {
log.WithError(err).Error("get user account by ID")
return members, err
}
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
members = append(members, Member{
ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon, Username: user.Username,
Role: &db.Role{Code: "owner", Name: "Owner"},
})
projectMembers, err := r.Repository.GetProjectMembersForProjectID(ctx, obj.ProjectID)
if err != nil {
log.WithError(err).Error("get project members for project id")
@ -891,7 +744,7 @@ func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Membe
}
for _, projectMember := range projectMembers {
user, err = r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
user, err := r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
if err != nil {
log.WithError(err).Error("get user account by ID")
return members, err
@ -964,11 +817,12 @@ func (r *queryResolver) FindUser(ctx context.Context, input FindUser) (*db.UserA
}
func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db.Project, error) {
projectID, err := uuid.Parse(input.ProjectID)
if err != nil {
return &db.Project{}, err
userID, role, ok := GetUser(ctx)
log.WithFields(log.Fields{"userID": userID, "role": role}).Info("find project user")
if !ok {
return &db.Project{}, nil
}
project, err := r.Repository.GetProjectByID(ctx, projectID)
project, err := r.Repository.GetProjectByID(ctx, input.ProjectID)
if err == sql.ErrNoRows {
return &db.Project{}, &gqlerror.Error{
Message: "Project not found",
@ -977,7 +831,26 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
},
}
}
if role == auth.RoleAdmin {
return &project, nil
}
projectRoles, err := GetProjectRoles(ctx, r.Repository, input.ProjectID)
log.WithFields(log.Fields{"projectID": input.ProjectID, "teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("get project roles ")
if err != nil {
return &project, err
}
if projectRoles.TeamRole == "" && projectRoles.ProjectRole == "" {
return &db.Project{}, &gqlerror.Error{
Message: "project not accessible",
Extensions: map[string]interface{}{
"code": "11-400",
},
}
}
return &project, nil
}
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) {
@ -986,10 +859,57 @@ func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task,
}
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]db.Project, error) {
userID, orgRole, ok := GetUser(ctx)
if !ok {
log.Info("user id was not found from middleware")
return []db.Project{}, nil
}
log.WithFields(log.Fields{"userID": userID}).Info("fetching projects")
if input != nil {
return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
}
if orgRole == "admin" {
log.Info("showing all projects for admin")
return r.Repository.GetAllProjects(ctx)
}
teams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
projects := make(map[string]db.Project)
for _, team := range teams {
log.WithFields(log.Fields{"teamID": team.TeamID}).Info("found team")
teamProjects, err := r.Repository.GetAllProjectsForTeam(ctx, team.TeamID)
if err != sql.ErrNoRows && err != nil {
log.Info("issue getting team projects")
return []db.Project{}, nil
}
for _, project := range teamProjects {
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding team project")
projects[project.ProjectID.String()] = project
}
}
visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
if err != nil {
log.Info("user id was not found from middleware")
return []db.Project{}, nil
}
for _, project := range visibleProjects {
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project")
if _, ok := projects[project.ProjectID.String()]; !ok {
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
projects[project.ProjectID.String()] = project
}
}
log.WithFields(log.Fields{"projectLength": len(projects)}).Info("making projects")
allProjects := make([]db.Project, 0, len(projects))
for _, project := range projects {
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("add project to final list")
allProjects = append(allProjects, project)
}
log.Info(allProjects)
return allProjects, nil
}
func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team, error) {
@ -1001,7 +921,47 @@ func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team,
}
func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
userID, orgRole, ok := GetUser(ctx)
if !ok {
log.Error("userID or orgRole does not exist!")
return []db.Team{}, errors.New("internal error")
}
if orgRole == "admin" {
return r.Repository.GetAllTeams(ctx)
}
teams := make(map[string]db.Team)
adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
if err != nil {
return []db.Team{}, err
}
for _, team := range adminTeams {
teams[team.TeamID.String()] = team
}
visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
if err != nil {
log.Info("user id was not found from middleware")
return []db.Team{}, err
}
for _, project := range visibleProjects {
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project")
if _, ok := teams[project.ProjectID.String()]; !ok {
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
team, err := r.Repository.GetTeamByID(ctx, project.TeamID)
if err != nil {
log.Info("user id was not found from middleware")
return []db.Team{}, err
}
teams[project.TeamID.String()] = team
}
}
foundTeams := make([]db.Team, 0, len(teams))
for _, team := range teams {
foundTeams = append(foundTeams, team)
}
return foundTeams, nil
}
func (r *queryResolver) LabelColors(ctx context.Context) ([]db.LabelColor, error) {
@ -1012,19 +972,37 @@ func (r *queryResolver) TaskGroups(ctx context.Context) ([]db.TaskGroup, error)
return r.Repository.GetAllTaskGroups(ctx)
}
func (r *queryResolver) Me(ctx context.Context) (*db.UserAccount, error) {
func (r *queryResolver) Me(ctx context.Context) (*MePayload, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &db.UserAccount{}, fmt.Errorf("internal server error")
return &MePayload{}, fmt.Errorf("internal server error")
}
user, err := r.Repository.GetUserAccountByID(ctx, userID)
if err == sql.ErrNoRows {
log.WithFields(log.Fields{"userID": userID}).Warning("can not find user for me query")
return &db.UserAccount{}, nil
return &MePayload{}, nil
} else if err != nil {
return &db.UserAccount{}, err
return &MePayload{}, err
}
return &user, err
var projectRoles []ProjectRole
projects, err := r.Repository.GetProjectRolesForUserID(ctx, userID)
if err != nil {
return &MePayload{}, err
}
for _, project := range projects {
projectRoles = append(projectRoles, ProjectRole{ProjectID: project.ProjectID, RoleCode: ConvertToRoleCode("admin")})
// projectRoles = append(projectRoles, ProjectRole{ProjectID: project.ProjectID, RoleCode: ConvertToRoleCode(project.RoleCode)})
}
var teamRoles []TeamRole
teams, err := r.Repository.GetTeamRolesForUserID(ctx, userID)
if err != nil {
return &MePayload{}, err
}
for _, team := range teams {
// teamRoles = append(teamRoles, TeamRole{TeamID: team.TeamID, RoleCode: ConvertToRoleCode(team.RoleCode)})
teamRoles = append(teamRoles, TeamRole{TeamID: team.TeamID, RoleCode: ConvertToRoleCode("admin")})
}
return &MePayload{User: &user, TeamRoles: teamRoles, ProjectRoles: projectRoles}, err
}
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
@ -1171,34 +1149,8 @@ func (r *teamResolver) ID(ctx context.Context, obj *db.Team) (uuid.UUID, error)
}
func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
members := []Member{}
log.WithFields(log.Fields{"teamID": obj.TeamID}).Info("getting members")
if err == sql.ErrNoRows {
return members, nil
}
if err != nil {
log.WithError(err).Error("get user account by ID")
return members, err
}
ownedList, err := GetOwnedList(ctx, r.Repository, user)
if err != nil {
return members, err
}
memberList, err := GetMemberList(ctx, r.Repository, user)
if err != nil {
return members, err
}
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
members = append(members, Member{
ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon, Username: user.Username,
Owned: ownedList, Member: memberList, Role: &db.Role{Code: "owner", Name: "Owner"},
})
teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID)
if err != nil {
log.WithError(err).Error("get project members for project id")
@ -1206,7 +1158,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
}
for _, teamMember := range teamMembers {
user, err = r.Repository.GetUserAccountByID(ctx, teamMember.UserID)
user, err := r.Repository.GetUserAccountByID(ctx, teamMember.UserID)
if err != nil {
log.WithError(err).Error("get user account by ID")
return members, err
@ -1262,15 +1214,7 @@ func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *db.UserAccou
}
func (r *userAccountResolver) Owned(ctx context.Context, obj *db.UserAccount) (*OwnedList, error) {
ownedTeams, err := r.Repository.GetOwnedTeamsForUserID(ctx, obj.UserID)
if err != sql.ErrNoRows && err != nil {
return &OwnedList{}, err
}
ownedProjects, err := r.Repository.GetOwnedProjectsForUserID(ctx, obj.UserID)
if err != sql.ErrNoRows && err != nil {
return &OwnedList{}, err
}
return &OwnedList{Teams: ownedTeams, Projects: ownedProjects}, nil
return &OwnedList{}, nil // TODO(jordanknott)
}
func (r *userAccountResolver) Member(ctx context.Context, obj *db.UserAccount) (*MemberList, error) {
@ -1330,7 +1274,9 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
// TaskChecklistItem returns TaskChecklistItemResolver implementation.
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver { return &taskChecklistItemResolver{r} }
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver {
return &taskChecklistItemResolver{r}
}
// TaskGroup returns TaskGroupResolver implementation.
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }

View File

@ -97,7 +97,6 @@ type Project {
createdAt: Time!
name: String!
team: Team!
owner: Member!
taskGroups: [TaskGroup!]!
members: [Member!]!
labels: [ProjectLabel!]!

View File

@ -1,3 +1,23 @@
enum RoleLevel {
ADMIN
MEMBER
}
enum ActionLevel {
ORG
TEAM
PROJECT
}
enum ObjectType {
ORG
TEAM
PROJECT
TASK
}
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
type Query {
organizations: [Organization!]!
users: [UserAccount!]!
@ -9,11 +29,27 @@ type Query {
teams: [Team!]!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]!
me: UserAccount!
me: MePayload!
}
type Mutation
type TeamRole {
teamID: UUID!
roleCode: RoleCode!
}
type ProjectRole {
projectID: UUID!
roleCode: RoleCode!
}
type MePayload {
user: UserAccount!
teamRoles: [TeamRole!]!
projectRoles: [ProjectRole!]!
}
input ProjectsFilter {
teamID: UUID
}
@ -23,7 +59,7 @@ input FindUser {
}
input FindProject {
projectId: String!
projectID: UUID!
}
input FindTask {

View File

@ -1,7 +1,9 @@
extend type Mutation {
createProject(input: NewProject!): Project!
deleteProject(input: DeleteProject!): DeleteProjectPayload!
updateProjectName(input: UpdateProjectName): Project!
createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
deleteProject(input: DeleteProject!):
DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectName(input: UpdateProjectName):
Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input NewProject {

View File

@ -1,9 +1,14 @@
extend type Mutation {
createProjectLabel(input: NewProjectLabel!): ProjectLabel!
deleteProjectLabel(input: DeleteProjectLabel!): ProjectLabel!
updateProjectLabel(input: UpdateProjectLabel!): ProjectLabel!
updateProjectLabelName(input: UpdateProjectLabelName!): ProjectLabel!
updateProjectLabelColor(input: UpdateProjectLabelColor!): ProjectLabel!
createProjectLabel(input: NewProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteProjectLabel(input: DeleteProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectLabel(input: UpdateProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectLabelName(input: UpdateProjectLabelName!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectLabelColor(input: UpdateProjectLabelColor!):
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input NewProjectLabel {

View File

@ -1,8 +1,10 @@
extend type Mutation {
createProjectMember(input: CreateProjectMember!): CreateProjectMemberPayload!
deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload!
updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload!
setProjectOwner(input: SetProjectOwner!): SetProjectOwnerPayload!
createProjectMember(input: CreateProjectMember!):
CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteProjectMember(input: DeleteProjectMember!):
DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectMemberRole(input: UpdateProjectMemberRole!):
UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input CreateProjectMember {
@ -36,13 +38,3 @@ type UpdateProjectMemberRolePayload {
ok: Boolean!
member: Member!
}
input SetProjectOwner {
projectID: UUID!
ownerID: UUID!
}
type SetProjectOwnerPayload {
ok: Boolean!
prevOwner: Member!
newOwner: Member!
}

View File

@ -1,15 +1,24 @@
extend type Mutation {
createTask(input: NewTask!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
createTask(input: NewTask!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteTask(input: DeleteTaskInput!):
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload!
updateTaskName(input: UpdateTaskName!): Task!
setTaskComplete(input: SetTaskComplete!): Task!
updateTaskDueDate(input: UpdateTaskDueDate!): Task!
updateTaskDescription(input: UpdateTaskDescriptionInput!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskLocation(input: NewTaskLocation!):
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskName(input: UpdateTaskName!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
setTaskComplete(input: SetTaskComplete!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskDueDate(input: UpdateTaskDueDate!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
assignTask(input: AssignTaskInput): Task!
unassignTask(input: UnassignTaskInput): Task!
assignTask(input: AssignTaskInput):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
unassignTask(input: UnassignTaskInput):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input NewTask {

View File

@ -1,14 +1,23 @@
extend type Mutation {
createTaskChecklist(input: CreateTaskChecklist!): TaskChecklist!
deleteTaskChecklist(input: DeleteTaskChecklist!): DeleteTaskChecklistPayload!
updateTaskChecklistName(input: UpdateTaskChecklistName!): TaskChecklist!
createTaskChecklistItem(input: CreateTaskChecklistItem!): TaskChecklistItem!
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem!
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem!
deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload!
createTaskChecklist(input: CreateTaskChecklist!):
TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteTaskChecklist(input: DeleteTaskChecklist!):
DeleteTaskChecklistPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskChecklistName(input: UpdateTaskChecklistName!):
TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
createTaskChecklistItem(input: CreateTaskChecklistItem!):
TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!):
TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!):
TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteTaskChecklistItem(input: DeleteTaskChecklistItem!):
DeleteTaskChecklistItemPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!):
UpdateTaskChecklistLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!):
UpdateTaskChecklistItemLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): UpdateTaskChecklistLocationPayload!
updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): UpdateTaskChecklistItemLocationPayload!
}
input UpdateTaskChecklistItemLocation {

View File

@ -1,8 +1,12 @@
extend type Mutation {
createTaskGroup(input: NewTaskGroup!): TaskGroup!
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
updateTaskGroupName(input: UpdateTaskGroupName!): TaskGroup!
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
createTaskGroup(input: NewTaskGroup!):
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskGroupLocation(input: NewTaskGroupLocation!):
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateTaskGroupName(input: UpdateTaskGroupName!):
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteTaskGroup(input: DeleteTaskGroupInput!):
DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input NewTaskGroupLocation {

View File

@ -16,7 +16,11 @@ type ToggleTaskLabelPayload {
task: Task!
}
extend type Mutation {
addTaskLabel(input: AddTaskLabelInput): Task!
removeTaskLabel(input: RemoveTaskLabelInput): Task!
toggleTaskLabel(input: ToggleTaskLabelInput!): ToggleTaskLabelPayload!
addTaskLabel(input: AddTaskLabelInput):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
removeTaskLabel(input: RemoveTaskLabelInput):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
toggleTaskLabel(input: ToggleTaskLabelInput!):
ToggleTaskLabelPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}

View File

@ -1,6 +1,8 @@
extend type Mutation {
deleteTeam(input: DeleteTeam!): DeleteTeamPayload!
createTeam(input: NewTeam!): Team!
deleteTeam(input: DeleteTeam!):
DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM)
createTeam(input: NewTeam!):
Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
input NewTeam {

View File

@ -1,8 +1,11 @@
extend type Mutation {
setTeamOwner(input: SetTeamOwner!): SetTeamOwnerPayload!
createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload!
updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload!
deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload!
createTeamMember(input: CreateTeamMember!):
CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
updateTeamMemberRole(input: UpdateTeamMemberRole!):
UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
deleteTeamMember(input: DeleteTeamMember!):
DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
}
input DeleteTeamMember {
@ -35,16 +38,6 @@ input UpdateTeamMemberRole {
type UpdateTeamMemberRolePayload {
ok: Boolean!
teamID: UUID!
member: Member!
}
input SetTeamOwner {
teamID: UUID!
userID: UUID!
}
type SetTeamOwnerPayload {
ok: Boolean!
prevOwner: Member!
newOwner: Member!
}

View File

@ -1,12 +1,16 @@
extend type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount!
deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
createUserAccount(input: NewUserAccount!):
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
deleteUserAccount(input: DeleteUserAccount!):
DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount!
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
updateUserRole(input: UpdateUserRole!):
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
input UpdateUserPassword {

View File

@ -62,7 +62,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
w.WriteHeader(http.StatusInternalServerError)
return
}
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly)
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -100,6 +100,13 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
return
}
user, err := h.repo.GetUserAccountByID(r.Context(), token.UserID)
if err != nil {
log.WithError(err).Error("user retrieve failure")
w.WriteHeader(http.StatusInternalServerError)
return
}
refreshCreatedAt := time.Now().UTC()
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{token.UserID, refreshCreatedAt, refreshExpiresAt})
@ -109,7 +116,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
w.WriteHeader(http.StatusInternalServerError)
}
accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted)
accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -175,7 +182,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted)
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -235,7 +242,7 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted)
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}

View File

@ -53,6 +53,7 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
}
ctx := context.WithValue(r.Context(), "userID", userID)
ctx = context.WithValue(ctx, "restricted_mode", accessClaims.Restricted)
ctx = context.WithValue(ctx, "org_role", accessClaims.OrgRole)
next.ServeHTTP(w, r.WithContext(ctx))
})

View File

@ -0,0 +1,4 @@
INSERT INTO project_member (user_id, project_id, added_at, role_code)
SELECT owner as user_id, project_id, NOW() as added_at, 'admin' as role_code
FROM project;
ALTER TABLE project DROP COLUMN owner;

View File

@ -0,0 +1,4 @@
INSERT INTO team_member (user_id, team_id, addeddate, role_code)
SELECT owner as user_id, team_id, NOW() as addeddate, 'admin' as role_code
FROM team ON CONFLICT ON CONSTRAINT team_member_team_id_user_id_key DO NOTHING;
ALTER TABLE team DROP COLUMN owner;