From e64f6f8569412202a4ae87faa85a5ce09c00fc1e Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Fri, 31 Jul 2020 20:01:14 -0500 Subject: [PATCH] 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. --- frontend/src/Admin/index.tsx | 11 +- frontend/src/App/Navbar.tsx | 31 - frontend/src/App/TopNavbar.tsx | 70 +- frontend/src/App/cache.ts | 5 + frontend/src/App/context.ts | 83 +- frontend/src/App/index.tsx | 23 +- frontend/src/Auth/index.tsx | 10 +- frontend/src/Install/index.tsx | 14 +- frontend/src/Profile/index.tsx | 4 +- frontend/src/Projects/Project/Board/index.tsx | 106 +- .../src/Projects/Project/Details/index.tsx | 18 +- .../Project/LabelManagerEditor/index.tsx | 23 +- frontend/src/Projects/Project/index.tsx | 20 +- frontend/src/Projects/index.tsx | 143 +- frontend/src/Teams/Members/index.tsx | 103 +- frontend/src/Teams/index.tsx | 9 +- frontend/src/index.tsx | 6 +- .../shared/components/Admin/Admin.stories.tsx | 1 + .../src/shared/components/Admin/index.tsx | 66 +- .../shared/components/DropdownMenu/index.tsx | 17 +- .../shared/components/MiniProfile/index.tsx | 29 - .../src/shared/components/TopNavbar/index.tsx | 78 +- frontend/src/shared/generated/graphql.tsx | 279 +-- frontend/src/shared/graphql/findProject.ts | 7 +- frontend/src/shared/graphql/me.graphqls | 22 +- .../shared/graphql/project/setProjectOwner.ts | 25 - .../graphql/team/updateTeamMemberRole.ts | 18 + frontend/src/shared/utils/user.ts | 5 + frontend/src/taskcafe.d.ts | 1 + go.mod | 1 - internal/auth/auth.go | 16 +- internal/commands/commands.go | 2 +- internal/commands/migrate.go | 11 +- internal/commands/migrate_prod.go | 13 + internal/commands/token.go | 27 + internal/db/models.go | 2 - internal/db/project.sql.go | 219 +- internal/db/querier.go | 13 +- internal/db/query/project.sql | 26 +- internal/db/query/team.sql | 19 +- internal/db/team.sql.go | 145 +- internal/graph/generated.go | 2083 +++++++++++------ internal/graph/graph.go | 134 +- internal/graph/helpers.go | 10 +- internal/graph/models_gen.go | 174 +- internal/graph/schema.graphqls | 121 +- internal/graph/schema.resolvers.go | 456 ++-- internal/graph/schema/_models.gql | 1 - internal/graph/schema/_root.gql | 40 +- internal/graph/schema/project.gql | 8 +- internal/graph/schema/project_label.gql | 15 +- internal/graph/schema/project_member.gql | 20 +- internal/graph/schema/task.gql | 27 +- internal/graph/schema/task_checklist.gql | 27 +- internal/graph/schema/task_group.gql | 12 +- internal/graph/schema/task_label.gql | 10 +- internal/graph/schema/team.gql | 6 +- internal/graph/schema/team_member.gql | 23 +- internal/graph/schema/user.gql | 10 +- internal/route/auth.go | 15 +- internal/route/middleware.go | 1 + ...ove-owner-column-from-project-table.up.sql | 4 + ...remove-owner-column-from-team-table.up.sql | 4 + 63 files changed, 3017 insertions(+), 1905 deletions(-) delete mode 100644 frontend/src/App/Navbar.tsx create mode 100644 frontend/src/App/cache.ts delete mode 100644 frontend/src/shared/graphql/project/setProjectOwner.ts create mode 100644 frontend/src/shared/graphql/team/updateTeamMemberRole.ts create mode 100644 frontend/src/shared/utils/user.ts create mode 100644 internal/commands/migrate_prod.go create mode 100644 internal/commands/token.go create mode 100644 migrations/0049_remove-owner-column-from-project-table.up.sql create mode 100644 migrations/0050_remove-owner-column-from-team-table.up.sql diff --git a/frontend/src/Admin/index.tsx b/frontend/src/Admin/index.tsx index 49c446f..80e3a2b 100644 --- a/frontend/src/Admin/index.tsx +++ b/frontend/src/Admin/index.tsx @@ -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(client, UsersDocument, cache => @@ -201,13 +204,17 @@ const AdminRoute = () => { if (loading) { return {}} name={null} />; } - if (data) { + if (data && user) { + if (user.roles.org != 'admin') { + return ; + } return ( <> {}} name={null} /> {}} onUpdateUserPassword={(user, password) => { console.log(user); diff --git a/frontend/src/App/Navbar.tsx b/frontend/src/App/Navbar.tsx deleted file mode 100644 index 656a0f0..0000000 --- a/frontend/src/App/Navbar.tsx +++ /dev/null @@ -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 ( - - - - - - - - - - - - - - - - ); -}; - -export default GlobalNavbar; diff --git a/frontend/src/App/TopNavbar.tsx b/frontend/src/App/TopNavbar.tsx index 502ed9c..c28284b 100644 --- a/frontend/src/App/TopNavbar.tsx +++ b/frontend/src/App/TopNavbar.tsx @@ -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 = ({ 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 = ({ onSetTab, menuType, projectID, + teamID, onChangeProjectOwner, onChangeRole, name, @@ -250,10 +254,27 @@ const GlobalTopNavbar: React.FC = ({ 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()), + projects: data.me.projectRoles.reduce((map, obj) => { + map.set(obj.projectID, obj.roleCode); + return map; + }, new Map()), + }); + } + }, + }); 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 = ({ }).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 = ({ { history.push('/admin'); hidePopup(); @@ -295,9 +318,41 @@ const GlobalTopNavbar: React.FC = ({ } }; - if (!userID) { + if (!user) { return null; } + const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID); + const onMemberProfile = ($targetRef: React.RefObject, memberID: string) => { + const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null; + const warning = + 'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.'; + if (member) { + showPopup( + $targetRef, + { + if (onChangeRole) { + onChangeRole(member.id, roleCode); + } + }} + onRemoveFromBoard={ + member.role && member.role.code === 'owner' + ? undefined + : () => { + if (onRemoveFromBoard) { + onRemoveFromBoard(member.id); + } + } + } + user={member} + bio="" + />, + ); + } + }; + return ( <> = ({ ); }} 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} diff --git a/frontend/src/App/cache.ts b/frontend/src/App/cache.ts new file mode 100644 index 0000000..48b5e73 --- /dev/null +++ b/frontend/src/App/cache.ts @@ -0,0 +1,5 @@ +import { InMemoryCache } from 'apollo-cache-inmemory'; + +const cache = new InMemoryCache(); + +export default cache; diff --git a/frontend/src/App/context.ts b/frontend/src/App/context.ts index 86d30a9..13f5f2e 100644 --- a/frontend/src/App/context.ts +++ b/frontend/src/App/context.ts @@ -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; + projects: Map; }; -export const UserIDContext = React.createContext({ 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({ + 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; diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx index d87a784..e5e0be7 100644 --- a/frontend/src/App/index.tsx +++ b/frontend/src/App/index.tsx @@ -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(null); + const [user, setUser] = useState(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(), projects: new Map() }, + }; + setUser(currentUser); setAccessToken(accessToken); if (!isInstalled) { history.replace('/install'); @@ -46,7 +57,7 @@ const App = () => { return ( <> - + @@ -62,7 +73,7 @@ const App = () => { - + ); }; diff --git a/frontend/src/Auth/index.tsx b/frontend/src/Auth/index.tsx index 971ea9c..a7dc2a3 100644 --- a/frontend/src/Auth/index.tsx +++ b/frontend/src/Auth/index.tsx @@ -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(), projects: new Map() }, + }; + setUser(currentUser); setComplete(true); setAccessToken(accessToken); diff --git a/frontend/src/Install/index.tsx b/frontend/src/Install/index.tsx index 6273bba..484fabb 100644 --- a/frontend/src/Install/index.tsx +++ b/frontend/src/Install/index.tsx @@ -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(), + projects: new Map(), + }, + }; + setUser(currentUser); setAccessToken(accessToken); if (!isInstalled) { history.replace('/install'); diff --git a/frontend/src/Profile/index.tsx b/frontend/src/Profile/index.tsx index b005a1e..8434dbb 100644 --- a/frontend/src/Profile/index.tsx +++ b/frontend/src/Profile/index.tsx @@ -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 = () => { {}} name={null} /> {!loading && data && ( { if ($fileUpload && $fileUpload.current) { $fileUpload.current.click(); diff --git a/frontend/src/Projects/Project/Board/index.tsx b/frontend/src/Projects/Project/Board/index.tsx index 3565b92..7fd4c7d 100644 --- a/frontend/src/Projects/Project/Board/index.tsx +++ b/frontend/src/Projects/Project/Board/index.tsx @@ -8,7 +8,6 @@ import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons'; import { usePopup, Popup } from 'shared/components/PopupMenu'; import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom'; import { - useSetProjectOwnerMutation, useUpdateProjectMemberRoleMutation, useCreateProjectMemberMutation, useDeleteProjectMemberMutation, @@ -44,7 +43,7 @@ import SimpleLists from 'shared/components/Lists'; import produce from 'immer'; import MiniProfile from 'shared/components/MiniProfile'; import DueDateManager from 'shared/components/DueDateManager'; -import UserIDContext from 'App/context'; +import UserContext, { useCurrentUser } from 'App/context'; import LabelManager from 'shared/components/PopupMenu/LabelManager'; import LabelEditor from 'shared/components/PopupMenu/LabelEditor'; import EmptyBoard from 'shared/components/EmptyBoard'; @@ -106,16 +105,48 @@ const initialQuickCardEditorState: QuickCardEditorState = { type ProjectBoardProps = { onCardLabelClick?: () => void; cardLabelVariant?: CardLabelVariant; - projectID?: string; - loading?: boolean; + projectID: string; }; -const ProjectBoard: React.FC = ({ - projectID, - onCardLabelClick, - cardLabelVariant, - loading: isLoading = false, -}) => { +export const BoardLoading = () => { + return ( + <> + + + + + All Tasks + + + + Filter + + + + Sort + + + + + + Labels + + + + Fields + + + + Rules + + + + + + ); +}; + +const ProjectBoard: React.FC = ({ projectID, onCardLabelClick, cardLabelVariant }) => { const [assignTask] = useAssignTaskMutation(); const [unassignTask] = useUnassignTaskMutation(); const $labelsRef = useRef(null); @@ -124,7 +155,7 @@ const ProjectBoard: React.FC = ({ const { showPopup, hidePopup } = usePopup(); const taskLabelsRef = useRef>([]); const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState); - const { userID } = useContext(UserIDContext); + const { user } = useCurrentUser(); const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({}); const history = useHistory(); const [deleteTaskGroup] = useDeleteTaskGroupMutation({ @@ -138,7 +169,7 @@ const ProjectBoard: React.FC = ({ (taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id, ); }), - { projectId: projectID }, + { projectID }, ); }, }); @@ -156,7 +187,7 @@ const ProjectBoard: React.FC = ({ draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask }); } }), - { projectId: projectID }, + { projectID }, ); }, }); @@ -170,14 +201,14 @@ const ProjectBoard: React.FC = ({ produce(cache, draftCache => { draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] }); }), - { projectId: projectID }, + { projectID }, ); }, }); const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({}); const { loading, data } = useFindProjectQuery({ - variables: { projectId: projectID ?? '' }, + variables: { projectID }, }); const [updateTaskDueDate] = useUpdateTaskDueDateMutation(); @@ -205,7 +236,7 @@ const ProjectBoard: React.FC = ({ } } }), - { projectId: projectID }, + { projectID }, ); }, }); @@ -238,7 +269,7 @@ const ProjectBoard: React.FC = ({ __typename: 'Mutation', createTask: { __typename: 'Task', - id: '' + Math.round(Math.random() * -1000000), + id: `${Math.round(Math.random() * -1000000)}`, name, complete: false, taskGroup: { @@ -248,6 +279,7 @@ const ProjectBoard: React.FC = ({ position: taskGroup.position, }, badges: { + __typename: 'TaskBadges', checklist: null, }, position, @@ -273,42 +305,8 @@ const ProjectBoard: React.FC = ({ } }; - if (loading || isLoading) { - return ( - <> - - - - - All Tasks - - - - Filter - - - - Sort - - - - - - Labels - - - - Fields - - - - Rules - - - - - - ); + if (loading) { + return ; } if (data) { labelsRef.current = data.findProject.labels; @@ -534,7 +532,7 @@ const ProjectBoard: React.FC = ({ tasks: taskGroup.tasks.filter(t => t.id !== cardId), })); }), - { projectId: projectID }, + { projectID }, ); }, }) diff --git a/frontend/src/Projects/Project/Details/index.tsx b/frontend/src/Projects/Project/Details/index.tsx index d12d9ee..3cfcefb 100644 --- a/frontend/src/Projects/Project/Details/index.tsx +++ b/frontend/src/Projects/Project/Details/index.tsx @@ -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 = ({ 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 = ({ user={member} bio="None" onRemoveFromTask={() => { - unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); + if (user) { + unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } }); + } }} /> , @@ -422,10 +424,12 @@ const Details: React.FC = ({ availableMembers={availableMembers} activeMembers={data.findTask.assigned} onMemberChange={(member, isActive) => { - if (isActive) { - assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); - } else { - unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); + if (user) { + if (isActive) { + assignTask({ variables: { taskID: data.findTask.id, userID: user.id } }); + } else { + unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } }); + } } }} /> diff --git a/frontend/src/Projects/Project/LabelManagerEditor/index.tsx b/frontend/src/Projects/Project/LabelManagerEditor/index.tsx index b9dca1e..8b50e4c 100644 --- a/frontend/src/Projects/Project/LabelManagerEditor/index.tsx +++ b/frontend/src/Projects/Project/LabelManagerEditor/index.tsx @@ -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 = ({ taskLabels: taskLabelsRef, }) => { const [currentLabel, setCurrentLabel] = useState(''); - const {setTab, hidePopup} = usePopup(); + const { setTab, hidePopup } = usePopup(); const [createProjectLabel] = useCreateProjectLabelMutation({ update: (client, newLabelData) => { updateApolloCache( @@ -58,10 +57,10 @@ const LabelManagerEditor: React.FC = ({ 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 = ({ label => label.id !== newLabelData.data.deleteProjectLabel.id, ); }), - {projectId: projectID}, + { projectID }, ); }, }); @@ -108,7 +107,7 @@ const LabelManagerEditor: React.FC = ({ if (newProjectLabel) { setCurrentTaskLabels([ ...currentTaskLabels, - {id: '', assignedDate: '', projectLabel: {...newProjectLabel}}, + { id: '', assignedDate: '', projectLabel: { ...newProjectLabel } }, ]); } } @@ -127,12 +126,12 @@ const LabelManagerEditor: React.FC = ({ 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 = ({ 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 = ({ ); }; -export default LabelManagerEditor +export default LabelManagerEditor; diff --git a/frontend/src/Projects/Project/index.tsx b/frontend/src/Projects/Project/index.tsx index ccb0246..453adbe 100644 --- a/frontend/src/Projects/Project/index.tsx +++ b/frontend/src/Projects/Project/index.tsx @@ -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( @@ -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 ( <> {}} name="" projectID={null} /> - + ); } @@ -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} /> } /> diff --git a/frontend/src/Projects/index.tsx b/frontend/src/Projects/index.tsx index d16cac0..7b17265 100644 --- a/frontend/src/Projects/index.tsx +++ b/frontend/src/Projects/index.tsx @@ -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({ open: false, initialTeamID: null }); - const { userID, setUserID } = useContext(UserIDContext); + const { user, setUser } = useCurrentUser(); const [createTeam] = useCreateTeamMutation({ update: (client, createData) => { updateApolloCache(client, GetProjectsDocument, cache => @@ -261,47 +260,63 @@ 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 => { - return { - id: team.id, - name: team.name, - projects: projects.filter(project => project.team.id === team.id), - }; - }); + 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) + .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 ( <> {}} projectID={null} name={null} /> - { - showPopup( - $target, - { - hidePopup(); - }} - > - { - if (organizationID) { - createTeam({ variables: { name: teamName, organizationID } }); - hidePopup(); - } + {user.roles.org === 'admin' && ( + { + showPopup( + $target, + { + hidePopup(); }} - /> - , - ); - }} - > - Add Team - + > + { + if (organizationID) { + createTeam({ variables: { name: teamName, organizationID } }); + hidePopup(); + } + }} + /> + , + ); + }} + > + Add Team + + )} {projectTeams.length === 0 && ( @@ -340,17 +355,19 @@ const Projects = () => {
{team.name} - - - Projects - - - Members - - - Settings - - + {user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && ( + + + Projects + + + Members + + + Settings + + + )} {team.projects.map((project, idx) => ( @@ -363,18 +380,20 @@ const Projects = () => { ))} - - { - setShowNewProject({ open: true, initialTeamID: team.id }); - }} - > - - - Create new project - - - + {user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && ( + + { + setShowNewProject({ open: true, initialTeamID: team.id }); + }} + > + + + Create new project + + + + )}
); @@ -383,8 +402,8 @@ const Projects = () => { { - if (userID) { - createProject({ variables: { teamID, name, userID } }); + if (user) { + createProject({ variables: { teamID, name, userID: user.id } }); setShowNewProject({ open: false, initialTeamID: null }); } }} diff --git a/frontend/src/Teams/Members/index.tsx b/frontend/src/Teams/Members/index.tsx index e95cc6a..cfeb27b 100644 --- a/frontend/src/Teams/Members/index.tsx +++ b/frontend/src/Teams/Members/index.tsx @@ -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 = ({ @@ -175,7 +177,6 @@ const TeamRoleManagerPopup: React.FC = ({ currentUserID, canChangeRole, onRemoveFromTeam, - onChangeTeamOwner, onChangeRole, }) => { const { hidePopup, setTab } = usePopup(); @@ -185,15 +186,6 @@ const TeamRoleManagerPopup: React.FC = ({ - {onChangeTeamOwner && ( - { - setTab(3); - }} - > - Set as team owner... - - )} {subject.role && ( { @@ -298,24 +290,6 @@ const TeamRoleManagerPopup: React.FC = ({ - hidePopup()} tab={3}> - - - 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. - - { - if (onChangeTeamOwner) { - onChangeTeamOwner(subject.id); - } - }} - > - Set as Project Owner - - - ); }; @@ -444,7 +418,7 @@ type MembersProps = { const Members: React.FC = ({ teamID }) => { const { showPopup, hidePopup } = usePopup(); const { loading, data } = useGetTeamQuery({ variables: { teamID } }); - const { userID } = useContext(UserIDContext); + const { user, setUserRoles } = useCurrentUser(); const warning = 'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.'; const [createTeamMember] = useCreateTeamMemberMutation({ @@ -454,12 +428,27 @@ const Members: React.FC = ({ 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( @@ -479,7 +468,7 @@ const Members: React.FC = ({ teamID }) => { return loading; } - if (data) { + if (data && user) { return ( @@ -497,23 +486,26 @@ const Members: React.FC = ({ teamID }) => { - { - showPopup( - $target, - { - createTeamMember({ variables: { userID, teamID } }); - }} - />, - ); - }} - > - - Invite Team Members - + {user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && ( + { + showPopup( + $target, + { + console.log(`team: ${userID}`); + createTeamMember({ variables: { userID, teamID } }); + }} + />, + ); + }} + > + + Invite Team Members + + )} @@ -532,15 +524,14 @@ const Members: React.FC = ({ teamID }) => { showPopup( $target, {} : 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 diff --git a/frontend/src/Teams/index.tsx b/frontend/src/Teams/index.tsx index 739ac1c..2e32e96 100644 --- a/frontend/src/Teams/index.tsx +++ b/frontend/src/Teams/index.tsx @@ -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(); 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 ; + } return ( <> { ` 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 = ({ {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 => ( = ({ Choose a new user to take over ownership of this user's teams & projects. 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 = ({ Removing this user from the organzation will remove them from assigned tasks, projects, and teams. {`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`} - {}} - value={null} - options={users.map((u) => ({ label: u.fullName, value: u.id }))} - /> + {}} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} /> { // 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) => { {}}> - params.onDeleteUser($target, params.value)}> + params.onDeleteUser($target, params.value)}> @@ -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) => void; users: Array; + canInviteUser: boolean; onUpdateUserPassword: (user: TaskUser, password: string) => void; }; @@ -631,6 +628,7 @@ const Admin: React.FC = ({ initialTab, onAddUser, onUpdateUserPassword, + canInviteUser, onDeleteUser, onInviteUser, users, @@ -675,18 +673,20 @@ const Admin: React.FC = ({ - { - onAddUser($target); - }} - > - - New Member - + {canInviteUser && ( + { + onAddUser($target); + }} + > + + New Member + + )} - {users.map((member) => { + {users.map(member => { const projectTotal = member.owned.projects.length + member.member.projects.length; return ( @@ -699,7 +699,7 @@ const Admin: React.FC = ({ {`On ${projectTotal} projects`} { + onClick={$target => { showPopup( $target, = ({ onUpdateUserPassword(user, password); }} canChangeRole={(member.role && member.role.code !== 'owner') ?? false} - onChangeRole={(roleCode) => { + onChangeRole={roleCode => { updateUserRole({ variables: { userID: member.id, roleCode } }); }} onDeleteUser={onDeleteUser} diff --git a/frontend/src/shared/components/DropdownMenu/index.tsx b/frontend/src/shared/components/DropdownMenu/index.tsx index 53d10ed..6a60fa4 100644 --- a/frontend/src/shared/components/DropdownMenu/index.tsx +++ b/frontend/src/shared/components/DropdownMenu/index.tsx @@ -37,17 +37,22 @@ const DropdownMenu: React.FC = ({ left, top, onLogout, onClos type ProfileMenuProps = { onProfile: () => void; onLogout: () => void; + showAdminConsole: boolean; onAdminConsole: () => void; }; -const ProfileMenu: React.FC = ({ onAdminConsole, onProfile, onLogout }) => { +const ProfileMenu: React.FC = ({ showAdminConsole, onAdminConsole, onProfile, onLogout }) => { return ( <> - - - Admin Console - - + {showAdminConsole && ( + <> + + + Admin Console + + + + )} Profile diff --git a/frontend/src/shared/components/MiniProfile/index.tsx b/frontend/src/shared/components/MiniProfile/index.tsx index 2e92254..a892d45 100644 --- a/frontend/src/shared/components/MiniProfile/index.tsx +++ b/frontend/src/shared/components/MiniProfile/index.tsx @@ -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 = ({ user, bio, canChangeRole, - onChangeProjectOwner, onRemoveFromTask, onChangeRole, onRemoveFromBoard, @@ -91,15 +89,6 @@ const MiniProfile: React.FC = ({ Remove from card )} - {onChangeProjectOwner && ( - { - setTab(3); - }} - > - Set as project owner - - )} {onChangeRole && user.role && ( { @@ -193,24 +182,6 @@ const MiniProfile: React.FC = ({ - hidePopup()} tab={3}> - - - 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. - - { - if (onChangeProjectOwner) { - onChangeProjectOwner(user.id); - } - }} - > - Set as Project Owner - - - ); }; diff --git a/frontend/src/shared/components/TopNavbar/index.tsx b/frontend/src/shared/components/TopNavbar/index.tsx index ed29b09..fce46ff 100644 --- a/frontend/src/shared/components/TopNavbar/index.tsx +++ b/frontend/src/shared/components/TopNavbar/index.tsx @@ -38,6 +38,7 @@ const HomeDashboard = styled(Home)``; type ProjectHeadingProps = { onFavorite?: () => void; name: string; + canEditProjectName: boolean; onSaveProjectName?: (projectName: string) => void; onOpenSettings: ($target: React.RefObject) => void; }; @@ -46,6 +47,7 @@ const ProjectHeading: React.FC = ({ onFavorite, name: initialProjectName, onSaveProjectName, + canEditProjectName, onOpenSettings, }) => { const [isEditProjectName, setEditProjectName] = useState(false); @@ -94,7 +96,9 @@ const ProjectHeading: React.FC = ({ ) : ( { - setEditProjectName(true); + if (canEditProjectName) { + setEditProjectName(true); + } }} > {projectName} @@ -142,19 +146,25 @@ type NavBarProps = { onProfileClick: ($target: React.RefObject) => void; onSaveName?: (name: string) => void; onNotificationClick: () => void; + canEditProjectName?: boolean; + canInviteUser?: boolean; onInviteUser?: ($target: React.RefObject) => void; onDashboardClick: () => void; user: TaskUser | null; onOpenSettings: ($target: React.RefObject) => void; projectMembers?: Array | null; onRemoveFromBoard?: (userID: string) => void; + onMemberProfile?: ($targetRef: React.RefObject, memberID: string) => void; }; const NavBar: React.FC = ({ menuType, + canInviteUser = false, onInviteUser, onChangeProjectOwner, currentTab, + onMemberProfile, + canEditProjectName = false, onOpenProjectFinder, onFavorite, onSetTab, @@ -175,47 +185,6 @@ const NavBar: React.FC = ({ } }; const { showPopup } = usePopup(); - const onMemberProfile = ($targetRef: React.RefObject, memberID: string) => { - const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null; - const warning = - 'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.'; - if (member) { - console.log(member); - showPopup( - $targetRef, - { - 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 ( @@ -226,6 +195,7 @@ const NavBar: React.FC = ({ onFavorite={onFavorite} onOpenSettings={onOpenSettings} name={name} + canEditProjectName={canEditProjectName} onSaveProjectName={onSaveName} /> )} @@ -255,7 +225,7 @@ const NavBar: React.FC = ({ Taskcafé - {projectMembers && ( + {projectMembers && onMemberProfile && ( <> {projectMembers.map((member, idx) => ( @@ -268,16 +238,18 @@ const NavBar: React.FC = ({ onMemberProfile={onMemberProfile} /> ))} - { - if (onInviteUser) { - onInviteUser($target); - } - }} - variant="outline" - > - Invite - + {canInviteUser && ( + { + if (onInviteUser) { + onInviteUser($target); + } + }} + variant="outline" + > + Invite + + )} diff --git a/frontend/src/shared/generated/graphql.tsx b/frontend/src/shared/generated/graphql.tsx index 26689b0..a58a556 100644 --- a/frontend/src/shared/generated/graphql.tsx +++ b/frontend/src/shared/generated/graphql.tsx @@ -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; members: Array; labels: Array; @@ -192,6 +192,24 @@ export type TaskChecklist = { items: Array; }; +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; @@ -204,7 +222,7 @@ export type Query = { teams: Array; labelColors: Array; taskGroups: Array; - 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; + projectRoles: Array; +}; + export type ProjectsFilter = { teamID?: Maybe; }; @@ -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 - & { members: Array<( + & { team: ( + { __typename?: 'Team' } + & Pick + ), members: Array<( { __typename?: 'Member' } & Pick & { role: ( @@ -1242,12 +1247,21 @@ export type MeQueryVariables = {}; export type MeQuery = ( { __typename?: 'Query' } & { me: ( - { __typename?: 'UserAccount' } - & Pick - & { profileIcon: ( - { __typename?: 'ProfileIcon' } - & Pick - ) } + { __typename?: 'MePayload' } + & { user: ( + { __typename?: 'UserAccount' } + & Pick + & { profileIcon: ( + { __typename?: 'ProfileIcon' } + & Pick + ) } + ), teamRoles: Array<( + { __typename?: 'TeamRole' } + & Pick + )>, projectRoles: Array<( + { __typename?: 'ProjectRole' } + & Pick + )> } ) } ); @@ -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 - & { newOwner: ( - { __typename?: 'Member' } - & Pick - & { role: ( - { __typename?: 'Role' } - & Pick - ) } - ), prevOwner: ( - { __typename?: 'Member' } - & Pick - & { role: ( - { __typename?: 'Role' } - & Pick - ) } - ) } - ) } -); - 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 + & { member: ( + { __typename?: 'Member' } + & Pick + & { role: ( + { __typename?: 'Role' } + & Pick + ) } + ) } + ) } +); + export type ToggleTaskLabelMutationVariables = { taskID: Scalars['UUID']; projectLabelID: Scalars['UUID']; @@ -2325,9 +2333,12 @@ export type DeleteTaskGroupMutationHookResult = ReturnType; export type DeleteTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions; 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,12 +2574,22 @@ export type GetProjectsQueryResult = ApolloReactCommon.QueryResult; export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult; export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions; -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; - -/** - * __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) { - return ApolloReactHooks.useMutation(SetProjectOwnerDocument, baseOptions); - } -export type SetProjectOwnerMutationHookResult = ReturnType; -export type SetProjectOwnerMutationResult = ApolloReactCommon.MutationResult; -export type SetProjectOwnerMutationOptions = ApolloReactCommon.BaseMutationOptions; 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; export type GetTeamLazyQueryHookResult = ReturnType; export type GetTeamQueryResult = ApolloReactCommon.QueryResult; +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; + +/** + * __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) { + return ApolloReactHooks.useMutation(UpdateTeamMemberRoleDocument, baseOptions); + } +export type UpdateTeamMemberRoleMutationHookResult = ReturnType; +export type UpdateTeamMemberRoleMutationResult = ApolloReactCommon.MutationResult; +export type UpdateTeamMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions; export const ToggleTaskLabelDocument = gql` mutation toggleTaskLabel($taskID: UUID!, $projectLabelID: UUID!) { toggleTaskLabel(input: {taskID: $taskID, projectLabelID: $projectLabelID}) { diff --git a/frontend/src/shared/graphql/findProject.ts b/frontend/src/shared/graphql/findProject.ts index ffa0e36..4230ecd 100644 --- a/frontend/src/shared/graphql/findProject.ts +++ b/frontend/src/shared/graphql/findProject.ts @@ -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 diff --git a/frontend/src/shared/graphql/me.graphqls b/frontend/src/shared/graphql/me.graphqls index 1da4d4f..8dddb33 100644 --- a/frontend/src/shared/graphql/me.graphqls +++ b/frontend/src/shared/graphql/me.graphqls @@ -1,11 +1,21 @@ query me { me { - id - fullName - profileIcon { - initials - bgColor - url + user { + id + fullName + profileIcon { + initials + bgColor + url + } + } + teamRoles { + teamID + roleCode + } + projectRoles { + projectID + roleCode } } } diff --git a/frontend/src/shared/graphql/project/setProjectOwner.ts b/frontend/src/shared/graphql/project/setProjectOwner.ts deleted file mode 100644 index 5310de6..0000000 --- a/frontend/src/shared/graphql/project/setProjectOwner.ts +++ /dev/null @@ -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; diff --git a/frontend/src/shared/graphql/team/updateTeamMemberRole.ts b/frontend/src/shared/graphql/team/updateTeamMemberRole.ts new file mode 100644 index 0000000..53b406f --- /dev/null +++ b/frontend/src/shared/graphql/team/updateTeamMemberRole.ts @@ -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; diff --git a/frontend/src/shared/utils/user.ts b/frontend/src/shared/utils/user.ts new file mode 100644 index 0000000..a0a9d69 --- /dev/null +++ b/frontend/src/shared/utils/user.ts @@ -0,0 +1,5 @@ +import { PermissionObjectType, PermissionLevel } from 'App/context'; + +export default function userCan(level: PermissionLevel, objectType: PermissionObjectType) { + return false; +} diff --git a/frontend/src/taskcafe.d.ts b/frontend/src/taskcafe.d.ts index 8df1928..d89c2ab 100644 --- a/frontend/src/taskcafe.d.ts +++ b/frontend/src/taskcafe.d.ts @@ -1,5 +1,6 @@ interface JWTToken { userId: string; + orgRole: string; iat: string; exp: string; } diff --git a/go.mod b/go.mod index 9f47757..8232e00 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/auth/auth.go b/internal/auth/auth.go index ddd321a..9100e36 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -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()}, } diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 163bc7b..b8f6a82 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -35,6 +35,6 @@ var rootCmd = &cobra.Command{ func Execute() { rootCmd.SetVersionTemplate(versionTemplate) - rootCmd.AddCommand(newWebCmd(), newMigrateCmd()) + rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd()) rootCmd.Execute() } diff --git a/internal/commands/migrate.go b/internal/commands/migrate.go index 8f4466d..910255a 100644 --- a/internal/commands/migrate.go +++ b/internal/commands/migrate.go @@ -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 } diff --git a/internal/commands/migrate_prod.go b/internal/commands/migrate_prod.go new file mode 100644 index 0000000..4eace59 --- /dev/null +++ b/internal/commands/migrate_prod.go @@ -0,0 +1,13 @@ +// +build prod + +package commands + +import ( + "fmt" + + "github.com/jordanknott/taskcafe/internal/migrations" +) + +func init() { + migration = migrations.Migrations +} diff --git a/internal/commands/token.go b/internal/commands/token.go new file mode 100644 index 0000000..e0dbd0c --- /dev/null +++ b/internal/commands/token.go @@ -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) + }, + } +} diff --git a/internal/db/models.go b/internal/db/models.go index 62ccbcf..76bd415 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -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 { diff --git a/internal/db/project.sql.go b/internal/db/project.sql.go index 80c7e4e..378b755 100644 --- a/internal/db/project.sql.go +++ b/internal/db/project.sql.go @@ -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,8 +258,40 @@ 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 +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 -} diff --git a/internal/db/querier.go b/internal/db/querier.go index 1094e0d..8b66f6a 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -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) } diff --git a/internal/db/query/project.sql b/internal/db/query/project.sql index e3cc9d8..9a56728 100644 --- a/internal/db/query/project.sql +++ b/internal/db/query/project.sql @@ -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 *; @@ -23,7 +20,7 @@ DELETE FROM project WHERE project_id = $1; SELECT * FROM project_member WHERE project_id = $1; -- name: GetRoleForProjectMemberByUserID :one -SELECT code, role.name FROM project_member INNER JOIN role ON role.code = project_member.role_code +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; -- name: CreateProjectMember :one @@ -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; diff --git a/internal/db/query/team.sql b/internal/db/query/team.sql index f8f16f0..4937bdf 100644 --- a/internal/db/query/team.sql +++ b/internal/db/query/team.sql @@ -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; diff --git a/internal/db/team.sql.go b/internal/db/team.sql.go index fe85b13..f4bd8e5 100644 --- a/internal/db/team.sql.go +++ b/internal/db/team.sql.go @@ -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) +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, + ) + 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 []Team + var items []GetTeamRolesForUserIDRow for rows.Next() { - var i Team - if err := rows.Scan( - &i.TeamID, - &i.CreatedAt, - &i.Name, - &i.OrganizationID, - &i.Owner, - ); err != nil { + 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 diff --git a/internal/graph/generated.go b/internal/graph/generated.go index ad3bbb6..2112203 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "errors" + "fmt" "strconv" "sync" "sync/atomic" @@ -54,6 +55,7 @@ type ResolverRoot interface { } type DirectiveRoot struct { + HasRole func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (res interface{}, err error) } type ComplexityRoot struct { @@ -127,6 +129,12 @@ type ComplexityRoot struct { Position func(childComplexity int) int } + MePayload struct { + ProjectRoles func(childComplexity int) int + TeamRoles func(childComplexity int) int + User func(childComplexity int) int + } + Member struct { FullName func(childComplexity int) int ID func(childComplexity int) int @@ -169,10 +177,8 @@ type ComplexityRoot struct { DeleteUserAccount func(childComplexity int, input DeleteUserAccount) int LogoutUser func(childComplexity int, input LogoutUser) int RemoveTaskLabel func(childComplexity int, input *RemoveTaskLabelInput) int - SetProjectOwner func(childComplexity int, input SetProjectOwner) int SetTaskChecklistItemComplete func(childComplexity int, input SetTaskChecklistItemComplete) int SetTaskComplete func(childComplexity int, input SetTaskComplete) int - SetTeamOwner func(childComplexity int, input SetTeamOwner) int ToggleTaskLabel func(childComplexity int, input ToggleTaskLabelInput) int UnassignTask func(childComplexity int, input *UnassignTaskInput) int UpdateProjectLabel func(childComplexity int, input UpdateProjectLabel) int @@ -222,7 +228,6 @@ type ComplexityRoot struct { Labels func(childComplexity int) int Members func(childComplexity int) int Name func(childComplexity int) int - Owner func(childComplexity int) int TaskGroups func(childComplexity int) int Team func(childComplexity int) int } @@ -234,6 +239,11 @@ type ComplexityRoot struct { Name func(childComplexity int) int } + ProjectRole struct { + ProjectID func(childComplexity int) int + RoleCode func(childComplexity int) int + } + Query struct { FindProject func(childComplexity int, input FindProject) int FindTask func(childComplexity int, input FindTask) int @@ -260,18 +270,6 @@ type ComplexityRoot struct { Name func(childComplexity int) int } - SetProjectOwnerPayload struct { - NewOwner func(childComplexity int) int - Ok func(childComplexity int) int - PrevOwner func(childComplexity int) int - } - - SetTeamOwnerPayload struct { - NewOwner func(childComplexity int) int - Ok func(childComplexity int) int - PrevOwner func(childComplexity int) int - } - Task struct { Assigned func(childComplexity int) int Badges func(childComplexity int) int @@ -329,6 +327,11 @@ type ComplexityRoot struct { Name func(childComplexity int) int } + TeamRole struct { + RoleCode func(childComplexity int) int + TeamID func(childComplexity int) int + } + ToggleTaskLabelPayload struct { Active func(childComplexity int) int Task func(childComplexity int) int @@ -357,6 +360,7 @@ type ComplexityRoot struct { UpdateTeamMemberRolePayload struct { Member func(childComplexity int) int Ok func(childComplexity int) int + TeamID func(childComplexity int) int } UpdateUserPasswordPayload struct { @@ -397,7 +401,6 @@ type MutationResolver interface { CreateProjectMember(ctx context.Context, input CreateProjectMember) (*CreateProjectMemberPayload, error) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) - SetProjectOwner(ctx context.Context, input SetProjectOwner) (*SetProjectOwnerPayload, error) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) UpdateTaskDescription(ctx context.Context, input UpdateTaskDescriptionInput) (*db.Task, error) @@ -425,7 +428,6 @@ type MutationResolver interface { ToggleTaskLabel(ctx context.Context, input ToggleTaskLabelInput) (*ToggleTaskLabelPayload, error) DeleteTeam(ctx context.Context, input DeleteTeam) (*DeleteTeamPayload, error) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) - SetTeamOwner(ctx context.Context, input SetTeamOwner) (*SetTeamOwnerPayload, error) CreateTeamMember(ctx context.Context, input CreateTeamMember) (*CreateTeamMemberPayload, error) UpdateTeamMemberRole(ctx context.Context, input UpdateTeamMemberRole) (*UpdateTeamMemberRolePayload, error) DeleteTeamMember(ctx context.Context, input DeleteTeamMember) (*DeleteTeamMemberPayload, error) @@ -444,7 +446,6 @@ type ProjectResolver interface { ID(ctx context.Context, obj *db.Project) (uuid.UUID, error) Team(ctx context.Context, obj *db.Project) (*db.Team, error) - Owner(ctx context.Context, obj *db.Project) (*Member, error) TaskGroups(ctx context.Context, obj *db.Project) ([]db.TaskGroup, error) Members(ctx context.Context, obj *db.Project) ([]Member, error) Labels(ctx context.Context, obj *db.Project) ([]db.ProjectLabel, error) @@ -466,7 +467,7 @@ type QueryResolver interface { Teams(ctx context.Context) ([]db.Team, error) LabelColors(ctx context.Context) ([]db.LabelColor, error) TaskGroups(ctx context.Context) ([]db.TaskGroup, error) - Me(ctx context.Context) (*db.UserAccount, error) + Me(ctx context.Context) (*MePayload, error) } type RefreshTokenResolver interface { ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) @@ -750,6 +751,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.LabelColor.Position(childComplexity), true + case "MePayload.projectRoles": + if e.complexity.MePayload.ProjectRoles == nil { + break + } + + return e.complexity.MePayload.ProjectRoles(childComplexity), true + + case "MePayload.teamRoles": + if e.complexity.MePayload.TeamRoles == nil { + break + } + + return e.complexity.MePayload.TeamRoles(childComplexity), true + + case "MePayload.user": + if e.complexity.MePayload.User == nil { + break + } + + return e.complexity.MePayload.User(childComplexity), true + case "Member.fullName": if e.complexity.Member.FullName == nil { break @@ -1120,18 +1142,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.RemoveTaskLabel(childComplexity, args["input"].(*RemoveTaskLabelInput)), true - case "Mutation.setProjectOwner": - if e.complexity.Mutation.SetProjectOwner == nil { - break - } - - args, err := ec.field_Mutation_setProjectOwner_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.SetProjectOwner(childComplexity, args["input"].(SetProjectOwner)), true - case "Mutation.setTaskChecklistItemComplete": if e.complexity.Mutation.SetTaskChecklistItemComplete == nil { break @@ -1156,18 +1166,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.SetTaskComplete(childComplexity, args["input"].(SetTaskComplete)), true - case "Mutation.setTeamOwner": - if e.complexity.Mutation.SetTeamOwner == nil { - break - } - - args, err := ec.field_Mutation_setTeamOwner_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.SetTeamOwner(childComplexity, args["input"].(SetTeamOwner)), true - case "Mutation.toggleTaskLabel": if e.complexity.Mutation.ToggleTaskLabel == nil { break @@ -1506,13 +1504,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Project.Name(childComplexity), true - case "Project.owner": - if e.complexity.Project.Owner == nil { - break - } - - return e.complexity.Project.Owner(childComplexity), true - case "Project.taskGroups": if e.complexity.Project.TaskGroups == nil { break @@ -1555,6 +1546,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ProjectLabel.Name(childComplexity), true + case "ProjectRole.projectID": + if e.complexity.ProjectRole.ProjectID == nil { + break + } + + return e.complexity.ProjectRole.ProjectID(childComplexity), true + + case "ProjectRole.roleCode": + if e.complexity.ProjectRole.RoleCode == nil { + break + } + + return e.complexity.ProjectRole.RoleCode(childComplexity), true + case "Query.findProject": if e.complexity.Query.FindProject == nil { break @@ -1699,48 +1704,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Role.Name(childComplexity), true - case "SetProjectOwnerPayload.newOwner": - if e.complexity.SetProjectOwnerPayload.NewOwner == nil { - break - } - - return e.complexity.SetProjectOwnerPayload.NewOwner(childComplexity), true - - case "SetProjectOwnerPayload.ok": - if e.complexity.SetProjectOwnerPayload.Ok == nil { - break - } - - return e.complexity.SetProjectOwnerPayload.Ok(childComplexity), true - - case "SetProjectOwnerPayload.prevOwner": - if e.complexity.SetProjectOwnerPayload.PrevOwner == nil { - break - } - - return e.complexity.SetProjectOwnerPayload.PrevOwner(childComplexity), true - - case "SetTeamOwnerPayload.newOwner": - if e.complexity.SetTeamOwnerPayload.NewOwner == nil { - break - } - - return e.complexity.SetTeamOwnerPayload.NewOwner(childComplexity), true - - case "SetTeamOwnerPayload.ok": - if e.complexity.SetTeamOwnerPayload.Ok == nil { - break - } - - return e.complexity.SetTeamOwnerPayload.Ok(childComplexity), true - - case "SetTeamOwnerPayload.prevOwner": - if e.complexity.SetTeamOwnerPayload.PrevOwner == nil { - break - } - - return e.complexity.SetTeamOwnerPayload.PrevOwner(childComplexity), true - case "Task.assigned": if e.complexity.Task.Assigned == nil { break @@ -1993,6 +1956,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Team.Name(childComplexity), true + case "TeamRole.roleCode": + if e.complexity.TeamRole.RoleCode == nil { + break + } + + return e.complexity.TeamRole.RoleCode(childComplexity), true + + case "TeamRole.teamID": + if e.complexity.TeamRole.TeamID == nil { + break + } + + return e.complexity.TeamRole.TeamID(childComplexity), true + case "ToggleTaskLabelPayload.active": if e.complexity.ToggleTaskLabelPayload.Active == nil { break @@ -2077,6 +2054,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.UpdateTeamMemberRolePayload.Ok(childComplexity), true + case "UpdateTeamMemberRolePayload.teamID": + if e.complexity.UpdateTeamMemberRolePayload.TeamID == nil { + break + } + + return e.complexity.UpdateTeamMemberRolePayload.TeamID(childComplexity), true + case "UpdateUserPasswordPayload.ok": if e.complexity.UpdateUserPasswordPayload.Ok == nil { break @@ -2331,7 +2315,6 @@ type Project { createdAt: Time! name: String! team: Team! - owner: Member! taskGroups: [TaskGroup!]! members: [Member!]! labels: [ProjectLabel!]! @@ -2391,6 +2374,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!]! @@ -2402,11 +2405,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 } @@ -2416,7 +2435,7 @@ input FindUser { } input FindProject { - projectId: String! + projectID: UUID! } input FindTask { @@ -2428,9 +2447,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 { @@ -2455,11 +2476,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 { @@ -2489,10 +2515,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 { @@ -2527,16 +2555,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! @@ -2740,8 +2758,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 { @@ -2760,10 +2780,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 { @@ -2796,29 +2817,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 { @@ -2875,6 +2890,36 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) dir_hasRole_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 []RoleLevel + if tmp, ok := rawArgs["roles"]; ok { + arg0, err = ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, tmp) + if err != nil { + return nil, err + } + } + args["roles"] = arg0 + var arg1 ActionLevel + if tmp, ok := rawArgs["level"]; ok { + arg1, err = ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, tmp) + if err != nil { + return nil, err + } + } + args["level"] = arg1 + var arg2 ObjectType + if tmp, ok := rawArgs["type"]; ok { + arg2, err = ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, tmp) + if err != nil { + return nil, err + } + } + args["type"] = arg2 + return args, nil +} + func (ec *executionContext) field_Mutation_addTaskLabel_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3225,20 +3270,6 @@ func (ec *executionContext) field_Mutation_removeTaskLabel_args(ctx context.Cont return args, nil } -func (ec *executionContext) field_Mutation_setProjectOwner_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 SetProjectOwner - if tmp, ok := rawArgs["input"]; ok { - arg0, err = ec.unmarshalNSetProjectOwner2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetProjectOwner(ctx, tmp) - if err != nil { - return nil, err - } - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_setTaskChecklistItemComplete_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3267,20 +3298,6 @@ func (ec *executionContext) field_Mutation_setTaskComplete_args(ctx context.Cont return args, nil } -func (ec *executionContext) field_Mutation_setTeamOwner_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 SetTeamOwner - if tmp, ok := rawArgs["input"]; ok { - arg0, err = ec.unmarshalNSetTeamOwner2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetTeamOwner(ctx, tmp) - if err != nil { - return nil, err - } - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_toggleTaskLabel_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4735,6 +4752,108 @@ func (ec *executionContext) _LabelColor_colorHex(ctx context.Context, field grap return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _MePayload_user(ctx context.Context, field graphql.CollectedField, obj *MePayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MePayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.User, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*db.UserAccount) + fc.Result = res + return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res) +} + +func (ec *executionContext) _MePayload_teamRoles(ctx context.Context, field graphql.CollectedField, obj *MePayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MePayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TeamRoles, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]TeamRole) + fc.Result = res + return ec.marshalNTeamRole2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTeamRoleᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) _MePayload_projectRoles(ctx context.Context, field graphql.CollectedField, obj *MePayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MePayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ProjectRoles, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]ProjectRole) + fc.Result = res + return ec.marshalNProjectRole2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectRoleᚄ(ctx, field.Selections, res) +} + func (ec *executionContext) _Member_id(ctx context.Context, field graphql.CollectedField, obj *Member) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5064,8 +5183,40 @@ func (ec *executionContext) _Mutation_createProject(ctx context.Context, field g } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateProject(rctx, args["input"].(NewProject)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateProject(rctx, args["input"].(NewProject)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "TEAM") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TEAM") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.Project); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Project`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5105,8 +5256,40 @@ func (ec *executionContext) _Mutation_deleteProject(ctx context.Context, field g } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteProject(rctx, args["input"].(DeleteProject)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteProject(rctx, args["input"].(DeleteProject)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*DeleteProjectPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteProjectPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5146,8 +5329,40 @@ func (ec *executionContext) _Mutation_updateProjectName(ctx context.Context, fie } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateProjectName(rctx, args["input"].(*UpdateProjectName)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateProjectName(rctx, args["input"].(*UpdateProjectName)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.Project); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Project`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5187,8 +5402,40 @@ func (ec *executionContext) _Mutation_createProjectLabel(ctx context.Context, fi } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateProjectLabel(rctx, args["input"].(NewProjectLabel)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateProjectLabel(rctx, args["input"].(NewProjectLabel)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.ProjectLabel); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.ProjectLabel`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5228,8 +5475,40 @@ func (ec *executionContext) _Mutation_deleteProjectLabel(ctx context.Context, fi } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteProjectLabel(rctx, args["input"].(DeleteProjectLabel)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteProjectLabel(rctx, args["input"].(DeleteProjectLabel)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.ProjectLabel); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.ProjectLabel`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5269,8 +5548,40 @@ func (ec *executionContext) _Mutation_updateProjectLabel(ctx context.Context, fi } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateProjectLabel(rctx, args["input"].(UpdateProjectLabel)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateProjectLabel(rctx, args["input"].(UpdateProjectLabel)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.ProjectLabel); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.ProjectLabel`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5310,8 +5621,40 @@ func (ec *executionContext) _Mutation_updateProjectLabelName(ctx context.Context } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateProjectLabelName(rctx, args["input"].(UpdateProjectLabelName)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateProjectLabelName(rctx, args["input"].(UpdateProjectLabelName)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.ProjectLabel); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.ProjectLabel`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5351,8 +5694,40 @@ func (ec *executionContext) _Mutation_updateProjectLabelColor(ctx context.Contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateProjectLabelColor(rctx, args["input"].(UpdateProjectLabelColor)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateProjectLabelColor(rctx, args["input"].(UpdateProjectLabelColor)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.ProjectLabel); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.ProjectLabel`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5392,8 +5767,40 @@ func (ec *executionContext) _Mutation_createProjectMember(ctx context.Context, f } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateProjectMember(rctx, args["input"].(CreateProjectMember)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateProjectMember(rctx, args["input"].(CreateProjectMember)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*CreateProjectMemberPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.CreateProjectMemberPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5433,8 +5840,40 @@ func (ec *executionContext) _Mutation_deleteProjectMember(ctx context.Context, f } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteProjectMember(rctx, args["input"].(DeleteProjectMember)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteProjectMember(rctx, args["input"].(DeleteProjectMember)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*DeleteProjectMemberPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteProjectMemberPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5474,8 +5913,40 @@ func (ec *executionContext) _Mutation_updateProjectMemberRole(ctx context.Contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateProjectMemberRole(rctx, args["input"].(UpdateProjectMemberRole)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateProjectMemberRole(rctx, args["input"].(UpdateProjectMemberRole)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*UpdateProjectMemberRolePayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateProjectMemberRolePayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5492,47 +5963,6 @@ func (ec *executionContext) _Mutation_updateProjectMemberRole(ctx context.Contex return ec.marshalNUpdateProjectMemberRolePayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateProjectMemberRolePayload(ctx, field.Selections, res) } -func (ec *executionContext) _Mutation_setProjectOwner(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Mutation", - Field: field, - Args: nil, - IsMethod: true, - } - - ctx = graphql.WithFieldContext(ctx, fc) - rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Mutation_setProjectOwner_args(ctx, rawArgs) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - fc.Args = args - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SetProjectOwner(rctx, args["input"].(SetProjectOwner)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*SetProjectOwnerPayload) - fc.Result = res - return ec.marshalNSetProjectOwnerPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetProjectOwnerPayload(ctx, field.Selections, res) -} - func (ec *executionContext) _Mutation_createTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6581,8 +7011,40 @@ func (ec *executionContext) _Mutation_deleteTeam(ctx context.Context, field grap } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteTeam(rctx, args["input"].(DeleteTeam)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteTeam(rctx, args["input"].(DeleteTeam)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "TEAM") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TEAM") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*DeleteTeamPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteTeamPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6622,8 +7084,40 @@ func (ec *executionContext) _Mutation_createTeam(ctx context.Context, field grap } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateTeam(rctx, args["input"].(NewTeam)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateTeam(rctx, args["input"].(NewTeam)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "ORG") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "ORG") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.Team); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Team`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6640,47 +7134,6 @@ func (ec *executionContext) _Mutation_createTeam(ctx context.Context, field grap return ec.marshalNTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx, field.Selections, res) } -func (ec *executionContext) _Mutation_setTeamOwner(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Mutation", - Field: field, - Args: nil, - IsMethod: true, - } - - ctx = graphql.WithFieldContext(ctx, fc) - rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Mutation_setTeamOwner_args(ctx, rawArgs) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - fc.Args = args - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SetTeamOwner(rctx, args["input"].(SetTeamOwner)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*SetTeamOwnerPayload) - fc.Result = res - return ec.marshalNSetTeamOwnerPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetTeamOwnerPayload(ctx, field.Selections, res) -} - func (ec *executionContext) _Mutation_createTeamMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6704,8 +7157,40 @@ func (ec *executionContext) _Mutation_createTeamMember(ctx context.Context, fiel } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateTeamMember(rctx, args["input"].(CreateTeamMember)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateTeamMember(rctx, args["input"].(CreateTeamMember)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "TEAM") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TEAM") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*CreateTeamMemberPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.CreateTeamMemberPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6745,8 +7230,40 @@ func (ec *executionContext) _Mutation_updateTeamMemberRole(ctx context.Context, } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTeamMemberRole(rctx, args["input"].(UpdateTeamMemberRole)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTeamMemberRole(rctx, args["input"].(UpdateTeamMemberRole)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "TEAM") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TEAM") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*UpdateTeamMemberRolePayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateTeamMemberRolePayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6786,8 +7303,40 @@ func (ec *executionContext) _Mutation_deleteTeamMember(ctx context.Context, fiel } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteTeamMember(rctx, args["input"].(DeleteTeamMember)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteTeamMember(rctx, args["input"].(DeleteTeamMember)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "TEAM") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TEAM") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*DeleteTeamMemberPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteTeamMemberPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6868,8 +7417,40 @@ func (ec *executionContext) _Mutation_createUserAccount(ctx context.Context, fie } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateUserAccount(rctx, args["input"].(NewUserAccount)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateUserAccount(rctx, args["input"].(NewUserAccount)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "ORG") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "ORG") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*db.UserAccount); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.UserAccount`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6909,8 +7490,40 @@ func (ec *executionContext) _Mutation_deleteUserAccount(ctx context.Context, fie } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteUserAccount(rctx, args["input"].(DeleteUserAccount)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteUserAccount(rctx, args["input"].(DeleteUserAccount)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "ORG") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "ORG") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*DeleteUserAccountPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteUserAccountPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -7066,8 +7679,40 @@ func (ec *executionContext) _Mutation_updateUserRole(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateUserRole(rctx, args["input"].(UpdateUserRole)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateUserRole(rctx, args["input"].(UpdateUserRole)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "ORG") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "ORG") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*UpdateUserRolePayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateUserRolePayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -7517,40 +8162,6 @@ func (ec *executionContext) _Project_team(ctx context.Context, field graphql.Col return ec.marshalNTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx, field.Selections, res) } -func (ec *executionContext) _Project_owner(ctx context.Context, field graphql.CollectedField, obj *db.Project) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Project", - Field: field, - Args: nil, - IsMethod: true, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Project().Owner(rctx, obj) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*Member) - fc.Result = res - return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) -} - func (ec *executionContext) _Project_taskGroups(ctx context.Context, field graphql.CollectedField, obj *db.Project) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7786,6 +8397,74 @@ func (ec *executionContext) _ProjectLabel_name(ctx context.Context, field graphq return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _ProjectRole_projectID(ctx context.Context, field graphql.CollectedField, obj *ProjectRole) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProjectRole", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ProjectID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) _ProjectRole_roleCode(ctx context.Context, field graphql.CollectedField, obj *ProjectRole) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProjectRole", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RoleCode, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(RoleCode) + fc.Result = res + return ec.marshalNRoleCode2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleCode(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_organizations(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -8190,9 +8869,9 @@ func (ec *executionContext) _Query_me(ctx context.Context, field graphql.Collect } return graphql.Null } - res := resTmp.(*db.UserAccount) + res := resTmp.(*MePayload) fc.Result = res - return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res) + return ec.marshalNMePayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMePayload(ctx, field.Selections, res) } func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -8468,210 +9147,6 @@ func (ec *executionContext) _Role_name(ctx context.Context, field graphql.Collec return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _SetProjectOwnerPayload_ok(ctx context.Context, field graphql.CollectedField, obj *SetProjectOwnerPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "SetProjectOwnerPayload", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Ok, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) -} - -func (ec *executionContext) _SetProjectOwnerPayload_prevOwner(ctx context.Context, field graphql.CollectedField, obj *SetProjectOwnerPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "SetProjectOwnerPayload", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.PrevOwner, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*Member) - fc.Result = res - return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) -} - -func (ec *executionContext) _SetProjectOwnerPayload_newOwner(ctx context.Context, field graphql.CollectedField, obj *SetProjectOwnerPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "SetProjectOwnerPayload", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.NewOwner, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*Member) - fc.Result = res - return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) -} - -func (ec *executionContext) _SetTeamOwnerPayload_ok(ctx context.Context, field graphql.CollectedField, obj *SetTeamOwnerPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "SetTeamOwnerPayload", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Ok, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) -} - -func (ec *executionContext) _SetTeamOwnerPayload_prevOwner(ctx context.Context, field graphql.CollectedField, obj *SetTeamOwnerPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "SetTeamOwnerPayload", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.PrevOwner, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*Member) - fc.Result = res - return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) -} - -func (ec *executionContext) _SetTeamOwnerPayload_newOwner(ctx context.Context, field graphql.CollectedField, obj *SetTeamOwnerPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "SetTeamOwnerPayload", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.NewOwner, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*Member) - fc.Result = res - return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) -} - func (ec *executionContext) _Task_id(ctx context.Context, field graphql.CollectedField, obj *db.Task) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -9887,6 +10362,74 @@ func (ec *executionContext) _Team_members(ctx context.Context, field graphql.Col return ec.marshalNMember2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _TeamRole_teamID(ctx context.Context, field graphql.CollectedField, obj *TeamRole) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TeamRole", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TeamID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) _TeamRole_roleCode(ctx context.Context, field graphql.CollectedField, obj *TeamRole) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TeamRole", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RoleCode, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(RoleCode) + fc.Result = res + return ec.marshalNRoleCode2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleCode(ctx, field.Selections, res) +} + func (ec *executionContext) _ToggleTaskLabelPayload_active(ctx context.Context, field graphql.CollectedField, obj *ToggleTaskLabelPayload) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -10261,6 +10804,40 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload_ok(ctx context.Context, return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _UpdateTeamMemberRolePayload_teamID(ctx context.Context, field graphql.CollectedField, obj *UpdateTeamMemberRolePayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "UpdateTeamMemberRolePayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TeamID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + func (ec *executionContext) _UpdateTeamMemberRolePayload_member(ctx context.Context, field graphql.CollectedField, obj *UpdateTeamMemberRolePayload) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -12158,9 +12735,9 @@ func (ec *executionContext) unmarshalInputFindProject(ctx context.Context, obj i for k, v := range asMap { switch k { - case "projectId": + case "projectID": var err error - it.ProjectID, err = ec.unmarshalNString2string(ctx, v) + it.ProjectID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) if err != nil { return it, err } @@ -12542,30 +13119,6 @@ func (ec *executionContext) unmarshalInputRemoveTaskLabelInput(ctx context.Conte return it, nil } -func (ec *executionContext) unmarshalInputSetProjectOwner(ctx context.Context, obj interface{}) (SetProjectOwner, error) { - var it SetProjectOwner - var asMap = obj.(map[string]interface{}) - - for k, v := range asMap { - switch k { - case "projectID": - var err error - it.ProjectID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) - if err != nil { - return it, err - } - case "ownerID": - var err error - it.OwnerID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) - if err != nil { - return it, err - } - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputSetTaskChecklistItemComplete(ctx context.Context, obj interface{}) (SetTaskChecklistItemComplete, error) { var it SetTaskChecklistItemComplete var asMap = obj.(map[string]interface{}) @@ -12614,30 +13167,6 @@ func (ec *executionContext) unmarshalInputSetTaskComplete(ctx context.Context, o return it, nil } -func (ec *executionContext) unmarshalInputSetTeamOwner(ctx context.Context, obj interface{}) (SetTeamOwner, error) { - var it SetTeamOwner - var asMap = obj.(map[string]interface{}) - - for k, v := range asMap { - switch k { - case "teamID": - var err error - it.TeamID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) - if err != nil { - return it, err - } - case "userID": - var err error - it.UserID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) - if err != nil { - return it, err - } - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputToggleTaskLabelInput(ctx context.Context, obj interface{}) (ToggleTaskLabelInput, error) { var it ToggleTaskLabelInput var asMap = obj.(map[string]interface{}) @@ -13552,6 +14081,43 @@ func (ec *executionContext) _LabelColor(ctx context.Context, sel ast.SelectionSe return out } +var mePayloadImplementors = []string{"MePayload"} + +func (ec *executionContext) _MePayload(ctx context.Context, sel ast.SelectionSet, obj *MePayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mePayloadImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MePayload") + case "user": + out.Values[i] = ec._MePayload_user(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "teamRoles": + out.Values[i] = ec._MePayload_teamRoles(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "projectRoles": + out.Values[i] = ec._MePayload_projectRoles(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var memberImplementors = []string{"Member"} func (ec *executionContext) _Member(ctx context.Context, sel ast.SelectionSet, obj *Member) graphql.Marshaler { @@ -13711,11 +14277,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } - case "setProjectOwner": - out.Values[i] = ec._Mutation_setProjectOwner(ctx, field) - if out.Values[i] == graphql.Null { - invalids++ - } case "createTask": out.Values[i] = ec._Mutation_createTask(ctx, field) if out.Values[i] == graphql.Null { @@ -13851,11 +14412,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } - case "setTeamOwner": - out.Values[i] = ec._Mutation_setTeamOwner(ctx, field) - if out.Values[i] == graphql.Null { - invalids++ - } case "createTeamMember": out.Values[i] = ec._Mutation_createTeamMember(ctx, field) if out.Values[i] == graphql.Null { @@ -14099,20 +14655,6 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } return res }) - case "owner": - field := field - out.Concurrently(i, func() (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Project_owner(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } - return res - }) case "taskGroups": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -14232,6 +14774,38 @@ func (ec *executionContext) _ProjectLabel(ctx context.Context, sel ast.Selection return out } +var projectRoleImplementors = []string{"ProjectRole"} + +func (ec *executionContext) _ProjectRole(ctx context.Context, sel ast.SelectionSet, obj *ProjectRole) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, projectRoleImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ProjectRole") + case "projectID": + out.Values[i] = ec._ProjectRole_projectID(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "roleCode": + out.Values[i] = ec._ProjectRole_roleCode(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -14499,80 +15073,6 @@ func (ec *executionContext) _Role(ctx context.Context, sel ast.SelectionSet, obj return out } -var setProjectOwnerPayloadImplementors = []string{"SetProjectOwnerPayload"} - -func (ec *executionContext) _SetProjectOwnerPayload(ctx context.Context, sel ast.SelectionSet, obj *SetProjectOwnerPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, setProjectOwnerPayloadImplementors) - - out := graphql.NewFieldSet(fields) - var invalids uint32 - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("SetProjectOwnerPayload") - case "ok": - out.Values[i] = ec._SetProjectOwnerPayload_ok(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "prevOwner": - out.Values[i] = ec._SetProjectOwnerPayload_prevOwner(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "newOwner": - out.Values[i] = ec._SetProjectOwnerPayload_newOwner(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch() - if invalids > 0 { - return graphql.Null - } - return out -} - -var setTeamOwnerPayloadImplementors = []string{"SetTeamOwnerPayload"} - -func (ec *executionContext) _SetTeamOwnerPayload(ctx context.Context, sel ast.SelectionSet, obj *SetTeamOwnerPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, setTeamOwnerPayloadImplementors) - - out := graphql.NewFieldSet(fields) - var invalids uint32 - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("SetTeamOwnerPayload") - case "ok": - out.Values[i] = ec._SetTeamOwnerPayload_ok(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "prevOwner": - out.Values[i] = ec._SetTeamOwnerPayload_prevOwner(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "newOwner": - out.Values[i] = ec._SetTeamOwnerPayload_newOwner(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch() - if invalids > 0 { - return graphql.Null - } - return out -} - var taskImplementors = []string{"Task"} func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj *db.Task) graphql.Marshaler { @@ -15069,6 +15569,38 @@ func (ec *executionContext) _Team(ctx context.Context, sel ast.SelectionSet, obj return out } +var teamRoleImplementors = []string{"TeamRole"} + +func (ec *executionContext) _TeamRole(ctx context.Context, sel ast.SelectionSet, obj *TeamRole) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, teamRoleImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("TeamRole") + case "teamID": + out.Values[i] = ec._TeamRole_teamID(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "roleCode": + out.Values[i] = ec._TeamRole_roleCode(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var toggleTaskLabelPayloadImplementors = []string{"ToggleTaskLabelPayload"} func (ec *executionContext) _ToggleTaskLabelPayload(ctx context.Context, sel ast.SelectionSet, obj *ToggleTaskLabelPayload) graphql.Marshaler { @@ -15245,6 +15777,11 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload(ctx context.Context, se if out.Values[i] == graphql.Null { invalids++ } + case "teamID": + out.Values[i] = ec._UpdateTeamMemberRolePayload_teamID(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "member": out.Values[i] = ec._UpdateTeamMemberRolePayload_member(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -15682,6 +16219,15 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx context.Context, v interface{}) (ActionLevel, error) { + var res ActionLevel + return res, res.UnmarshalGQL(v) +} + +func (ec *executionContext) marshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx context.Context, sel ast.SelectionSet, v ActionLevel) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) { return graphql.UnmarshalBoolean(v) } @@ -16019,6 +16565,20 @@ func (ec *executionContext) unmarshalNLogoutUser2githubᚗcomᚋjordanknottᚋta return ec.unmarshalInputLogoutUser(ctx, v) } +func (ec *executionContext) marshalNMePayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMePayload(ctx context.Context, sel ast.SelectionSet, v MePayload) graphql.Marshaler { + return ec._MePayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNMePayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMePayload(ctx context.Context, sel ast.SelectionSet, v *MePayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._MePayload(ctx, sel, v) +} + func (ec *executionContext) marshalNMember2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx context.Context, sel ast.SelectionSet, v Member) graphql.Marshaler { return ec._Member(ctx, sel, &v) } @@ -16120,6 +16680,15 @@ func (ec *executionContext) unmarshalNNewUserAccount2githubᚗcomᚋjordanknott return ec.unmarshalInputNewUserAccount(ctx, v) } +func (ec *executionContext) unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx context.Context, v interface{}) (ObjectType, error) { + var res ObjectType + return res, res.UnmarshalGQL(v) +} + +func (ec *executionContext) marshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx context.Context, sel ast.SelectionSet, v ObjectType) graphql.Marshaler { + return v +} + func (ec *executionContext) marshalNOrganization2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐOrganization(ctx context.Context, sel ast.SelectionSet, v db.Organization) graphql.Marshaler { return ec._Organization(ctx, sel, &v) } @@ -16291,6 +16860,47 @@ func (ec *executionContext) marshalNProjectLabel2ᚖgithubᚗcomᚋjordanknott return ec._ProjectLabel(ctx, sel, v) } +func (ec *executionContext) marshalNProjectRole2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectRole(ctx context.Context, sel ast.SelectionSet, v ProjectRole) graphql.Marshaler { + return ec._ProjectRole(ctx, sel, &v) +} + +func (ec *executionContext) marshalNProjectRole2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectRoleᚄ(ctx context.Context, sel ast.SelectionSet, v []ProjectRole) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNProjectRole2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectRole(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + func (ec *executionContext) marshalNRefreshToken2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐRefreshToken(ctx context.Context, sel ast.SelectionSet, v db.RefreshToken) graphql.Marshaler { return ec._RefreshToken(ctx, sel, &v) } @@ -16328,22 +16938,70 @@ func (ec *executionContext) marshalNRoleCode2githubᚗcomᚋjordanknottᚋtaskca return v } -func (ec *executionContext) unmarshalNSetProjectOwner2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetProjectOwner(ctx context.Context, v interface{}) (SetProjectOwner, error) { - return ec.unmarshalInputSetProjectOwner(ctx, v) +func (ec *executionContext) unmarshalNRoleLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevel(ctx context.Context, v interface{}) (RoleLevel, error) { + var res RoleLevel + return res, res.UnmarshalGQL(v) } -func (ec *executionContext) marshalNSetProjectOwnerPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetProjectOwnerPayload(ctx context.Context, sel ast.SelectionSet, v SetProjectOwnerPayload) graphql.Marshaler { - return ec._SetProjectOwnerPayload(ctx, sel, &v) +func (ec *executionContext) marshalNRoleLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevel(ctx context.Context, sel ast.SelectionSet, v RoleLevel) graphql.Marshaler { + return v } -func (ec *executionContext) marshalNSetProjectOwnerPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetProjectOwnerPayload(ctx context.Context, sel ast.SelectionSet, v *SetProjectOwnerPayload) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "must not be null") +func (ec *executionContext) unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx context.Context, v interface{}) ([]RoleLevel, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} } - return graphql.Null } - return ec._SetProjectOwnerPayload(ctx, sel, v) + var err error + res := make([]RoleLevel, len(vSlice)) + for i := range vSlice { + res[i], err = ec.unmarshalNRoleLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevel(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx context.Context, sel ast.SelectionSet, v []RoleLevel) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNRoleLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevel(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret } func (ec *executionContext) unmarshalNSetTaskChecklistItemComplete2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetTaskChecklistItemComplete(ctx context.Context, v interface{}) (SetTaskChecklistItemComplete, error) { @@ -16354,24 +17012,6 @@ func (ec *executionContext) unmarshalNSetTaskComplete2githubᚗcomᚋjordanknott return ec.unmarshalInputSetTaskComplete(ctx, v) } -func (ec *executionContext) unmarshalNSetTeamOwner2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetTeamOwner(ctx context.Context, v interface{}) (SetTeamOwner, error) { - return ec.unmarshalInputSetTeamOwner(ctx, v) -} - -func (ec *executionContext) marshalNSetTeamOwnerPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetTeamOwnerPayload(ctx context.Context, sel ast.SelectionSet, v SetTeamOwnerPayload) graphql.Marshaler { - return ec._SetTeamOwnerPayload(ctx, sel, &v) -} - -func (ec *executionContext) marshalNSetTeamOwnerPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSetTeamOwnerPayload(ctx context.Context, sel ast.SelectionSet, v *SetTeamOwnerPayload) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - return ec._SetTeamOwnerPayload(ctx, sel, v) -} - func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) { return graphql.UnmarshalString(v) } @@ -16696,6 +17336,47 @@ func (ec *executionContext) marshalNTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcaf return ec._Team(ctx, sel, v) } +func (ec *executionContext) marshalNTeamRole2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTeamRole(ctx context.Context, sel ast.SelectionSet, v TeamRole) graphql.Marshaler { + return ec._TeamRole(ctx, sel, &v) +} + +func (ec *executionContext) marshalNTeamRole2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTeamRoleᚄ(ctx context.Context, sel ast.SelectionSet, v []TeamRole) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNTeamRole2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTeamRole(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + func (ec *executionContext) unmarshalNTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) { return graphql.UnmarshalTime(v) } diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 136832b..a5e8407 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -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 +} diff --git a/internal/graph/helpers.go b/internal/graph/helpers.go index 1b1327d..aa1992b 100644 --- a/internal/graph/helpers.go +++ b/internal/graph/helpers.go @@ -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) diff --git a/internal/graph/models_gen.go b/internal/graph/models_gen.go index 792b49d..974e237 100644 --- a/internal/graph/models_gen.go +++ b/internal/graph/models_gen.go @@ -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"` @@ -409,8 +403,9 @@ type UpdateTeamMemberRole struct { } type UpdateTeamMemberRolePayload struct { - Ok bool `json:"ok"` - Member *Member `json:"member"` + Ok bool `json:"ok"` + TeamID uuid.UUID `json:"teamID"` + Member *Member `json:"member"` } type UpdateUserPassword struct { @@ -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())) +} diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index 9962621..1edd598 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -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 { diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index d188d7e..756ed49 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -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 } - createdAt := time.Now().UTC() - team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name, Owner: userID}) - 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") + if role == auth.RoleAdmin { + createdAt := time.Now().UTC() + team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name}) + return &team, err } - _, 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 }, } } - return &project, err + 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) } - return r.Repository.GetAllProjects(ctx) + + 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) { - return r.Repository.GetAllTeams(ctx) + 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} } diff --git a/internal/graph/schema/_models.gql b/internal/graph/schema/_models.gql index da069d8..df59914 100644 --- a/internal/graph/schema/_models.gql +++ b/internal/graph/schema/_models.gql @@ -97,7 +97,6 @@ type Project { createdAt: Time! name: String! team: Team! - owner: Member! taskGroups: [TaskGroup!]! members: [Member!]! labels: [ProjectLabel!]! diff --git a/internal/graph/schema/_root.gql b/internal/graph/schema/_root.gql index 736252e..796cb93 100644 --- a/internal/graph/schema/_root.gql +++ b/internal/graph/schema/_root.gql @@ -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 { diff --git a/internal/graph/schema/project.gql b/internal/graph/schema/project.gql index 3272bba..cc22595 100644 --- a/internal/graph/schema/project.gql +++ b/internal/graph/schema/project.gql @@ -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 { diff --git a/internal/graph/schema/project_label.gql b/internal/graph/schema/project_label.gql index 2e7cef5..863f56e 100644 --- a/internal/graph/schema/project_label.gql +++ b/internal/graph/schema/project_label.gql @@ -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 { diff --git a/internal/graph/schema/project_member.gql b/internal/graph/schema/project_member.gql index c5a415e..5c22b36 100644 --- a/internal/graph/schema/project_member.gql +++ b/internal/graph/schema/project_member.gql @@ -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! -} diff --git a/internal/graph/schema/task.gql b/internal/graph/schema/task.gql index a437735..3e7e99b 100644 --- a/internal/graph/schema/task.gql +++ b/internal/graph/schema/task.gql @@ -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 { diff --git a/internal/graph/schema/task_checklist.gql b/internal/graph/schema/task_checklist.gql index a9ff1b4..70af81e 100644 --- a/internal/graph/schema/task_checklist.gql +++ b/internal/graph/schema/task_checklist.gql @@ -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 { diff --git a/internal/graph/schema/task_group.gql b/internal/graph/schema/task_group.gql index 28c7d92..56f0210 100644 --- a/internal/graph/schema/task_group.gql +++ b/internal/graph/schema/task_group.gql @@ -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 { diff --git a/internal/graph/schema/task_label.gql b/internal/graph/schema/task_label.gql index c7a12a0..ef2886d 100644 --- a/internal/graph/schema/task_label.gql +++ b/internal/graph/schema/task_label.gql @@ -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) + } diff --git a/internal/graph/schema/team.gql b/internal/graph/schema/team.gql index 8300e25..3647134 100644 --- a/internal/graph/schema/team.gql +++ b/internal/graph/schema/team.gql @@ -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 { diff --git a/internal/graph/schema/team_member.gql b/internal/graph/schema/team_member.gql index ff585c6..ca4ef71 100644 --- a/internal/graph/schema/team_member.gql +++ b/internal/graph/schema/team_member.gql @@ -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! -} diff --git a/internal/graph/schema/user.gql b/internal/graph/schema/user.gql index da50b21..d7cd123 100644 --- a/internal/graph/schema/user.gql +++ b/internal/graph/schema/user.gql @@ -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 { diff --git a/internal/route/auth.go b/internal/route/auth.go index d361897..de3390f 100644 --- a/internal/route/auth.go +++ b/internal/route/auth.go @@ -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) } diff --git a/internal/route/middleware.go b/internal/route/middleware.go index ab2e98a..a616003 100644 --- a/internal/route/middleware.go +++ b/internal/route/middleware.go @@ -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)) }) diff --git a/migrations/0049_remove-owner-column-from-project-table.up.sql b/migrations/0049_remove-owner-column-from-project-table.up.sql new file mode 100644 index 0000000..b95df84 --- /dev/null +++ b/migrations/0049_remove-owner-column-from-project-table.up.sql @@ -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; diff --git a/migrations/0050_remove-owner-column-from-team-table.up.sql b/migrations/0050_remove-owner-column-from-team-table.up.sql new file mode 100644 index 0000000..34c1592 --- /dev/null +++ b/migrations/0050_remove-owner-column-from-team-table.up.sql @@ -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;