feat: enforce user roles
enforces user admin role requirement for - creating / deleting / setting role for organization users - creating / deleting / setting role for project users - updating project name - deleting project hides action elements based on role for - admin console - team settings if team is only visible through project membership - add project tile if not team admin - project name text editor if not team / project admin - add redirect from team page if settings only visible through project membership - add redirect from admin console if not org admin role enforcement is handled on the api side through a custom GraphQL directive `hasRole`. on the client side, role information is fetched in the TopNavbar's `me` query and stored in the `UserContext`. there is a custom hook, `useCurrentUser`, that provides a user object with two functions, `isVisibile` & `isAdmin` which is used to check roles in order to render/hide relevant UI elements.
This commit is contained in:
parent
5dbdc20b36
commit
e64f6f8569
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
import Admin from 'shared/components/Admin';
|
import Admin from 'shared/components/Admin';
|
||||||
import Select from 'shared/components/Select';
|
import Select from 'shared/components/Select';
|
||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
@ -16,6 +16,8 @@ import { useForm, Controller } from 'react-hook-form';
|
|||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
|
import UserContext, { useCurrentUser } from 'App/context';
|
||||||
|
import { Redirect } from 'react-router';
|
||||||
|
|
||||||
const DeleteUserWrapper = styled.div`
|
const DeleteUserWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -170,6 +172,7 @@ const AdminRoute = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
const { loading, data } = useUsersQuery();
|
const { loading, data } = useUsersQuery();
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
|
const { user } = useCurrentUser();
|
||||||
const [deleteUser] = useDeleteUserAccountMutation({
|
const [deleteUser] = useDeleteUserAccountMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||||
@ -201,13 +204,17 @@ const AdminRoute = () => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />;
|
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />;
|
||||||
}
|
}
|
||||||
if (data) {
|
if (data && user) {
|
||||||
|
if (user.roles.org != 'admin') {
|
||||||
|
return <Redirect to="/" />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
|
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
|
||||||
<Admin
|
<Admin
|
||||||
initialTab={0}
|
initialTab={0}
|
||||||
users={data.users}
|
users={data.users}
|
||||||
|
canInviteUser={user.roles.org == 'admin'}
|
||||||
onInviteUser={() => {}}
|
onInviteUser={() => {}}
|
||||||
onUpdateUserPassword={(user, password) => {
|
onUpdateUserPassword={(user, password) => {
|
||||||
console.log(user);
|
console.log(user);
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import React, { useContext } from 'react';
|
|
||||||
import { Home, Stack } from 'shared/icons';
|
|
||||||
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from 'shared/components/Navbar';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import UserIDContext from './context';
|
|
||||||
|
|
||||||
const GlobalNavbar = () => {
|
|
||||||
const { userID } = useContext(UserIDContext);
|
|
||||||
if (!userID) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Navbar>
|
|
||||||
<PrimaryLogo />
|
|
||||||
<ButtonContainer>
|
|
||||||
<Link to="/">
|
|
||||||
<ActionButton name="Home">
|
|
||||||
<Home width={28} height={28} />
|
|
||||||
</ActionButton>
|
|
||||||
</Link>
|
|
||||||
<Link to="/projects">
|
|
||||||
<ActionButton name="Projects">
|
|
||||||
<Stack size={28} color="#c2c6dc" />
|
|
||||||
</ActionButton>
|
|
||||||
</Link>
|
|
||||||
</ButtonContainer>
|
|
||||||
</Navbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GlobalNavbar;
|
|
@ -4,7 +4,7 @@ import styled from 'styled-components/macro';
|
|||||||
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
|
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
|
||||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import UserIDContext from 'App/context';
|
import { UserContext, PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||||
import {
|
import {
|
||||||
RoleCode,
|
RoleCode,
|
||||||
useMeQuery,
|
useMeQuery,
|
||||||
@ -16,6 +16,8 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
|
|||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
|
import cache from 'App/cache';
|
||||||
|
|
||||||
const TeamContainer = styled.div`
|
const TeamContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -221,6 +223,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
|
|||||||
type GlobalTopNavbarProps = {
|
type GlobalTopNavbarProps = {
|
||||||
nameOnly?: boolean;
|
nameOnly?: boolean;
|
||||||
projectID: string | null;
|
projectID: string | null;
|
||||||
|
teamID?: string | null;
|
||||||
onChangeProjectOwner?: (userID: string) => void;
|
onChangeProjectOwner?: (userID: string) => void;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
currentTab?: number;
|
currentTab?: number;
|
||||||
@ -239,6 +242,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
onSetTab,
|
onSetTab,
|
||||||
menuType,
|
menuType,
|
||||||
projectID,
|
projectID,
|
||||||
|
teamID,
|
||||||
onChangeProjectOwner,
|
onChangeProjectOwner,
|
||||||
onChangeRole,
|
onChangeRole,
|
||||||
name,
|
name,
|
||||||
@ -250,10 +254,27 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
nameOnly,
|
nameOnly,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(popupContent);
|
console.log(popupContent);
|
||||||
const { loading, data } = useMeQuery();
|
const { user, setUserRoles, setUser } = useCurrentUser();
|
||||||
|
const { loading, data } = useMeQuery({
|
||||||
|
onCompleted: data => {
|
||||||
|
console.log('me query has completed!');
|
||||||
|
if (user && user.roles) {
|
||||||
|
setUserRoles({
|
||||||
|
org: user.roles.org,
|
||||||
|
teams: data.me.teamRoles.reduce((map, obj) => {
|
||||||
|
map.set(obj.teamID, obj.roleCode);
|
||||||
|
return map;
|
||||||
|
}, new Map<string, string>()),
|
||||||
|
projects: data.me.projectRoles.reduce((map, obj) => {
|
||||||
|
map.set(obj.projectID, obj.roleCode);
|
||||||
|
return map;
|
||||||
|
}, new Map<string, string>()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
const { showPopup, hidePopup, setTab } = usePopup();
|
const { showPopup, hidePopup, setTab } = usePopup();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { userID, setUserID } = useContext(UserIDContext);
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
fetch('/auth/logout', {
|
fetch('/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -261,8 +282,9 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
}).then(async x => {
|
}).then(async x => {
|
||||||
const { status } = x;
|
const { status } = x;
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
|
cache.reset();
|
||||||
history.replace('/login');
|
history.replace('/login');
|
||||||
setUserID(null);
|
setUser(null);
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -273,6 +295,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
<Popup title={null} tab={0}>
|
<Popup title={null} tab={0}>
|
||||||
<ProfileMenu
|
<ProfileMenu
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
|
showAdminConsole={user ? user.roles.org === 'admin' : false}
|
||||||
onAdminConsole={() => {
|
onAdminConsole={() => {
|
||||||
history.push('/admin');
|
history.push('/admin');
|
||||||
hidePopup();
|
hidePopup();
|
||||||
@ -295,9 +318,41 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!userID) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
|
||||||
|
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
||||||
|
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
|
||||||
|
const warning =
|
||||||
|
'You 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,
|
||||||
|
<MiniProfile
|
||||||
|
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||||
|
canChangeRole={userIsTeamOrProjectAdmin}
|
||||||
|
onChangeRole={roleCode => {
|
||||||
|
if (onChangeRole) {
|
||||||
|
onChangeRole(member.id, roleCode);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRemoveFromBoard={
|
||||||
|
member.role && member.role.code === 'owner'
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
if (onRemoveFromBoard) {
|
||||||
|
onRemoveFromBoard(member.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user={member}
|
||||||
|
bio=""
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopNavbar
|
<TopNavbar
|
||||||
@ -312,7 +367,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
user={data ? data.me : null}
|
user={data ? data.me.user : null}
|
||||||
|
canEditProjectName={userIsTeamOrProjectAdmin}
|
||||||
|
canInviteUser={userIsTeamOrProjectAdmin}
|
||||||
|
onMemberProfile={onMemberProfile}
|
||||||
onInviteUser={onInviteUser}
|
onInviteUser={onInviteUser}
|
||||||
onChangeRole={onChangeRole}
|
onChangeRole={onChangeRole}
|
||||||
onChangeProjectOwner={onChangeProjectOwner}
|
onChangeProjectOwner={onChangeProjectOwner}
|
||||||
|
5
frontend/src/App/cache.ts
Normal file
5
frontend/src/App/cache.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||||
|
|
||||||
|
const cache = new InMemoryCache();
|
||||||
|
|
||||||
|
export default cache;
|
@ -1,9 +1,80 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
type UserIDContextState = {
|
export enum PermissionLevel {
|
||||||
userID: string | null;
|
ORG,
|
||||||
setUserID: (userID: string | null) => void;
|
TEAM,
|
||||||
|
PROJECT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PermissionObjectType {
|
||||||
|
ORG,
|
||||||
|
TEAM,
|
||||||
|
PROJECT,
|
||||||
|
TASK,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CurrentUserRoles = {
|
||||||
|
org: string;
|
||||||
|
teams: Map<string, string>;
|
||||||
|
projects: Map<string, string>;
|
||||||
};
|
};
|
||||||
export const UserIDContext = React.createContext<UserIDContextState>({ userID: null, setUserID: _userID => null });
|
|
||||||
|
|
||||||
export default UserIDContext;
|
export interface CurrentUserRaw {
|
||||||
|
id: string;
|
||||||
|
roles: CurrentUserRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserContextState = {
|
||||||
|
user: CurrentUserRaw | null;
|
||||||
|
setUser: (user: CurrentUserRaw | null) => void;
|
||||||
|
setUserRoles: (roles: CurrentUserRoles) => void;
|
||||||
|
};
|
||||||
|
export const UserContext = React.createContext<UserContextState>({
|
||||||
|
user: null,
|
||||||
|
setUser: _user => null,
|
||||||
|
setUserRoles: roles => null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface CurrentUser extends CurrentUserRaw {
|
||||||
|
isAdmin: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||||
|
isVisible: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCurrentUser = () => {
|
||||||
|
const { user, setUser, setUserRoles } = useContext(UserContext);
|
||||||
|
let currentUser: CurrentUser | null = null;
|
||||||
|
if (user) {
|
||||||
|
currentUser = {
|
||||||
|
...user,
|
||||||
|
isAdmin(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||||
|
if (user.roles.org === 'admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (level) {
|
||||||
|
case PermissionLevel.TEAM:
|
||||||
|
return subjectID ? this.roles.teams.get(subjectID) === 'admin' : false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isVisible(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||||
|
if (user.roles.org === 'admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (level) {
|
||||||
|
case PermissionLevel.TEAM:
|
||||||
|
return subjectID ? this.roles.teams.get(subjectID) !== null : false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
user: currentUser,
|
||||||
|
setUser,
|
||||||
|
setUserRoles,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserContext;
|
||||||
|
@ -9,8 +9,7 @@ import NormalizeStyles from './NormalizeStyles';
|
|||||||
import BaseStyles from './BaseStyles';
|
import BaseStyles from './BaseStyles';
|
||||||
import { theme } from './ThemeStyles';
|
import { theme } from './ThemeStyles';
|
||||||
import Routes from './Routes';
|
import Routes from './Routes';
|
||||||
import { UserIDContext } from './context';
|
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context';
|
||||||
import Navbar from './Navbar';
|
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
type RefreshTokenResponse = {
|
type RefreshTokenResponse = {
|
||||||
@ -20,7 +19,15 @@ type RefreshTokenResponse = {
|
|||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [userID, setUserID] = useState<string | null>(null);
|
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
||||||
|
const setUserRoles = (roles: CurrentUserRoles) => {
|
||||||
|
if (user) {
|
||||||
|
setUser({
|
||||||
|
...user,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/auth/refresh_token', {
|
fetch('/auth/refresh_token', {
|
||||||
@ -34,7 +41,11 @@ const App = () => {
|
|||||||
const response: RefreshTokenResponse = await x.json();
|
const response: RefreshTokenResponse = await x.json();
|
||||||
const { accessToken, isInstalled } = response;
|
const { accessToken, isInstalled } = response;
|
||||||
const claims: JWTToken = jwtDecode(accessToken);
|
const claims: JWTToken = jwtDecode(accessToken);
|
||||||
setUserID(claims.userId);
|
const currentUser = {
|
||||||
|
id: claims.userId,
|
||||||
|
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||||
|
};
|
||||||
|
setUser(currentUser);
|
||||||
setAccessToken(accessToken);
|
setAccessToken(accessToken);
|
||||||
if (!isInstalled) {
|
if (!isInstalled) {
|
||||||
history.replace('/install');
|
history.replace('/install');
|
||||||
@ -46,7 +57,7 @@ const App = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserIDContext.Provider value={{ userID, setUserID }}>
|
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<NormalizeStyles />
|
<NormalizeStyles />
|
||||||
<BaseStyles />
|
<BaseStyles />
|
||||||
@ -62,7 +73,7 @@ const App = () => {
|
|||||||
</PopupProvider>
|
</PopupProvider>
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</UserIDContext.Provider>
|
</UserContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,13 +6,13 @@ import { setAccessToken } from 'shared/utils/accessToken';
|
|||||||
|
|
||||||
import Login from 'shared/components/Login';
|
import Login from 'shared/components/Login';
|
||||||
import { Container, LoginWrapper } from './Styles';
|
import { Container, LoginWrapper } from './Styles';
|
||||||
import UserIDContext from 'App/context';
|
import UserContext, { PermissionLevel, PermissionObjectType } from 'App/context';
|
||||||
import JwtDecode from 'jwt-decode';
|
import JwtDecode from 'jwt-decode';
|
||||||
|
|
||||||
const Auth = () => {
|
const Auth = () => {
|
||||||
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
|
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { setUserID } = useContext(UserIDContext);
|
const { setUser } = useContext(UserContext);
|
||||||
const login = (
|
const login = (
|
||||||
data: LoginFormData,
|
data: LoginFormData,
|
||||||
setComplete: (val: boolean) => void,
|
setComplete: (val: boolean) => void,
|
||||||
@ -35,7 +35,11 @@ const Auth = () => {
|
|||||||
const response = await x.json();
|
const response = await x.json();
|
||||||
const { accessToken } = response;
|
const { accessToken } = response;
|
||||||
const claims: JWTToken = JwtDecode(accessToken);
|
const claims: JWTToken = JwtDecode(accessToken);
|
||||||
setUserID(claims.userId);
|
const currentUser = {
|
||||||
|
id: claims.userId,
|
||||||
|
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||||
|
};
|
||||||
|
setUser(currentUser);
|
||||||
setComplete(true);
|
setComplete(true);
|
||||||
setAccessToken(accessToken);
|
setAccessToken(accessToken);
|
||||||
|
|
||||||
|
@ -8,13 +8,13 @@ import { getAccessToken, setAccessToken } from 'shared/utils/accessToken';
|
|||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { useApolloClient } from '@apollo/react-hooks';
|
import { useApolloClient } from '@apollo/react-hooks';
|
||||||
import UserIDContext from 'App/context';
|
import UserContext, { PermissionLevel, PermissionObjectType } from 'App/context';
|
||||||
import jwtDecode from 'jwt-decode';
|
import jwtDecode from 'jwt-decode';
|
||||||
|
|
||||||
const Install = () => {
|
const Install = () => {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { setUserID } = useContext(UserIDContext);
|
const { setUser } = useContext(UserContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/auth/refresh_token', {
|
fetch('/auth/refresh_token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -65,7 +65,15 @@ const Install = () => {
|
|||||||
const response: RefreshTokenResponse = await x.data;
|
const response: RefreshTokenResponse = await x.data;
|
||||||
const { accessToken, isInstalled } = response;
|
const { accessToken, isInstalled } = response;
|
||||||
const claims: JWTToken = jwtDecode(accessToken);
|
const claims: JWTToken = jwtDecode(accessToken);
|
||||||
setUserID(claims.userId);
|
const currentUser = {
|
||||||
|
id: claims.userId,
|
||||||
|
roles: {
|
||||||
|
org: claims.orgRole,
|
||||||
|
teams: new Map<string, string>(),
|
||||||
|
projects: new Map<string, string>(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setUser(currentUser);
|
||||||
setAccessToken(accessToken);
|
setAccessToken(accessToken);
|
||||||
if (!isInstalled) {
|
if (!isInstalled) {
|
||||||
history.replace('/install');
|
history.replace('/install');
|
||||||
|
@ -3,9 +3,7 @@ import styled from 'styled-components/macro';
|
|||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { getAccessToken } from 'shared/utils/accessToken';
|
import { getAccessToken } from 'shared/utils/accessToken';
|
||||||
import Navbar from 'App/Navbar';
|
|
||||||
import Settings from 'shared/components/Settings';
|
import Settings from 'shared/components/Settings';
|
||||||
import UserIDContext from 'App/context';
|
|
||||||
import { useMeQuery, useClearProfileAvatarMutation } from 'shared/generated/graphql';
|
import { useMeQuery, useClearProfileAvatarMutation } from 'shared/generated/graphql';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@ -53,7 +51,7 @@ const Projects = () => {
|
|||||||
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
|
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
|
||||||
{!loading && data && (
|
{!loading && data && (
|
||||||
<Settings
|
<Settings
|
||||||
profile={data.me}
|
profile={data.me.user}
|
||||||
onProfileAvatarChange={() => {
|
onProfileAvatarChange={() => {
|
||||||
if ($fileUpload && $fileUpload.current) {
|
if ($fileUpload && $fileUpload.current) {
|
||||||
$fileUpload.current.click();
|
$fileUpload.current.click();
|
||||||
|
@ -8,7 +8,6 @@ import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons';
|
|||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom';
|
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
useSetProjectOwnerMutation,
|
|
||||||
useUpdateProjectMemberRoleMutation,
|
useUpdateProjectMemberRoleMutation,
|
||||||
useCreateProjectMemberMutation,
|
useCreateProjectMemberMutation,
|
||||||
useDeleteProjectMemberMutation,
|
useDeleteProjectMemberMutation,
|
||||||
@ -44,7 +43,7 @@ import SimpleLists from 'shared/components/Lists';
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import MiniProfile from 'shared/components/MiniProfile';
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
import DueDateManager from 'shared/components/DueDateManager';
|
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 LabelManager from 'shared/components/PopupMenu/LabelManager';
|
||||||
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
||||||
import EmptyBoard from 'shared/components/EmptyBoard';
|
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||||
@ -106,174 +105,10 @@ const initialQuickCardEditorState: QuickCardEditorState = {
|
|||||||
type ProjectBoardProps = {
|
type ProjectBoardProps = {
|
||||||
onCardLabelClick?: () => void;
|
onCardLabelClick?: () => void;
|
||||||
cardLabelVariant?: CardLabelVariant;
|
cardLabelVariant?: CardLabelVariant;
|
||||||
projectID?: string;
|
projectID: string;
|
||||||
loading?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
export const BoardLoading = () => {
|
||||||
projectID,
|
|
||||||
onCardLabelClick,
|
|
||||||
cardLabelVariant,
|
|
||||||
loading: isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [assignTask] = useAssignTaskMutation();
|
|
||||||
const [unassignTask] = useUnassignTaskMutation();
|
|
||||||
const $labelsRef = useRef<HTMLDivElement>(null);
|
|
||||||
const match = useRouteMatch();
|
|
||||||
const labelsRef = useRef<Array<ProjectLabel>>([]);
|
|
||||||
const { showPopup, hidePopup } = usePopup();
|
|
||||||
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
|
|
||||||
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
|
|
||||||
const { userID } = useContext(UserIDContext);
|
|
||||||
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
|
|
||||||
const history = useHistory();
|
|
||||||
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
|
|
||||||
update: (client, deletedTaskGroupData) => {
|
|
||||||
updateApolloCache<FindProjectQuery>(
|
|
||||||
client,
|
|
||||||
FindProjectDocument,
|
|
||||||
cache =>
|
|
||||||
produce(cache, draftCache => {
|
|
||||||
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
|
||||||
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
{ projectId: projectID },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [updateTaskName] = useUpdateTaskNameMutation();
|
|
||||||
const [createTask] = useCreateTaskMutation({
|
|
||||||
update: (client, newTaskData) => {
|
|
||||||
updateApolloCache<FindProjectQuery>(
|
|
||||||
client,
|
|
||||||
FindProjectDocument,
|
|
||||||
cache =>
|
|
||||||
produce(cache, draftCache => {
|
|
||||||
const { taskGroups } = cache.findProject;
|
|
||||||
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
|
|
||||||
if (idx !== -1) {
|
|
||||||
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{ projectId: projectID },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [createTaskGroup] = useCreateTaskGroupMutation({
|
|
||||||
update: (client, newTaskGroupData) => {
|
|
||||||
updateApolloCache<FindProjectQuery>(
|
|
||||||
client,
|
|
||||||
FindProjectDocument,
|
|
||||||
cache =>
|
|
||||||
produce(cache, draftCache => {
|
|
||||||
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
|
||||||
}),
|
|
||||||
{ projectId: projectID },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
|
|
||||||
const { loading, data } = useFindProjectQuery({
|
|
||||||
variables: { projectId: projectID ?? '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
|
|
||||||
const [setTaskComplete] = useSetTaskCompleteMutation();
|
|
||||||
const [updateTaskLocation] = useUpdateTaskLocationMutation({
|
|
||||||
update: (client, newTask) => {
|
|
||||||
updateApolloCache<FindProjectQuery>(
|
|
||||||
client,
|
|
||||||
FindProjectDocument,
|
|
||||||
cache =>
|
|
||||||
produce(cache, draftCache => {
|
|
||||||
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
|
||||||
if (previousTaskGroupID !== task.taskGroup.id) {
|
|
||||||
const { taskGroups } = cache.findProject;
|
|
||||||
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
|
||||||
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
|
||||||
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
|
||||||
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
|
||||||
(t: Task) => t.id !== task.id,
|
|
||||||
);
|
|
||||||
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
|
|
||||||
...taskGroups[newTaskGroupIdx].tasks,
|
|
||||||
{ ...task },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{ projectId: projectID },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [deleteTask] = useDeleteTaskMutation();
|
|
||||||
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
|
||||||
onCompleted: newTaskLabel => {
|
|
||||||
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
|
||||||
console.log(taskLabelsRef.current);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCreateTask = (taskGroupID: string, name: string) => {
|
|
||||||
if (data) {
|
|
||||||
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
|
||||||
console.log(`taskGroup ${taskGroup}`);
|
|
||||||
if (taskGroup) {
|
|
||||||
let position = 65535;
|
|
||||||
if (taskGroup.tasks.length !== 0) {
|
|
||||||
const [lastTask] = taskGroup.tasks
|
|
||||||
.slice()
|
|
||||||
.sort((a: any, b: any) => a.position - b.position)
|
|
||||||
.slice(-1);
|
|
||||||
position = Math.ceil(lastTask.position) * 2 + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`position ${position}`);
|
|
||||||
createTask({
|
|
||||||
variables: { taskGroupID, name, position },
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: 'Mutation',
|
|
||||||
createTask: {
|
|
||||||
__typename: 'Task',
|
|
||||||
id: '' + Math.round(Math.random() * -1000000),
|
|
||||||
name,
|
|
||||||
complete: false,
|
|
||||||
taskGroup: {
|
|
||||||
__typename: 'TaskGroup',
|
|
||||||
id: taskGroup.id,
|
|
||||||
name: taskGroup.name,
|
|
||||||
position: taskGroup.position,
|
|
||||||
},
|
|
||||||
badges: {
|
|
||||||
checklist: null,
|
|
||||||
},
|
|
||||||
position,
|
|
||||||
dueDate: null,
|
|
||||||
description: null,
|
|
||||||
labels: [],
|
|
||||||
assigned: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCreateList = (listName: string) => {
|
|
||||||
if (data && projectID) {
|
|
||||||
const [lastColumn] = data.findProject.taskGroups.sort((a, b) => a.position - b.position).slice(-1);
|
|
||||||
let position = 65535;
|
|
||||||
if (lastColumn) {
|
|
||||||
position = lastColumn.position * 2 + 1;
|
|
||||||
}
|
|
||||||
createTaskGroup({ variables: { projectID, name: listName, position } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading || isLoading) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectBar>
|
<ProjectBar>
|
||||||
@ -309,6 +144,169 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
|||||||
<EmptyBoard />
|
<EmptyBoard />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick, cardLabelVariant }) => {
|
||||||
|
const [assignTask] = useAssignTaskMutation();
|
||||||
|
const [unassignTask] = useUnassignTaskMutation();
|
||||||
|
const $labelsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const labelsRef = useRef<Array<ProjectLabel>>([]);
|
||||||
|
const { showPopup, hidePopup } = usePopup();
|
||||||
|
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
|
||||||
|
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
|
||||||
|
const { user } = useCurrentUser();
|
||||||
|
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
|
||||||
|
const history = useHistory();
|
||||||
|
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
|
||||||
|
update: (client, deletedTaskGroupData) => {
|
||||||
|
updateApolloCache<FindProjectQuery>(
|
||||||
|
client,
|
||||||
|
FindProjectDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
||||||
|
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
{ projectID },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [updateTaskName] = useUpdateTaskNameMutation();
|
||||||
|
const [createTask] = useCreateTaskMutation({
|
||||||
|
update: (client, newTaskData) => {
|
||||||
|
updateApolloCache<FindProjectQuery>(
|
||||||
|
client,
|
||||||
|
FindProjectDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
const { taskGroups } = cache.findProject;
|
||||||
|
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ projectID },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [createTaskGroup] = useCreateTaskGroupMutation({
|
||||||
|
update: (client, newTaskGroupData) => {
|
||||||
|
updateApolloCache<FindProjectQuery>(
|
||||||
|
client,
|
||||||
|
FindProjectDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
||||||
|
}),
|
||||||
|
{ projectID },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
|
||||||
|
const { loading, data } = useFindProjectQuery({
|
||||||
|
variables: { projectID },
|
||||||
|
});
|
||||||
|
|
||||||
|
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
|
||||||
|
const [setTaskComplete] = useSetTaskCompleteMutation();
|
||||||
|
const [updateTaskLocation] = useUpdateTaskLocationMutation({
|
||||||
|
update: (client, newTask) => {
|
||||||
|
updateApolloCache<FindProjectQuery>(
|
||||||
|
client,
|
||||||
|
FindProjectDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
||||||
|
if (previousTaskGroupID !== task.taskGroup.id) {
|
||||||
|
const { taskGroups } = cache.findProject;
|
||||||
|
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
||||||
|
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
||||||
|
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
||||||
|
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
||||||
|
(t: Task) => t.id !== task.id,
|
||||||
|
);
|
||||||
|
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
|
||||||
|
...taskGroups[newTaskGroupIdx].tasks,
|
||||||
|
{ ...task },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ projectID },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [deleteTask] = useDeleteTaskMutation();
|
||||||
|
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
||||||
|
onCompleted: newTaskLabel => {
|
||||||
|
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
||||||
|
console.log(taskLabelsRef.current);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCreateTask = (taskGroupID: string, name: string) => {
|
||||||
|
if (data) {
|
||||||
|
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
||||||
|
console.log(`taskGroup ${taskGroup}`);
|
||||||
|
if (taskGroup) {
|
||||||
|
let position = 65535;
|
||||||
|
if (taskGroup.tasks.length !== 0) {
|
||||||
|
const [lastTask] = taskGroup.tasks
|
||||||
|
.slice()
|
||||||
|
.sort((a: any, b: any) => a.position - b.position)
|
||||||
|
.slice(-1);
|
||||||
|
position = Math.ceil(lastTask.position) * 2 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`position ${position}`);
|
||||||
|
createTask({
|
||||||
|
variables: { taskGroupID, name, position },
|
||||||
|
optimisticResponse: {
|
||||||
|
__typename: 'Mutation',
|
||||||
|
createTask: {
|
||||||
|
__typename: 'Task',
|
||||||
|
id: `${Math.round(Math.random() * -1000000)}`,
|
||||||
|
name,
|
||||||
|
complete: false,
|
||||||
|
taskGroup: {
|
||||||
|
__typename: 'TaskGroup',
|
||||||
|
id: taskGroup.id,
|
||||||
|
name: taskGroup.name,
|
||||||
|
position: taskGroup.position,
|
||||||
|
},
|
||||||
|
badges: {
|
||||||
|
__typename: 'TaskBadges',
|
||||||
|
checklist: null,
|
||||||
|
},
|
||||||
|
position,
|
||||||
|
dueDate: null,
|
||||||
|
description: null,
|
||||||
|
labels: [],
|
||||||
|
assigned: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateList = (listName: string) => {
|
||||||
|
if (data && projectID) {
|
||||||
|
const [lastColumn] = data.findProject.taskGroups.sort((a, b) => a.position - b.position).slice(-1);
|
||||||
|
let position = 65535;
|
||||||
|
if (lastColumn) {
|
||||||
|
position = lastColumn.position * 2 + 1;
|
||||||
|
}
|
||||||
|
createTaskGroup({ variables: { projectID, name: listName, position } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <BoardLoading />;
|
||||||
}
|
}
|
||||||
if (data) {
|
if (data) {
|
||||||
labelsRef.current = data.findProject.labels;
|
labelsRef.current = data.findProject.labels;
|
||||||
@ -534,7 +532,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
|||||||
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
|
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
{ projectId: projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
FindTaskQuery,
|
FindTaskQuery,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import UserIDContext from 'App/context';
|
import UserContext, { useCurrentUser } from 'App/context';
|
||||||
import MiniProfile from 'shared/components/MiniProfile';
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
import DueDateManager from 'shared/components/DueDateManager';
|
import DueDateManager from 'shared/components/DueDateManager';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
@ -129,7 +129,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
availableMembers,
|
availableMembers,
|
||||||
refreshCache,
|
refreshCache,
|
||||||
}) => {
|
}) => {
|
||||||
const { userID } = useContext(UserIDContext);
|
const { user } = useCurrentUser();
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
@ -407,7 +407,9 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
user={member}
|
user={member}
|
||||||
bio="None"
|
bio="None"
|
||||||
onRemoveFromTask={() => {
|
onRemoveFromTask={() => {
|
||||||
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
if (user) {
|
||||||
|
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Popup>,
|
</Popup>,
|
||||||
@ -422,10 +424,12 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
availableMembers={availableMembers}
|
availableMembers={availableMembers}
|
||||||
activeMembers={data.findTask.assigned}
|
activeMembers={data.findTask.assigned}
|
||||||
onMemberChange={(member, isActive) => {
|
onMemberChange={(member, isActive) => {
|
||||||
|
if (user) {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
assignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||||
} else {
|
} else {
|
||||||
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import React, {useState} from 'react';
|
import React, { useState } from 'react';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import {usePopup, Popup} from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import {
|
import {
|
||||||
useSetProjectOwnerMutation,
|
|
||||||
useUpdateProjectMemberRoleMutation,
|
useUpdateProjectMemberRoleMutation,
|
||||||
useCreateProjectMemberMutation,
|
useCreateProjectMemberMutation,
|
||||||
useDeleteProjectMemberMutation,
|
useDeleteProjectMemberMutation,
|
||||||
@ -50,7 +49,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
taskLabels: taskLabelsRef,
|
taskLabels: taskLabelsRef,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentLabel, setCurrentLabel] = useState('');
|
const [currentLabel, setCurrentLabel] = useState('');
|
||||||
const {setTab, hidePopup} = usePopup();
|
const { setTab, hidePopup } = usePopup();
|
||||||
const [createProjectLabel] = useCreateProjectLabelMutation({
|
const [createProjectLabel] = useCreateProjectLabelMutation({
|
||||||
update: (client, newLabelData) => {
|
update: (client, newLabelData) => {
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
@ -58,10 +57,10 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.labels.push({...newLabelData.data.createProjectLabel});
|
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
projectId: projectID,
|
projectID,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -78,7 +77,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
label => label.id !== newLabelData.data.deleteProjectLabel.id,
|
label => label.id !== newLabelData.data.deleteProjectLabel.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{projectId: projectID},
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -108,7 +107,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
if (newProjectLabel) {
|
if (newProjectLabel) {
|
||||||
setCurrentTaskLabels([
|
setCurrentTaskLabels([
|
||||||
...currentTaskLabels,
|
...currentTaskLabels,
|
||||||
{id: '', assignedDate: '', projectLabel: {...newProjectLabel}},
|
{ id: '', assignedDate: '', projectLabel: { ...newProjectLabel } },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,12 +126,12 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
label={labels.find(label => label.id === currentLabel) ?? null}
|
label={labels.find(label => label.id === currentLabel) ?? null}
|
||||||
onLabelEdit={(projectLabelID, name, color) => {
|
onLabelEdit={(projectLabelID, name, color) => {
|
||||||
if (projectLabelID) {
|
if (projectLabelID) {
|
||||||
updateProjectLabel({variables: {projectLabelID, labelColorID: color.id, name: name ?? ''}});
|
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
|
||||||
}
|
}
|
||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
onLabelDelete={labelID => {
|
onLabelDelete={labelID => {
|
||||||
deleteProjectLabel({variables: {projectLabelID: labelID}});
|
deleteProjectLabel({ variables: { projectLabelID: labelID } });
|
||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -142,7 +141,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
labelColors={labelColors}
|
labelColors={labelColors}
|
||||||
label={null}
|
label={null}
|
||||||
onLabelEdit={(_labelId, name, color) => {
|
onLabelEdit={(_labelId, name, color) => {
|
||||||
createProjectLabel({variables: {projectID, labelColorID: color.id, name: name ?? ''}});
|
createProjectLabel({ variables: { projectID, labelColorID: color.id, name: name ?? '' } });
|
||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -151,4 +150,4 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LabelManagerEditor
|
export default LabelManagerEditor;
|
||||||
|
@ -15,7 +15,6 @@ import {
|
|||||||
Redirect,
|
Redirect,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
useSetProjectOwnerMutation,
|
|
||||||
useUpdateProjectMemberRoleMutation,
|
useUpdateProjectMemberRoleMutation,
|
||||||
useCreateProjectMemberMutation,
|
useCreateProjectMemberMutation,
|
||||||
useDeleteProjectMemberMutation,
|
useDeleteProjectMemberMutation,
|
||||||
@ -34,10 +33,10 @@ import {
|
|||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
|
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import UserIDContext from 'App/context';
|
import UserContext, { useCurrentUser } from 'App/context';
|
||||||
import Input from 'shared/components/Input';
|
import Input from 'shared/components/Input';
|
||||||
import Member from 'shared/components/Member';
|
import Member from 'shared/components/Member';
|
||||||
import Board from './Board';
|
import Board, { BoardLoading } from './Board';
|
||||||
import Details from './Details';
|
import Details from './Details';
|
||||||
import EmptyBoard from 'shared/components/EmptyBoard';
|
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||||
|
|
||||||
@ -140,7 +139,7 @@ const Project = () => {
|
|||||||
const [updateTaskName] = useUpdateTaskNameMutation();
|
const [updateTaskName] = useUpdateTaskNameMutation();
|
||||||
|
|
||||||
const { loading, data } = useFindProjectQuery({
|
const { loading, data } = useFindProjectQuery({
|
||||||
variables: { projectId: projectID },
|
variables: { projectID },
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateProjectName] = useUpdateProjectNameMutation({
|
const [updateProjectName] = useUpdateProjectNameMutation({
|
||||||
@ -152,7 +151,7 @@ const Project = () => {
|
|||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.name = newName.data.updateProjectName.name;
|
draftCache.findProject.name = newName.data.updateProjectName.name;
|
||||||
}),
|
}),
|
||||||
{ projectId: projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -166,11 +165,10 @@ const Project = () => {
|
|||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.members.push({ ...response.data.createProjectMember.member });
|
draftCache.findProject.members.push({ ...response.data.createProjectMember.member });
|
||||||
}),
|
}),
|
||||||
{ projectId: projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [setProjectOwner] = useSetProjectOwnerMutation();
|
|
||||||
const [deleteProjectMember] = useDeleteProjectMemberMutation({
|
const [deleteProjectMember] = useDeleteProjectMemberMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
@ -184,12 +182,12 @@ const Project = () => {
|
|||||||
m => m.id !== response.data.deleteProjectMember.member.id,
|
m => m.id !== response.data.deleteProjectMember.member.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectId: projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { userID } = useContext(UserIDContext);
|
const { user } = useCurrentUser();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
@ -205,7 +203,7 @@ const Project = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar onSaveProjectName={projectName => {}} name="" projectID={null} />
|
<GlobalTopNavbar onSaveProjectName={projectName => {}} name="" projectID={null} />
|
||||||
<Board loading />
|
<BoardLoading />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -221,7 +219,6 @@ const Project = () => {
|
|||||||
updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
|
updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
|
||||||
}}
|
}}
|
||||||
onChangeProjectOwner={uid => {
|
onChangeProjectOwner={uid => {
|
||||||
setProjectOwner({ variables: { ownerID: uid, projectID } });
|
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onRemoveFromBoard={userID => {
|
onRemoveFromBoard={userID => {
|
||||||
@ -248,6 +245,7 @@ const Project = () => {
|
|||||||
currentTab={0}
|
currentTab={0}
|
||||||
projectMembers={data.findProject.members}
|
projectMembers={data.findProject.members}
|
||||||
projectID={projectID}
|
projectID={projectID}
|
||||||
|
teamID={data.findProject.team.id}
|
||||||
name={data.findProject.name}
|
name={data.findProject.name}
|
||||||
/>
|
/>
|
||||||
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
|
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
|
||||||
|
@ -12,9 +12,8 @@ import {
|
|||||||
|
|
||||||
import ProjectGridItem, { AddProjectItem } from 'shared/components/ProjectGridItem';
|
import ProjectGridItem, { AddProjectItem } from 'shared/components/ProjectGridItem';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Navbar from 'App/Navbar';
|
|
||||||
import NewProject from 'shared/components/NewProject';
|
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 Button from 'shared/components/Button';
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -227,7 +226,7 @@ const ProjectLink = styled(Link)``;
|
|||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { loading, data } = useGetProjectsQuery();
|
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Taskcafé';
|
document.title = 'Taskcafé';
|
||||||
}, []);
|
}, []);
|
||||||
@ -242,7 +241,7 @@ const Projects = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [showNewProject, setShowNewProject] = useState<ShowNewProject>({ open: false, initialTeamID: null });
|
const [showNewProject, setShowNewProject] = useState<ShowNewProject>({ open: false, initialTeamID: null });
|
||||||
const { userID, setUserID } = useContext(UserIDContext);
|
const { user, setUser } = useCurrentUser();
|
||||||
const [createTeam] = useCreateTeamMutation({
|
const [createTeam] = useCreateTeamMutation({
|
||||||
update: (client, createData) => {
|
update: (client, createData) => {
|
||||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||||
@ -261,21 +260,36 @@ const Projects = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
||||||
if (data) {
|
if (data && user) {
|
||||||
|
console.log(user);
|
||||||
const { projects, teams, organizations } = data;
|
const { projects, teams, organizations } = data;
|
||||||
const organizationID = organizations[0].id ?? null;
|
const organizationID = organizations[0].id ?? null;
|
||||||
const projectTeams = teams.map(team => {
|
const projectTeams = teams
|
||||||
|
.sort((a, b) => {
|
||||||
|
const textA = a.name.toUpperCase();
|
||||||
|
const textB = b.name.toUpperCase();
|
||||||
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
|
})
|
||||||
|
.map(team => {
|
||||||
return {
|
return {
|
||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
projects: projects.filter(project => project.team.id === team.id),
|
projects: projects
|
||||||
|
.filter(project => project.team.id === team.id)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const textA = a.name.toUpperCase();
|
||||||
|
const textB = b.name.toUpperCase();
|
||||||
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
console.log(projectTeams);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar onSaveProjectName={() => {}} projectID={null} name={null} />
|
<GlobalTopNavbar onSaveProjectName={() => {}} projectID={null} name={null} />
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ProjectsContainer>
|
<ProjectsContainer>
|
||||||
|
{user.roles.org === 'admin' && (
|
||||||
<AddTeamButton
|
<AddTeamButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={$target => {
|
onClick={$target => {
|
||||||
@ -302,6 +316,7 @@ const Projects = () => {
|
|||||||
>
|
>
|
||||||
Add Team
|
Add Team
|
||||||
</AddTeamButton>
|
</AddTeamButton>
|
||||||
|
)}
|
||||||
{projectTeams.length === 0 && (
|
{projectTeams.length === 0 && (
|
||||||
<EmptyStateContent>
|
<EmptyStateContent>
|
||||||
<EmptyState width={425} height={425} />
|
<EmptyState width={425} height={425} />
|
||||||
@ -340,6 +355,7 @@ const Projects = () => {
|
|||||||
<div key={team.id}>
|
<div key={team.id}>
|
||||||
<ProjectSectionTitleWrapper>
|
<ProjectSectionTitleWrapper>
|
||||||
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
||||||
|
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||||
<SectionActions>
|
<SectionActions>
|
||||||
<SectionActionLink to={`/teams/${team.id}`}>
|
<SectionActionLink to={`/teams/${team.id}`}>
|
||||||
<SectionAction variant="outline">Projects</SectionAction>
|
<SectionAction variant="outline">Projects</SectionAction>
|
||||||
@ -351,6 +367,7 @@ const Projects = () => {
|
|||||||
<SectionAction variant="outline">Settings</SectionAction>
|
<SectionAction variant="outline">Settings</SectionAction>
|
||||||
</SectionActionLink>
|
</SectionActionLink>
|
||||||
</SectionActions>
|
</SectionActions>
|
||||||
|
)}
|
||||||
</ProjectSectionTitleWrapper>
|
</ProjectSectionTitleWrapper>
|
||||||
<ProjectList>
|
<ProjectList>
|
||||||
{team.projects.map((project, idx) => (
|
{team.projects.map((project, idx) => (
|
||||||
@ -363,6 +380,7 @@ const Projects = () => {
|
|||||||
</ProjectTile>
|
</ProjectTile>
|
||||||
</ProjectListItem>
|
</ProjectListItem>
|
||||||
))}
|
))}
|
||||||
|
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||||
<ProjectListItem>
|
<ProjectListItem>
|
||||||
<ProjectAddTile
|
<ProjectAddTile
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -375,6 +393,7 @@ const Projects = () => {
|
|||||||
</ProjectAddTileDetails>
|
</ProjectAddTileDetails>
|
||||||
</ProjectAddTile>
|
</ProjectAddTile>
|
||||||
</ProjectListItem>
|
</ProjectListItem>
|
||||||
|
)}
|
||||||
</ProjectList>
|
</ProjectList>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -383,8 +402,8 @@ const Projects = () => {
|
|||||||
<NewProject
|
<NewProject
|
||||||
initialTeamID={showNewProject.initialTeamID}
|
initialTeamID={showNewProject.initialTeamID}
|
||||||
onCreateProject={(name, teamID) => {
|
onCreateProject={(name, teamID) => {
|
||||||
if (userID) {
|
if (user) {
|
||||||
createProject({ variables: { teamID, name, userID } });
|
createProject({ variables: { teamID, name, userID: user.id } });
|
||||||
setShowNewProject({ open: false, initialTeamID: null });
|
setShowNewProject({ open: false, initialTeamID: null });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -3,15 +3,18 @@ import Input from 'shared/components/Input';
|
|||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import Button from 'shared/components/Button';
|
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 Select from 'shared/components/Select';
|
||||||
import {
|
import {
|
||||||
useGetTeamQuery,
|
useGetTeamQuery,
|
||||||
RoleCode,
|
RoleCode,
|
||||||
useCreateTeamMemberMutation,
|
useCreateTeamMemberMutation,
|
||||||
useDeleteTeamMemberMutation,
|
useDeleteTeamMemberMutation,
|
||||||
|
useUpdateTeamMemberRoleMutation,
|
||||||
GetTeamQuery,
|
GetTeamQuery,
|
||||||
GetTeamDocument,
|
GetTeamDocument,
|
||||||
|
MeDocument,
|
||||||
|
MeQuery,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import { UserPlus, Checkmark } from 'shared/icons';
|
import { UserPlus, Checkmark } from 'shared/icons';
|
||||||
import styled, { css } from 'styled-components/macro';
|
import styled, { css } from 'styled-components/macro';
|
||||||
@ -165,7 +168,6 @@ type TeamRoleManagerPopupProps = {
|
|||||||
canChangeRole: boolean;
|
canChangeRole: boolean;
|
||||||
onChangeRole: (roleCode: RoleCode) => void;
|
onChangeRole: (roleCode: RoleCode) => void;
|
||||||
onRemoveFromTeam?: (newOwnerID: string | null) => void;
|
onRemoveFromTeam?: (newOwnerID: string | null) => void;
|
||||||
onChangeTeamOwner?: (userID: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||||
@ -175,7 +177,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
currentUserID,
|
currentUserID,
|
||||||
canChangeRole,
|
canChangeRole,
|
||||||
onRemoveFromTeam,
|
onRemoveFromTeam,
|
||||||
onChangeTeamOwner,
|
|
||||||
onChangeRole,
|
onChangeRole,
|
||||||
}) => {
|
}) => {
|
||||||
const { hidePopup, setTab } = usePopup();
|
const { hidePopup, setTab } = usePopup();
|
||||||
@ -185,15 +186,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
<Popup title={null} tab={0}>
|
<Popup title={null} tab={0}>
|
||||||
<MiniProfileActions>
|
<MiniProfileActions>
|
||||||
<MiniProfileActionWrapper>
|
<MiniProfileActionWrapper>
|
||||||
{onChangeTeamOwner && (
|
|
||||||
<MiniProfileActionItem
|
|
||||||
onClick={() => {
|
|
||||||
setTab(3);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Set as team owner...
|
|
||||||
</MiniProfileActionItem>
|
|
||||||
)}
|
|
||||||
{subject.role && (
|
{subject.role && (
|
||||||
<MiniProfileActionItem
|
<MiniProfileActionItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -298,24 +290,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
</RemoveMemberButton>
|
</RemoveMemberButton>
|
||||||
</Content>
|
</Content>
|
||||||
</Popup>
|
</Popup>
|
||||||
<Popup title="Set as Team Owner?" onClose={() => hidePopup()} tab={3}>
|
|
||||||
<Content>
|
|
||||||
<DeleteDescription>
|
|
||||||
This will change the project owner from you to this subject. They will be able to view and edit cards,
|
|
||||||
remove members, and change all settings for the project. They will also be able to delete the project.
|
|
||||||
</DeleteDescription>
|
|
||||||
<RemoveMemberButton
|
|
||||||
color="warning"
|
|
||||||
onClick={() => {
|
|
||||||
if (onChangeTeamOwner) {
|
|
||||||
onChangeTeamOwner(subject.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Set as Project Owner
|
|
||||||
</RemoveMemberButton>
|
|
||||||
</Content>
|
|
||||||
</Popup>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -444,7 +418,7 @@ type MembersProps = {
|
|||||||
const Members: React.FC<MembersProps> = ({ teamID }) => {
|
const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
||||||
const { userID } = useContext(UserIDContext);
|
const { user, setUserRoles } = useCurrentUser();
|
||||||
const warning =
|
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”.';
|
'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({
|
const [createTeamMember] = useCreateTeamMemberMutation({
|
||||||
@ -454,12 +428,27 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
GetTeamDocument,
|
GetTeamDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
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 },
|
{ 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({
|
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<GetTeamQuery>(
|
updateApolloCache<GetTeamQuery>(
|
||||||
@ -479,7 +468,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
return <span>loading</span>;
|
return <span>loading</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data && user) {
|
||||||
return (
|
return (
|
||||||
<MemberContainer>
|
<MemberContainer>
|
||||||
<FilterTab>
|
<FilterTab>
|
||||||
@ -497,6 +486,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
</ListDesc>
|
</ListDesc>
|
||||||
<ListActions>
|
<ListActions>
|
||||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||||
|
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && (
|
||||||
<InviteMemberButton
|
<InviteMemberButton
|
||||||
onClick={$target => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
@ -505,6 +495,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
users={data.users}
|
users={data.users}
|
||||||
teamMembers={data.findTeam.members}
|
teamMembers={data.findTeam.members}
|
||||||
onAddTeamMember={userID => {
|
onAddTeamMember={userID => {
|
||||||
|
console.log(`team: ${userID}`);
|
||||||
createTeamMember({ variables: { userID, teamID } });
|
createTeamMember({ variables: { userID, teamID } });
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
@ -514,6 +505,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
<InviteIcon width={16} height={16} />
|
<InviteIcon width={16} height={16} />
|
||||||
Invite Team Members
|
Invite Team Members
|
||||||
</InviteMemberButton>
|
</InviteMemberButton>
|
||||||
|
)}
|
||||||
</ListActions>
|
</ListActions>
|
||||||
</MemberListHeader>
|
</MemberListHeader>
|
||||||
<MemberList>
|
<MemberList>
|
||||||
@ -532,15 +524,14 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
currentUserID={userID ?? ''}
|
currentUserID={user.id ?? ''}
|
||||||
subject={member}
|
subject={member}
|
||||||
members={data.findTeam.members}
|
members={data.findTeam.members}
|
||||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||||
onChangeTeamOwner={
|
canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)}
|
||||||
member.role && member.role.code !== 'owner' ? (userID: string) => {} : undefined
|
onChangeRole={roleCode => {
|
||||||
}
|
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
||||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
}}
|
||||||
onChangeRole={roleCode => {}}
|
|
||||||
onRemoveFromTeam={
|
onRemoveFromTeam={
|
||||||
member.role && member.role.code === 'owner'
|
member.role && member.role.code === 'owner'
|
||||||
? undefined
|
? undefined
|
||||||
|
@ -3,7 +3,7 @@ import styled, { css } from 'styled-components/macro';
|
|||||||
import { MENU_TYPES } from 'shared/components/TopNavbar';
|
import { MENU_TYPES } from 'shared/components/TopNavbar';
|
||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
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 Members from './Members';
|
||||||
import Projects from './Projects';
|
import Projects from './Projects';
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
|
|||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||||
|
import UserContext, { PermissionObjectType, PermissionLevel, useCurrentUser } from 'App/context';
|
||||||
|
|
||||||
const OuterWrapper = styled.div`
|
const OuterWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -87,6 +88,7 @@ const Teams = () => {
|
|||||||
const { teamID } = useParams<TeamsRouteProps>();
|
const { teamID } = useParams<TeamsRouteProps>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
||||||
|
const { user } = useCurrentUser();
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -99,7 +101,10 @@ const Teams = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (data) {
|
if (data && user) {
|
||||||
|
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
||||||
|
return <Redirect to="/" />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar
|
<GlobalTopNavbar
|
||||||
|
@ -4,14 +4,16 @@ import axios from 'axios';
|
|||||||
import createAuthRefreshInterceptor from 'axios-auth-refresh';
|
import createAuthRefreshInterceptor from 'axios-auth-refresh';
|
||||||
import { ApolloProvider } from '@apollo/react-hooks';
|
import { ApolloProvider } from '@apollo/react-hooks';
|
||||||
import { ApolloClient } from 'apollo-client';
|
import { ApolloClient } from 'apollo-client';
|
||||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
|
||||||
import { HttpLink } from 'apollo-link-http';
|
import { HttpLink } from 'apollo-link-http';
|
||||||
import { onError } from 'apollo-link-error';
|
import { onError } from 'apollo-link-error';
|
||||||
|
import { enableMapSet } from 'immer';
|
||||||
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
||||||
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
||||||
|
import cache from './App/cache';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
|
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
|
||||||
|
enableMapSet();
|
||||||
|
|
||||||
let forward$;
|
let forward$;
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
@ -135,7 +137,7 @@ const client = new ApolloClient({
|
|||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
cache: new InMemoryCache(),
|
cache,
|
||||||
});
|
});
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
@ -25,6 +25,7 @@ export const Default = () => {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Admin
|
<Admin
|
||||||
onInviteUser={action('invite user')}
|
onInviteUser={action('invite user')}
|
||||||
|
canInviteUser
|
||||||
initialTab={1}
|
initialTab={1}
|
||||||
onUpdateUserPassword={action('update user password')}
|
onUpdateUserPassword={action('update user password')}
|
||||||
onDeleteUser={action('delete user')}
|
onDeleteUser={action('delete user')}
|
||||||
|
@ -55,7 +55,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
${(props) =>
|
${props =>
|
||||||
props.disabled
|
props.disabled
|
||||||
? css`
|
? css`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -75,7 +75,7 @@ export const Content = styled.div`
|
|||||||
|
|
||||||
export const CurrentPermission = styled.span`
|
export const CurrentPermission = styled.span`
|
||||||
margin-left: 4px;
|
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`
|
export const Separator = styled.div`
|
||||||
@ -86,13 +86,13 @@ export const Separator = styled.div`
|
|||||||
|
|
||||||
export const WarningText = styled.span`
|
export const WarningText = styled.span`
|
||||||
display: flex;
|
display: flex;
|
||||||
color: rgba(${(props) => props.theme.colors.text.primary}, 0.4);
|
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DeleteDescription = styled.div`
|
export const DeleteDescription = styled.div`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgba(${(props) => props.theme.colors.text.primary});
|
color: rgba(${props => props.theme.colors.text.primary});
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RemoveMemberButton = styled(Button)`
|
export const RemoveMemberButton = styled(Button)`
|
||||||
@ -159,8 +159,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
<MiniProfileActions>
|
<MiniProfileActions>
|
||||||
<MiniProfileActionWrapper>
|
<MiniProfileActionWrapper>
|
||||||
{permissions
|
{permissions
|
||||||
.filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||||
.map((perm) => (
|
.map(perm => (
|
||||||
<MiniProfileActionItem
|
<MiniProfileActionItem
|
||||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||||
key={perm.code}
|
key={perm.code}
|
||||||
@ -211,9 +211,9 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
Choose a new user to take over ownership of this user's teams & projects.
|
Choose a new user to take over ownership of this user's teams & projects.
|
||||||
</DeleteDescription>
|
</DeleteDescription>
|
||||||
<UserSelect
|
<UserSelect
|
||||||
onChange={(v) => setDeleteUser(v)}
|
onChange={v => setDeleteUser(v)}
|
||||||
value={deleteUser}
|
value={deleteUser}
|
||||||
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
|
options={users.map(u => ({ label: u.fullName, value: u.id }))}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -239,11 +239,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||||
</DeleteDescription>
|
</DeleteDescription>
|
||||||
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
|
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
|
||||||
<UserSelect
|
<UserSelect onChange={() => {}} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} />
|
||||||
onChange={() => {}}
|
|
||||||
value={null}
|
|
||||||
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
|
|
||||||
/>
|
|
||||||
<UserPassConfirmButton
|
<UserPassConfirmButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// onDeleteUser();
|
// onDeleteUser();
|
||||||
@ -334,14 +330,14 @@ const MemberItemOption = styled(Button)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberList = styled.div`
|
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`
|
const MemberListItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: space-between;
|
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;
|
min-height: 40px;
|
||||||
padding: 12px 0 12px 40px;
|
padding: 12px 0 12px 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -365,11 +361,11 @@ const MemberProfile = styled(TaskAssignee)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberItemName = styled.p`
|
const MemberItemName = styled.p`
|
||||||
color: rgba(${(props) => props.theme.colors.text.secondary});
|
color: rgba(${props => props.theme.colors.text.secondary});
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberItemUsername = styled.p`
|
const MemberItemUsername = styled.p`
|
||||||
color: rgba(${(props) => props.theme.colors.text.primary});
|
color: rgba(${props => props.theme.colors.text.primary});
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberListHeader = styled.div`
|
const MemberListHeader = styled.div`
|
||||||
@ -378,12 +374,12 @@ const MemberListHeader = styled.div`
|
|||||||
`;
|
`;
|
||||||
const ListTitle = styled.h3`
|
const ListTitle = styled.h3`
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: rgba(${(props) => props.theme.colors.text.secondary});
|
color: rgba(${props => props.theme.colors.text.secondary});
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
`;
|
`;
|
||||||
const ListDesc = styled.span`
|
const ListDesc = styled.span`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: rgba(${(props) => props.theme.colors.text.primary});
|
color: rgba(${props => props.theme.colors.text.primary});
|
||||||
`;
|
`;
|
||||||
const FilterSearch = styled(Input)`
|
const FilterSearch = styled(Input)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -484,7 +480,7 @@ const ActionButtons = (params: any) => {
|
|||||||
<ActionButton onClick={() => {}}>
|
<ActionButton onClick={() => {}}>
|
||||||
<EditUserIcon width={16} height={16} />
|
<EditUserIcon width={16} height={16} />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton onClick={($target) => params.onDeleteUser($target, params.value)}>
|
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
|
||||||
<DeleteUserIcon width={16} height={16} />
|
<DeleteUserIcon width={16} height={16} />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</>
|
</>
|
||||||
@ -541,7 +537,7 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
color: ${(props) => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
|
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgba(115, 103, 240);
|
color: rgba(115, 103, 240);
|
||||||
}
|
}
|
||||||
@ -562,7 +558,7 @@ const TabNavLine = styled.span<{ top: number }>`
|
|||||||
width: 2px;
|
width: 2px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
transform: scaleX(1);
|
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));
|
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
|
||||||
box-shadow: 0 0 8px 0 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;
|
onDeleteUser: (userID: string, newOwnerID: string | null) => void;
|
||||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||||
users: Array<User>;
|
users: Array<User>;
|
||||||
|
canInviteUser: boolean;
|
||||||
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -631,6 +628,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
initialTab,
|
initialTab,
|
||||||
onAddUser,
|
onAddUser,
|
||||||
onUpdateUserPassword,
|
onUpdateUserPassword,
|
||||||
|
canInviteUser,
|
||||||
onDeleteUser,
|
onDeleteUser,
|
||||||
onInviteUser,
|
onInviteUser,
|
||||||
users,
|
users,
|
||||||
@ -675,18 +673,20 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
</ListDesc>
|
</ListDesc>
|
||||||
<ListActions>
|
<ListActions>
|
||||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||||
|
{canInviteUser && (
|
||||||
<InviteMemberButton
|
<InviteMemberButton
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
onAddUser($target);
|
onAddUser($target);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InviteIcon width={16} height={16} />
|
<InviteIcon width={16} height={16} />
|
||||||
New Member
|
New Member
|
||||||
</InviteMemberButton>
|
</InviteMemberButton>
|
||||||
|
)}
|
||||||
</ListActions>
|
</ListActions>
|
||||||
</MemberListHeader>
|
</MemberListHeader>
|
||||||
<MemberList>
|
<MemberList>
|
||||||
{users.map((member) => {
|
{users.map(member => {
|
||||||
const projectTotal = member.owned.projects.length + member.member.projects.length;
|
const projectTotal = member.owned.projects.length + member.member.projects.length;
|
||||||
return (
|
return (
|
||||||
<MemberListItem>
|
<MemberListItem>
|
||||||
@ -699,7 +699,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
|
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
|
||||||
<MemberItemOption
|
<MemberItemOption
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
@ -710,7 +710,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
onUpdateUserPassword(user, password);
|
onUpdateUserPassword(user, password);
|
||||||
}}
|
}}
|
||||||
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
|
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
|
||||||
onChangeRole={(roleCode) => {
|
onChangeRole={roleCode => {
|
||||||
updateUserRole({ variables: { userID: member.id, roleCode } });
|
updateUserRole({ variables: { userID: member.id, roleCode } });
|
||||||
}}
|
}}
|
||||||
onDeleteUser={onDeleteUser}
|
onDeleteUser={onDeleteUser}
|
||||||
|
@ -37,17 +37,22 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onClos
|
|||||||
type ProfileMenuProps = {
|
type ProfileMenuProps = {
|
||||||
onProfile: () => void;
|
onProfile: () => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
|
showAdminConsole: boolean;
|
||||||
onAdminConsole: () => void;
|
onAdminConsole: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileMenu: React.FC<ProfileMenuProps> = ({ onAdminConsole, onProfile, onLogout }) => {
|
const ProfileMenu: React.FC<ProfileMenuProps> = ({ showAdminConsole, onAdminConsole, onProfile, onLogout }) => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{showAdminConsole && (
|
||||||
<>
|
<>
|
||||||
<ActionItem onClick={onAdminConsole}>
|
<ActionItem onClick={onAdminConsole}>
|
||||||
<Cog size={16} color="#c2c6dc" />
|
<Cog size={16} color="#c2c6dc" />
|
||||||
<ActionTitle>Admin Console</ActionTitle>
|
<ActionTitle>Admin Console</ActionTitle>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<ActionItem onClick={onProfile}>
|
<ActionItem onClick={onProfile}>
|
||||||
<User size={16} color="#c2c6dc" />
|
<User size={16} color="#c2c6dc" />
|
||||||
<ActionTitle>Profile</ActionTitle>
|
<ActionTitle>Profile</ActionTitle>
|
||||||
|
@ -50,7 +50,6 @@ type MiniProfileProps = {
|
|||||||
onRemoveFromTask?: () => void;
|
onRemoveFromTask?: () => void;
|
||||||
onChangeRole?: (roleCode: RoleCode) => void;
|
onChangeRole?: (roleCode: RoleCode) => void;
|
||||||
onRemoveFromBoard?: () => void;
|
onRemoveFromBoard?: () => void;
|
||||||
onChangeProjectOwner?: (userID: string) => void;
|
|
||||||
warning?: string | null;
|
warning?: string | null;
|
||||||
canChangeRole?: boolean;
|
canChangeRole?: boolean;
|
||||||
};
|
};
|
||||||
@ -58,7 +57,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
|||||||
user,
|
user,
|
||||||
bio,
|
bio,
|
||||||
canChangeRole,
|
canChangeRole,
|
||||||
onChangeProjectOwner,
|
|
||||||
onRemoveFromTask,
|
onRemoveFromTask,
|
||||||
onChangeRole,
|
onChangeRole,
|
||||||
onRemoveFromBoard,
|
onRemoveFromBoard,
|
||||||
@ -91,15 +89,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
|||||||
Remove from card
|
Remove from card
|
||||||
</MiniProfileActionItem>
|
</MiniProfileActionItem>
|
||||||
)}
|
)}
|
||||||
{onChangeProjectOwner && (
|
|
||||||
<MiniProfileActionItem
|
|
||||||
onClick={() => {
|
|
||||||
setTab(3);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Set as project owner
|
|
||||||
</MiniProfileActionItem>
|
|
||||||
)}
|
|
||||||
{onChangeRole && user.role && (
|
{onChangeRole && user.role && (
|
||||||
<MiniProfileActionItem
|
<MiniProfileActionItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -193,24 +182,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
|||||||
</RemoveMemberButton>
|
</RemoveMemberButton>
|
||||||
</Content>
|
</Content>
|
||||||
</Popup>
|
</Popup>
|
||||||
<Popup title="Set as Project Owner?" onClose={() => hidePopup()} tab={3}>
|
|
||||||
<Content>
|
|
||||||
<DeleteDescription>
|
|
||||||
This will change the project owner from you to this user. They will be able to view and edit cards, remove
|
|
||||||
members, and change all settings for the project. They will also be able to delete the project.
|
|
||||||
</DeleteDescription>
|
|
||||||
<RemoveMemberButton
|
|
||||||
color="warning"
|
|
||||||
onClick={() => {
|
|
||||||
if (onChangeProjectOwner) {
|
|
||||||
onChangeProjectOwner(user.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Set as Project Owner
|
|
||||||
</RemoveMemberButton>
|
|
||||||
</Content>
|
|
||||||
</Popup>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -38,6 +38,7 @@ const HomeDashboard = styled(Home)``;
|
|||||||
type ProjectHeadingProps = {
|
type ProjectHeadingProps = {
|
||||||
onFavorite?: () => void;
|
onFavorite?: () => void;
|
||||||
name: string;
|
name: string;
|
||||||
|
canEditProjectName: boolean;
|
||||||
onSaveProjectName?: (projectName: string) => void;
|
onSaveProjectName?: (projectName: string) => void;
|
||||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||||
};
|
};
|
||||||
@ -46,6 +47,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
|||||||
onFavorite,
|
onFavorite,
|
||||||
name: initialProjectName,
|
name: initialProjectName,
|
||||||
onSaveProjectName,
|
onSaveProjectName,
|
||||||
|
canEditProjectName,
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
}) => {
|
}) => {
|
||||||
const [isEditProjectName, setEditProjectName] = useState(false);
|
const [isEditProjectName, setEditProjectName] = useState(false);
|
||||||
@ -94,7 +96,9 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<ProjectName
|
<ProjectName
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (canEditProjectName) {
|
||||||
setEditProjectName(true);
|
setEditProjectName(true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{projectName}
|
{projectName}
|
||||||
@ -142,19 +146,25 @@ type NavBarProps = {
|
|||||||
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
|
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||||
onSaveName?: (name: string) => void;
|
onSaveName?: (name: string) => void;
|
||||||
onNotificationClick: () => void;
|
onNotificationClick: () => void;
|
||||||
|
canEditProjectName?: boolean;
|
||||||
|
canInviteUser?: boolean;
|
||||||
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
|
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
|
||||||
onDashboardClick: () => void;
|
onDashboardClick: () => void;
|
||||||
user: TaskUser | null;
|
user: TaskUser | null;
|
||||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||||
projectMembers?: Array<TaskUser> | null;
|
projectMembers?: Array<TaskUser> | null;
|
||||||
onRemoveFromBoard?: (userID: string) => void;
|
onRemoveFromBoard?: (userID: string) => void;
|
||||||
|
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavBar: React.FC<NavBarProps> = ({
|
const NavBar: React.FC<NavBarProps> = ({
|
||||||
menuType,
|
menuType,
|
||||||
|
canInviteUser = false,
|
||||||
onInviteUser,
|
onInviteUser,
|
||||||
onChangeProjectOwner,
|
onChangeProjectOwner,
|
||||||
currentTab,
|
currentTab,
|
||||||
|
onMemberProfile,
|
||||||
|
canEditProjectName = false,
|
||||||
onOpenProjectFinder,
|
onOpenProjectFinder,
|
||||||
onFavorite,
|
onFavorite,
|
||||||
onSetTab,
|
onSetTab,
|
||||||
@ -175,47 +185,6 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, 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,
|
|
||||||
<MiniProfile
|
|
||||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
|
||||||
onChangeProjectOwner={
|
|
||||||
member.role && member.role.code !== 'owner'
|
|
||||||
? (userID: string) => {
|
|
||||||
if (user && onChangeProjectOwner) {
|
|
||||||
onChangeProjectOwner(userID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
|
||||||
onChangeRole={roleCode => {
|
|
||||||
if (onChangeRole) {
|
|
||||||
onChangeRole(member.id, roleCode);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onRemoveFromBoard={
|
|
||||||
member.role && member.role.code === 'owner'
|
|
||||||
? undefined
|
|
||||||
: () => {
|
|
||||||
if (onRemoveFromBoard) {
|
|
||||||
onRemoveFromBoard(member.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
user={member}
|
|
||||||
bio=""
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarWrapper>
|
<NavbarWrapper>
|
||||||
<NavbarHeader>
|
<NavbarHeader>
|
||||||
@ -226,6 +195,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
onFavorite={onFavorite}
|
onFavorite={onFavorite}
|
||||||
onOpenSettings={onOpenSettings}
|
onOpenSettings={onOpenSettings}
|
||||||
name={name}
|
name={name}
|
||||||
|
canEditProjectName={canEditProjectName}
|
||||||
onSaveProjectName={onSaveName}
|
onSaveProjectName={onSaveName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -255,7 +225,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
<TaskcafeTitle>Taskcafé</TaskcafeTitle>
|
<TaskcafeTitle>Taskcafé</TaskcafeTitle>
|
||||||
</LogoContainer>
|
</LogoContainer>
|
||||||
<GlobalActions>
|
<GlobalActions>
|
||||||
{projectMembers && (
|
{projectMembers && onMemberProfile && (
|
||||||
<>
|
<>
|
||||||
<ProjectMembers>
|
<ProjectMembers>
|
||||||
{projectMembers.map((member, idx) => (
|
{projectMembers.map((member, idx) => (
|
||||||
@ -268,6 +238,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
onMemberProfile={onMemberProfile}
|
onMemberProfile={onMemberProfile}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{canInviteUser && (
|
||||||
<InviteButton
|
<InviteButton
|
||||||
onClick={$target => {
|
onClick={$target => {
|
||||||
if (onInviteUser) {
|
if (onInviteUser) {
|
||||||
@ -278,6 +249,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
>
|
>
|
||||||
Invite
|
Invite
|
||||||
</InviteButton>
|
</InviteButton>
|
||||||
|
)}
|
||||||
</ProjectMembers>
|
</ProjectMembers>
|
||||||
<NavSeparator />
|
<NavSeparator />
|
||||||
</>
|
</>
|
||||||
|
@ -17,6 +17,7 @@ export type Scalars = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export enum RoleCode {
|
export enum RoleCode {
|
||||||
Owner = 'owner',
|
Owner = 'owner',
|
||||||
Admin = 'admin',
|
Admin = 'admin',
|
||||||
@ -125,7 +126,6 @@ export type Project = {
|
|||||||
createdAt: Scalars['Time'];
|
createdAt: Scalars['Time'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
team: Team;
|
team: Team;
|
||||||
owner: Member;
|
|
||||||
taskGroups: Array<TaskGroup>;
|
taskGroups: Array<TaskGroup>;
|
||||||
members: Array<Member>;
|
members: Array<Member>;
|
||||||
labels: Array<ProjectLabel>;
|
labels: Array<ProjectLabel>;
|
||||||
@ -192,6 +192,24 @@ export type TaskChecklist = {
|
|||||||
items: Array<TaskChecklistItem>;
|
items: Array<TaskChecklistItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum RoleLevel {
|
||||||
|
Admin = 'ADMIN',
|
||||||
|
Member = 'MEMBER'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ActionLevel {
|
||||||
|
Org = 'ORG',
|
||||||
|
Team = 'TEAM',
|
||||||
|
Project = 'PROJECT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ObjectType {
|
||||||
|
Org = 'ORG',
|
||||||
|
Team = 'TEAM',
|
||||||
|
Project = 'PROJECT',
|
||||||
|
Task = 'TASK'
|
||||||
|
}
|
||||||
|
|
||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
organizations: Array<Organization>;
|
organizations: Array<Organization>;
|
||||||
@ -204,7 +222,7 @@ export type Query = {
|
|||||||
teams: Array<Team>;
|
teams: Array<Team>;
|
||||||
labelColors: Array<LabelColor>;
|
labelColors: Array<LabelColor>;
|
||||||
taskGroups: Array<TaskGroup>;
|
taskGroups: Array<TaskGroup>;
|
||||||
me: UserAccount;
|
me: MePayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -260,10 +278,8 @@ export type Mutation = {
|
|||||||
deleteUserAccount: DeleteUserAccountPayload;
|
deleteUserAccount: DeleteUserAccountPayload;
|
||||||
logoutUser: Scalars['Boolean'];
|
logoutUser: Scalars['Boolean'];
|
||||||
removeTaskLabel: Task;
|
removeTaskLabel: Task;
|
||||||
setProjectOwner: SetProjectOwnerPayload;
|
|
||||||
setTaskChecklistItemComplete: TaskChecklistItem;
|
setTaskChecklistItemComplete: TaskChecklistItem;
|
||||||
setTaskComplete: Task;
|
setTaskComplete: Task;
|
||||||
setTeamOwner: SetTeamOwnerPayload;
|
|
||||||
toggleTaskLabel: ToggleTaskLabelPayload;
|
toggleTaskLabel: ToggleTaskLabelPayload;
|
||||||
unassignTask: Task;
|
unassignTask: Task;
|
||||||
updateProjectLabel: ProjectLabel;
|
updateProjectLabel: ProjectLabel;
|
||||||
@ -412,11 +428,6 @@ export type MutationRemoveTaskLabelArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationSetProjectOwnerArgs = {
|
|
||||||
input: SetProjectOwner;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationSetTaskChecklistItemCompleteArgs = {
|
export type MutationSetTaskChecklistItemCompleteArgs = {
|
||||||
input: SetTaskChecklistItemComplete;
|
input: SetTaskChecklistItemComplete;
|
||||||
};
|
};
|
||||||
@ -427,11 +438,6 @@ export type MutationSetTaskCompleteArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationSetTeamOwnerArgs = {
|
|
||||||
input: SetTeamOwner;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationToggleTaskLabelArgs = {
|
export type MutationToggleTaskLabelArgs = {
|
||||||
input: ToggleTaskLabelInput;
|
input: ToggleTaskLabelInput;
|
||||||
};
|
};
|
||||||
@ -531,6 +537,25 @@ export type MutationUpdateUserRoleArgs = {
|
|||||||
input: UpdateUserRole;
|
input: UpdateUserRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TeamRole = {
|
||||||
|
__typename?: 'TeamRole';
|
||||||
|
teamID: Scalars['UUID'];
|
||||||
|
roleCode: RoleCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProjectRole = {
|
||||||
|
__typename?: 'ProjectRole';
|
||||||
|
projectID: Scalars['UUID'];
|
||||||
|
roleCode: RoleCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MePayload = {
|
||||||
|
__typename?: 'MePayload';
|
||||||
|
user: UserAccount;
|
||||||
|
teamRoles: Array<TeamRole>;
|
||||||
|
projectRoles: Array<ProjectRole>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectsFilter = {
|
export type ProjectsFilter = {
|
||||||
teamID?: Maybe<Scalars['UUID']>;
|
teamID?: Maybe<Scalars['UUID']>;
|
||||||
};
|
};
|
||||||
@ -540,7 +565,7 @@ export type FindUser = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type FindProject = {
|
export type FindProject = {
|
||||||
projectId: Scalars['String'];
|
projectID: Scalars['UUID'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FindTask = {
|
export type FindTask = {
|
||||||
@ -633,18 +658,6 @@ export type UpdateProjectMemberRolePayload = {
|
|||||||
member: Member;
|
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 = {
|
export type NewTask = {
|
||||||
taskGroupID: Scalars['String'];
|
taskGroupID: Scalars['String'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
@ -868,19 +881,8 @@ export type UpdateTeamMemberRole = {
|
|||||||
export type UpdateTeamMemberRolePayload = {
|
export type UpdateTeamMemberRolePayload = {
|
||||||
__typename?: 'UpdateTeamMemberRolePayload';
|
__typename?: 'UpdateTeamMemberRolePayload';
|
||||||
ok: Scalars['Boolean'];
|
ok: Scalars['Boolean'];
|
||||||
member: Member;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SetTeamOwner = {
|
|
||||||
teamID: Scalars['UUID'];
|
teamID: Scalars['UUID'];
|
||||||
userID: Scalars['UUID'];
|
member: Member;
|
||||||
};
|
|
||||||
|
|
||||||
export type SetTeamOwnerPayload = {
|
|
||||||
__typename?: 'SetTeamOwnerPayload';
|
|
||||||
ok: Scalars['Boolean'];
|
|
||||||
prevOwner: Member;
|
|
||||||
newOwner: Member;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateUserPassword = {
|
export type UpdateUserPassword = {
|
||||||
@ -1066,7 +1068,7 @@ export type DeleteTaskGroupMutation = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export type FindProjectQueryVariables = {
|
export type FindProjectQueryVariables = {
|
||||||
projectId: Scalars['String'];
|
projectID: Scalars['UUID'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1075,7 +1077,10 @@ export type FindProjectQuery = (
|
|||||||
& { findProject: (
|
& { findProject: (
|
||||||
{ __typename?: 'Project' }
|
{ __typename?: 'Project' }
|
||||||
& Pick<Project, 'name'>
|
& Pick<Project, 'name'>
|
||||||
& { members: Array<(
|
& { team: (
|
||||||
|
{ __typename?: 'Team' }
|
||||||
|
& Pick<Team, 'id'>
|
||||||
|
), members: Array<(
|
||||||
{ __typename?: 'Member' }
|
{ __typename?: 'Member' }
|
||||||
& Pick<Member, 'id' | 'fullName' | 'username'>
|
& Pick<Member, 'id' | 'fullName' | 'username'>
|
||||||
& { role: (
|
& { role: (
|
||||||
@ -1242,12 +1247,21 @@ export type MeQueryVariables = {};
|
|||||||
export type MeQuery = (
|
export type MeQuery = (
|
||||||
{ __typename?: 'Query' }
|
{ __typename?: 'Query' }
|
||||||
& { me: (
|
& { me: (
|
||||||
|
{ __typename?: 'MePayload' }
|
||||||
|
& { user: (
|
||||||
{ __typename?: 'UserAccount' }
|
{ __typename?: 'UserAccount' }
|
||||||
& Pick<UserAccount, 'id' | 'fullName'>
|
& Pick<UserAccount, 'id' | 'fullName'>
|
||||||
& { profileIcon: (
|
& { profileIcon: (
|
||||||
{ __typename?: 'ProfileIcon' }
|
{ __typename?: 'ProfileIcon' }
|
||||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||||
) }
|
) }
|
||||||
|
), teamRoles: Array<(
|
||||||
|
{ __typename?: 'TeamRole' }
|
||||||
|
& Pick<TeamRole, 'teamID' | 'roleCode'>
|
||||||
|
)>, projectRoles: Array<(
|
||||||
|
{ __typename?: 'ProjectRole' }
|
||||||
|
& Pick<ProjectRole, 'projectID' | 'roleCode'>
|
||||||
|
)> }
|
||||||
) }
|
) }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1311,35 +1325,6 @@ export type DeleteProjectMemberMutation = (
|
|||||||
) }
|
) }
|
||||||
);
|
);
|
||||||
|
|
||||||
export type SetProjectOwnerMutationVariables = {
|
|
||||||
projectID: Scalars['UUID'];
|
|
||||||
ownerID: Scalars['UUID'];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type SetProjectOwnerMutation = (
|
|
||||||
{ __typename?: 'Mutation' }
|
|
||||||
& { setProjectOwner: (
|
|
||||||
{ __typename?: 'SetProjectOwnerPayload' }
|
|
||||||
& Pick<SetProjectOwnerPayload, 'ok'>
|
|
||||||
& { newOwner: (
|
|
||||||
{ __typename?: 'Member' }
|
|
||||||
& Pick<Member, 'id'>
|
|
||||||
& { role: (
|
|
||||||
{ __typename?: 'Role' }
|
|
||||||
& Pick<Role, 'code' | 'name'>
|
|
||||||
) }
|
|
||||||
), prevOwner: (
|
|
||||||
{ __typename?: 'Member' }
|
|
||||||
& Pick<Member, 'id'>
|
|
||||||
& { role: (
|
|
||||||
{ __typename?: 'Role' }
|
|
||||||
& Pick<Role, 'code' | 'name'>
|
|
||||||
) }
|
|
||||||
) }
|
|
||||||
) }
|
|
||||||
);
|
|
||||||
|
|
||||||
export type UpdateProjectMemberRoleMutationVariables = {
|
export type UpdateProjectMemberRoleMutationVariables = {
|
||||||
projectID: Scalars['UUID'];
|
projectID: Scalars['UUID'];
|
||||||
userID: Scalars['UUID'];
|
userID: Scalars['UUID'];
|
||||||
@ -1706,6 +1691,29 @@ export type GetTeamQuery = (
|
|||||||
)> }
|
)> }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type UpdateTeamMemberRoleMutationVariables = {
|
||||||
|
teamID: Scalars['UUID'];
|
||||||
|
userID: Scalars['UUID'];
|
||||||
|
roleCode: RoleCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type UpdateTeamMemberRoleMutation = (
|
||||||
|
{ __typename?: 'Mutation' }
|
||||||
|
& { updateTeamMemberRole: (
|
||||||
|
{ __typename?: 'UpdateTeamMemberRolePayload' }
|
||||||
|
& Pick<UpdateTeamMemberRolePayload, 'teamID'>
|
||||||
|
& { member: (
|
||||||
|
{ __typename?: 'Member' }
|
||||||
|
& Pick<Member, 'id'>
|
||||||
|
& { role: (
|
||||||
|
{ __typename?: 'Role' }
|
||||||
|
& Pick<Role, 'code' | 'name'>
|
||||||
|
) }
|
||||||
|
) }
|
||||||
|
) }
|
||||||
|
);
|
||||||
|
|
||||||
export type ToggleTaskLabelMutationVariables = {
|
export type ToggleTaskLabelMutationVariables = {
|
||||||
taskID: Scalars['UUID'];
|
taskID: Scalars['UUID'];
|
||||||
projectLabelID: Scalars['UUID'];
|
projectLabelID: Scalars['UUID'];
|
||||||
@ -2325,9 +2333,12 @@ export type DeleteTaskGroupMutationHookResult = ReturnType<typeof useDeleteTaskG
|
|||||||
export type DeleteTaskGroupMutationResult = ApolloReactCommon.MutationResult<DeleteTaskGroupMutation>;
|
export type DeleteTaskGroupMutationResult = ApolloReactCommon.MutationResult<DeleteTaskGroupMutation>;
|
||||||
export type DeleteTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>;
|
export type DeleteTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>;
|
||||||
export const FindProjectDocument = gql`
|
export const FindProjectDocument = gql`
|
||||||
query findProject($projectId: String!) {
|
query findProject($projectID: UUID!) {
|
||||||
findProject(input: {projectId: $projectId}) {
|
findProject(input: {projectID: $projectID}) {
|
||||||
name
|
name
|
||||||
|
team {
|
||||||
|
id
|
||||||
|
}
|
||||||
members {
|
members {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
@ -2418,7 +2429,7 @@ export const FindProjectDocument = gql`
|
|||||||
* @example
|
* @example
|
||||||
* const { data, loading, error } = useFindProjectQuery({
|
* const { data, loading, error } = useFindProjectQuery({
|
||||||
* variables: {
|
* variables: {
|
||||||
* projectId: // value for 'projectId'
|
* projectID: // value for 'projectID'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
@ -2563,6 +2574,7 @@ export type GetProjectsQueryResult = ApolloReactCommon.QueryResult<GetProjectsQu
|
|||||||
export const MeDocument = gql`
|
export const MeDocument = gql`
|
||||||
query me {
|
query me {
|
||||||
me {
|
me {
|
||||||
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
profileIcon {
|
profileIcon {
|
||||||
@ -2571,6 +2583,15 @@ export const MeDocument = gql`
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
teamRoles {
|
||||||
|
teamID
|
||||||
|
roleCode
|
||||||
|
}
|
||||||
|
projectRoles {
|
||||||
|
projectID
|
||||||
|
roleCode
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2717,53 +2738,6 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu
|
|||||||
export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>;
|
export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>;
|
||||||
export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>;
|
export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>;
|
||||||
export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMemberMutation, DeleteProjectMemberMutationVariables>;
|
export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMemberMutation, DeleteProjectMemberMutationVariables>;
|
||||||
export const SetProjectOwnerDocument = gql`
|
|
||||||
mutation setProjectOwner($projectID: UUID!, $ownerID: UUID!) {
|
|
||||||
setProjectOwner(input: {projectID: $projectID, ownerID: $ownerID}) {
|
|
||||||
ok
|
|
||||||
newOwner {
|
|
||||||
id
|
|
||||||
role {
|
|
||||||
code
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevOwner {
|
|
||||||
id
|
|
||||||
role {
|
|
||||||
code
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
export type SetProjectOwnerMutationFn = ApolloReactCommon.MutationFunction<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useSetProjectOwnerMutation__
|
|
||||||
*
|
|
||||||
* To run a mutation, you first call `useSetProjectOwnerMutation` within a React component and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useSetProjectOwnerMutation` returns a tuple that includes:
|
|
||||||
* - A mutate function that you can call at any time to execute the mutation
|
|
||||||
* - An object with fields that represent the current status of the mutation's execution
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const [setProjectOwnerMutation, { data, loading, error }] = useSetProjectOwnerMutation({
|
|
||||||
* variables: {
|
|
||||||
* projectID: // value for 'projectID'
|
|
||||||
* ownerID: // value for 'ownerID'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useSetProjectOwnerMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>) {
|
|
||||||
return ApolloReactHooks.useMutation<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>(SetProjectOwnerDocument, baseOptions);
|
|
||||||
}
|
|
||||||
export type SetProjectOwnerMutationHookResult = ReturnType<typeof useSetProjectOwnerMutation>;
|
|
||||||
export type SetProjectOwnerMutationResult = ApolloReactCommon.MutationResult<SetProjectOwnerMutation>;
|
|
||||||
export type SetProjectOwnerMutationOptions = ApolloReactCommon.BaseMutationOptions<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>;
|
|
||||||
export const UpdateProjectMemberRoleDocument = gql`
|
export const UpdateProjectMemberRoleDocument = gql`
|
||||||
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
||||||
updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) {
|
updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) {
|
||||||
@ -3510,6 +3484,47 @@ export function useGetTeamLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHook
|
|||||||
export type GetTeamQueryHookResult = ReturnType<typeof useGetTeamQuery>;
|
export type GetTeamQueryHookResult = ReturnType<typeof useGetTeamQuery>;
|
||||||
export type GetTeamLazyQueryHookResult = ReturnType<typeof useGetTeamLazyQuery>;
|
export type GetTeamLazyQueryHookResult = ReturnType<typeof useGetTeamLazyQuery>;
|
||||||
export type GetTeamQueryResult = ApolloReactCommon.QueryResult<GetTeamQuery, GetTeamQueryVariables>;
|
export type GetTeamQueryResult = ApolloReactCommon.QueryResult<GetTeamQuery, GetTeamQueryVariables>;
|
||||||
|
export const UpdateTeamMemberRoleDocument = gql`
|
||||||
|
mutation updateTeamMemberRole($teamID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
||||||
|
updateTeamMemberRole(input: {teamID: $teamID, userID: $userID, roleCode: $roleCode}) {
|
||||||
|
member {
|
||||||
|
id
|
||||||
|
role {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teamID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type UpdateTeamMemberRoleMutationFn = ApolloReactCommon.MutationFunction<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUpdateTeamMemberRoleMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUpdateTeamMemberRoleMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUpdateTeamMemberRoleMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [updateTeamMemberRoleMutation, { data, loading, error }] = useUpdateTeamMemberRoleMutation({
|
||||||
|
* variables: {
|
||||||
|
* teamID: // value for 'teamID'
|
||||||
|
* userID: // value for 'userID'
|
||||||
|
* roleCode: // value for 'roleCode'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUpdateTeamMemberRoleMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>) {
|
||||||
|
return ApolloReactHooks.useMutation<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>(UpdateTeamMemberRoleDocument, baseOptions);
|
||||||
|
}
|
||||||
|
export type UpdateTeamMemberRoleMutationHookResult = ReturnType<typeof useUpdateTeamMemberRoleMutation>;
|
||||||
|
export type UpdateTeamMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateTeamMemberRoleMutation>;
|
||||||
|
export type UpdateTeamMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>;
|
||||||
export const ToggleTaskLabelDocument = gql`
|
export const ToggleTaskLabelDocument = gql`
|
||||||
mutation toggleTaskLabel($taskID: UUID!, $projectLabelID: UUID!) {
|
mutation toggleTaskLabel($taskID: UUID!, $projectLabelID: UUID!) {
|
||||||
toggleTaskLabel(input: {taskID: $taskID, projectLabelID: $projectLabelID}) {
|
toggleTaskLabel(input: {taskID: $taskID, projectLabelID: $projectLabelID}) {
|
||||||
|
@ -2,9 +2,12 @@ import gql from 'graphql-tag';
|
|||||||
import TASK_FRAGMENT from './fragments/task';
|
import TASK_FRAGMENT from './fragments/task';
|
||||||
|
|
||||||
const FIND_PROJECT_QUERY = gql`
|
const FIND_PROJECT_QUERY = gql`
|
||||||
query findProject($projectId: String!) {
|
query findProject($projectID: UUID!) {
|
||||||
findProject(input: { projectId: $projectId }) {
|
findProject(input: { projectID: $projectID }) {
|
||||||
name
|
name
|
||||||
|
team {
|
||||||
|
id
|
||||||
|
}
|
||||||
members {
|
members {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
query me {
|
query me {
|
||||||
me {
|
me {
|
||||||
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
profileIcon {
|
profileIcon {
|
||||||
@ -8,4 +9,13 @@ query me {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
teamRoles {
|
||||||
|
teamID
|
||||||
|
roleCode
|
||||||
|
}
|
||||||
|
projectRoles {
|
||||||
|
projectID
|
||||||
|
roleCode
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
18
frontend/src/shared/graphql/team/updateTeamMemberRole.ts
Normal file
18
frontend/src/shared/graphql/team/updateTeamMemberRole.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
export const UPDATE_TEAM_MEMBER_ROLE_MUTATION = gql`
|
||||||
|
mutation updateTeamMemberRole($teamID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
||||||
|
updateTeamMemberRole(input: { teamID: $teamID, userID: $userID, roleCode: $roleCode }) {
|
||||||
|
member {
|
||||||
|
id
|
||||||
|
role {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teamID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default UPDATE_TEAM_MEMBER_ROLE_MUTATION;
|
5
frontend/src/shared/utils/user.ts
Normal file
5
frontend/src/shared/utils/user.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { PermissionObjectType, PermissionLevel } from 'App/context';
|
||||||
|
|
||||||
|
export default function userCan(level: PermissionLevel, objectType: PermissionObjectType) {
|
||||||
|
return false;
|
||||||
|
}
|
1
frontend/src/taskcafe.d.ts
vendored
1
frontend/src/taskcafe.d.ts
vendored
@ -1,5 +1,6 @@
|
|||||||
interface JWTToken {
|
interface JWTToken {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
orgRole: string;
|
||||||
iat: string;
|
iat: string;
|
||||||
exp: string;
|
exp: string;
|
||||||
}
|
}
|
||||||
|
1
go.mod
1
go.mod
@ -17,7 +17,6 @@ require (
|
|||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/jmoiron/sqlx v1.2.0
|
github.com/jmoiron/sqlx v1.2.0
|
||||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
|
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/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
|
||||||
github.com/lib/pq v1.3.0
|
github.com/lib/pq v1.3.0
|
||||||
github.com/magefile/mage v1.9.0
|
github.com/magefile/mage v1.9.0
|
||||||
|
@ -16,9 +16,17 @@ const (
|
|||||||
InstallOnly = "install_only"
|
InstallOnly = "install_only"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleAdmin Role = "admin"
|
||||||
|
RoleMember Role = "member"
|
||||||
|
)
|
||||||
|
|
||||||
type AccessTokenClaims struct {
|
type AccessTokenClaims struct {
|
||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
Restricted RestrictedMode `json:"restricted"`
|
Restricted RestrictedMode `json:"restricted"`
|
||||||
|
OrgRole Role `json:"orgRole"`
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,11 +47,16 @@ func (r *ErrMalformedToken) Error() string {
|
|||||||
return "token is malformed"
|
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)
|
accessExpirationTime := time.Now().Add(5 * time.Second)
|
||||||
accessClaims := &AccessTokenClaims{
|
accessClaims := &AccessTokenClaims{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Restricted: restrictedMode,
|
Restricted: restrictedMode,
|
||||||
|
OrgRole: role,
|
||||||
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
|
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +73,7 @@ func NewAccessTokenCustomExpiration(userID string, dur time.Duration) (string, e
|
|||||||
accessClaims := &AccessTokenClaims{
|
accessClaims := &AccessTokenClaims{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Restricted: Unrestricted,
|
Restricted: Unrestricted,
|
||||||
|
OrgRole: RoleMember,
|
||||||
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
|
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,6 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
rootCmd.SetVersionTemplate(versionTemplate)
|
rootCmd.SetVersionTemplate(versionTemplate)
|
||||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd())
|
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd())
|
||||||
rootCmd.Execute()
|
rootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
@ -10,7 +12,6 @@ import (
|
|||||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/jordanknott/taskcafe/internal/config"
|
"github.com/jordanknott/taskcafe/internal/config"
|
||||||
"github.com/jordanknott/taskcafe/internal/migrations"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,6 +28,12 @@ func (l *MigrateLog) Verbose() bool {
|
|||||||
return l.verbose
|
return l.verbose
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var migration http.FileSystem
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migration = http.Dir("./migrations")
|
||||||
|
}
|
||||||
|
|
||||||
func newMigrateCmd() *cobra.Command {
|
func newMigrateCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "migrate",
|
Use: "migrate",
|
||||||
@ -53,7 +60,7 @@ func newMigrateCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
src, err := httpfs.New(migrations.Migrations, "./")
|
src, err := httpfs.New(migration, "./")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
13
internal/commands/migrate_prod.go
Normal file
13
internal/commands/migrate_prod.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// +build prod
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jordanknott/taskcafe/internal/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migration = migrations.Migrations
|
||||||
|
}
|
27
internal/commands/token.go
Normal file
27
internal/commands/token.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jordanknott/taskcafe/internal/auth"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTokenCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "token",
|
||||||
|
Short: "Create a long lived JWT token for dev purposes",
|
||||||
|
Long: "Create a long lived JWT token for dev purposes",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
token, err := auth.NewAccessTokenCustomExpiration(args[0], time.Hour*24)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("issue while creating access token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(token)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,6 @@ type Project struct {
|
|||||||
TeamID uuid.UUID `json:"team_id"`
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Owner uuid.UUID `json:"owner"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectLabel struct {
|
type ProjectLabel struct {
|
||||||
@ -120,7 +119,6 @@ type Team struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
Owner uuid.UUID `json:"owner"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TeamMember struct {
|
type TeamMember struct {
|
||||||
|
@ -11,30 +11,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const createProject = `-- name: CreateProject :one
|
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 {
|
type CreateProjectParams struct {
|
||||||
Owner uuid.UUID `json:"owner"`
|
|
||||||
TeamID uuid.UUID `json:"team_id"`
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
|
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
|
||||||
row := q.db.QueryRowContext(ctx, createProject,
|
row := q.db.QueryRowContext(ctx, createProject, arg.TeamID, arg.CreatedAt, arg.Name)
|
||||||
arg.Owner,
|
|
||||||
arg.TeamID,
|
|
||||||
arg.CreatedAt,
|
|
||||||
arg.Name,
|
|
||||||
)
|
|
||||||
var i Project
|
var i Project
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ProjectID,
|
&i.ProjectID,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -93,7 +86,7 @@ func (q *Queries) DeleteProjectMember(ctx context.Context, arg DeleteProjectMemb
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAllProjects = `-- name: GetAllProjects :many
|
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) {
|
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.TeamID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -126,7 +118,7 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many
|
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) {
|
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.TeamID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.Name,
|
&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 {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -185,73 +209,8 @@ func (q *Queries) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.
|
|||||||
return items, nil
|
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
|
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) {
|
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.TeamID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -300,6 +258,38 @@ func (q *Queries) GetProjectMembersForProjectID(ctx context.Context, projectID u
|
|||||||
return items, nil
|
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
|
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
|
WHERE user_id = $1 AND project_id = $2
|
||||||
@ -317,25 +307,29 @@ func (q *Queries) GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRo
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const setProjectOwner = `-- name: SetProjectOwner :one
|
const getUserRolesForProject = `-- name: GetUserRolesForProject :one
|
||||||
UPDATE project SET owner = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name, owner
|
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"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
Owner uuid.UUID `json:"owner"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SetProjectOwner(ctx context.Context, arg SetProjectOwnerParams) (Project, error) {
|
type GetUserRolesForProjectRow struct {
|
||||||
row := q.db.QueryRowContext(ctx, setProjectOwner, arg.ProjectID, arg.Owner)
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
var i Project
|
TeamRole string `json:"team_role"`
|
||||||
err := row.Scan(
|
ProjectRole string `json:"project_role"`
|
||||||
&i.ProjectID,
|
}
|
||||||
&i.TeamID,
|
|
||||||
&i.CreatedAt,
|
func (q *Queries) GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error) {
|
||||||
&i.Name,
|
row := q.db.QueryRowContext(ctx, getUserRolesForProject, arg.UserID, arg.ProjectID)
|
||||||
&i.Owner,
|
var i GetUserRolesForProjectRow
|
||||||
)
|
err := row.Scan(&i.TeamID, &i.TeamRole, &i.ProjectRole)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +358,7 @@ func (q *Queries) UpdateProjectMemberRole(ctx context.Context, arg UpdateProject
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateProjectNameByID = `-- name: UpdateProjectNameByID :one
|
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 {
|
type UpdateProjectNameByIDParams struct {
|
||||||
@ -380,39 +374,6 @@ func (q *Queries) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNa
|
|||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
|
||||||
)
|
)
|
||||||
return i, err
|
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
|
|
||||||
}
|
|
||||||
|
@ -49,19 +49,18 @@ type Querier interface {
|
|||||||
GetAllTasks(ctx context.Context) ([]Task, error)
|
GetAllTasks(ctx context.Context) ([]Task, error)
|
||||||
GetAllTeams(ctx context.Context) ([]Team, error)
|
GetAllTeams(ctx context.Context) ([]Team, error)
|
||||||
GetAllUserAccounts(ctx context.Context) ([]UserAccount, 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)
|
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
|
||||||
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
|
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
|
||||||
GetLabelColors(ctx context.Context) ([]LabelColor, error)
|
GetLabelColors(ctx context.Context) ([]LabelColor, error)
|
||||||
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||||
GetMemberTeamIDsForUserID(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)
|
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
|
||||||
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
|
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
|
||||||
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
|
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
|
||||||
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
|
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
|
||||||
GetProjectMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectMember, 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)
|
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
|
||||||
GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
|
GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
|
||||||
GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (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)
|
GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error)
|
||||||
GetTeamMemberByID(ctx context.Context, arg GetTeamMemberByIDParams) (TeamMember, error)
|
GetTeamMemberByID(ctx context.Context, arg GetTeamMemberByIDParams) (TeamMember, error)
|
||||||
GetTeamMembersForTeamID(ctx context.Context, teamID uuid.UUID) ([]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)
|
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)
|
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
|
||||||
GetUserAccountByUsername(ctx context.Context, username string) (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)
|
SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
|
||||||
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
|
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
|
||||||
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, 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)
|
SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
|
||||||
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
|
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
|
||||||
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
|
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
|
||||||
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
|
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
|
||||||
UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error)
|
UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error)
|
||||||
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, 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)
|
UpdateTaskChecklistItemLocation(ctx context.Context, arg UpdateTaskChecklistItemLocationParams) (TaskChecklistItem, error)
|
||||||
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
|
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
|
||||||
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
|
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
|
||||||
@ -106,7 +106,6 @@ type Querier interface {
|
|||||||
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
|
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
|
||||||
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
|
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
|
||||||
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, 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)
|
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
|
||||||
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
|
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,7 @@ SELECT * FROM project WHERE team_id = $1;
|
|||||||
SELECT * FROM project WHERE project_id = $1;
|
SELECT * FROM project WHERE project_id = $1;
|
||||||
|
|
||||||
-- name: CreateProject :one
|
-- name: CreateProject :one
|
||||||
INSERT INTO project(owner, team_id, created_at, name) VALUES ($1, $2, $3, $4) RETURNING *;
|
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING *;
|
||||||
|
|
||||||
-- name: SetProjectOwner :one
|
|
||||||
UPDATE project SET owner = $2 WHERE project_id = $1 RETURNING *;
|
|
||||||
|
|
||||||
-- name: UpdateProjectNameByID :one
|
-- name: UpdateProjectNameByID :one
|
||||||
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING *;
|
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING *;
|
||||||
@ -37,14 +34,19 @@ DELETE FROM project_member WHERE user_id = $1 AND project_id = $2;
|
|||||||
UPDATE project_member SET role_code = $3 WHERE project_id = $1 AND user_id = $2
|
UPDATE project_member SET role_code = $3 WHERE project_id = $1 AND user_id = $2
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetOwnedTeamProjectsForUserID :many
|
-- name: GetProjectRolesForUserID :many
|
||||||
SELECT project_id FROM project WHERE owner = $1 AND team_id = $2;
|
SELECT project_id, role_code FROM project_member WHERE user_id = $1;
|
||||||
|
|
||||||
-- name: GetOwnedProjectsForUserID :many
|
|
||||||
SELECT * FROM project WHERE owner = $1;
|
|
||||||
|
|
||||||
-- name: GetMemberProjectIDsForUserID :many
|
-- name: GetMemberProjectIDsForUserID :many
|
||||||
SELECT project_id FROM project_member WHERE user_id = $1;
|
SELECT project_id FROM project_member WHERE user_id = $1;
|
||||||
|
|
||||||
-- name: UpdateProjectOwnerByOwnerID :many
|
-- name: GetAllVisibleProjectsForUserID :many
|
||||||
UPDATE project SET owner = $2 WHERE owner = $1 RETURNING project_id;
|
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;
|
||||||
|
@ -5,7 +5,7 @@ SELECT * FROM team;
|
|||||||
SELECT * FROM team WHERE team_id = $1;
|
SELECT * FROM team WHERE team_id = $1;
|
||||||
|
|
||||||
-- name: CreateTeam :one
|
-- 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
|
-- name: DeleteTeamByID :exec
|
||||||
DELETE FROM team WHERE team_id = $1;
|
DELETE FROM team WHERE team_id = $1;
|
||||||
@ -13,14 +13,15 @@ DELETE FROM team WHERE team_id = $1;
|
|||||||
-- name: GetTeamsForOrganization :many
|
-- name: GetTeamsForOrganization :many
|
||||||
SELECT * FROM team WHERE organization_id = $1;
|
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
|
-- name: GetMemberTeamIDsForUserID :many
|
||||||
SELECT team_id FROM team_member WHERE user_id = $1;
|
SELECT team_id FROM team_member WHERE user_id = $1;
|
||||||
|
|
||||||
-- name: UpdateTeamOwnerByOwnerID :many
|
-- name: GetTeamRoleForUserID :one
|
||||||
UPDATE team SET owner = $2 WHERE owner = $1 RETURNING team_id;
|
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;
|
||||||
|
@ -11,30 +11,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const createTeam = `-- name: CreateTeam :one
|
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 {
|
type CreateTeamParams struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Owner uuid.UUID `json:"owner"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error) {
|
func (q *Queries) CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error) {
|
||||||
row := q.db.QueryRowContext(ctx, createTeam,
|
row := q.db.QueryRowContext(ctx, createTeam, arg.OrganizationID, arg.CreatedAt, arg.Name)
|
||||||
arg.OrganizationID,
|
|
||||||
arg.CreatedAt,
|
|
||||||
arg.Name,
|
|
||||||
arg.Owner,
|
|
||||||
)
|
|
||||||
var i Team
|
var i Team
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OrganizationID,
|
&i.OrganizationID,
|
||||||
&i.Owner,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -49,7 +42,7 @@ func (q *Queries) DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAllTeams = `-- name: GetAllTeams :many
|
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) {
|
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.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OrganizationID,
|
&i.OrganizationID,
|
||||||
&i.Owner,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -108,26 +100,62 @@ func (q *Queries) GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUI
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOwnedTeamsForUserID = `-- name: GetOwnedTeamsForUserID :many
|
const getTeamByID = `-- name: GetTeamByID :one
|
||||||
SELECT team_id, created_at, name, organization_id, owner FROM team WHERE owner = $1
|
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) {
|
func (q *Queries) GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, getOwnedTeamsForUserID, owner)
|
row := q.db.QueryRowContext(ctx, getTeamByID, teamID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Team
|
|
||||||
for rows.Next() {
|
|
||||||
var i Team
|
var i Team
|
||||||
if err := rows.Scan(
|
err := row.Scan(
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OrganizationID,
|
&i.OrganizationID,
|
||||||
&i.Owner,
|
)
|
||||||
); err != nil {
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTeamRoleForUserID = `-- name: GetTeamRoleForUserID :one
|
||||||
|
SELECT team_id, role_code FROM team_member WHERE user_id = $1 AND team_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTeamRoleForUserIDParams struct {
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetTeamRoleForUserIDRow struct {
|
||||||
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
|
RoleCode string `json:"role_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTeamRoleForUserID(ctx context.Context, arg GetTeamRoleForUserIDParams) (GetTeamRoleForUserIDRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getTeamRoleForUserID, arg.UserID, arg.TeamID)
|
||||||
|
var i GetTeamRoleForUserIDRow
|
||||||
|
err := row.Scan(&i.TeamID, &i.RoleCode)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTeamRolesForUserID = `-- name: GetTeamRolesForUserID :many
|
||||||
|
SELECT team_id, role_code FROM team_member WHERE user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTeamRolesForUserIDRow struct {
|
||||||
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
|
RoleCode string `json:"role_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getTeamRolesForUserID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetTeamRolesForUserIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetTeamRolesForUserIDRow
|
||||||
|
if err := rows.Scan(&i.TeamID, &i.RoleCode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
@ -141,25 +169,8 @@ func (q *Queries) GetOwnedTeamsForUserID(ctx context.Context, owner uuid.UUID) (
|
|||||||
return items, nil
|
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
|
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) {
|
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.CreatedAt,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.OrganizationID,
|
&i.OrganizationID,
|
||||||
&i.Owner,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -191,50 +201,29 @@ func (q *Queries) GetTeamsForOrganization(ctx context.Context, organizationID uu
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTeamOwner = `-- name: SetTeamOwner :one
|
const getTeamsForUserIDWhereAdmin = `-- name: GetTeamsForUserIDWhereAdmin :many
|
||||||
UPDATE team SET owner = $2 WHERE team_id = $1 RETURNING team_id, created_at, name, organization_id, owner
|
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 {
|
func (q *Queries) GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error) {
|
||||||
TeamID uuid.UUID `json:"team_id"`
|
rows, err := q.db.QueryContext(ctx, getTeamsForUserIDWhereAdmin, userID)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []uuid.UUID
|
var items []Team
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var team_id uuid.UUID
|
var i Team
|
||||||
if err := rows.Scan(&team_id); err != nil {
|
if err := rows.Scan(
|
||||||
|
&i.TeamID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.OrganizationID,
|
||||||
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, team_id)
|
items = append(items, i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,10 +2,13 @@ package graph
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"github.com/99designs/gqlgen/graphql/handler"
|
||||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||||
"github.com/99designs/gqlgen/graphql/handler/lru"
|
"github.com/99designs/gqlgen/graphql/handler/lru"
|
||||||
@ -15,16 +18,71 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
"github.com/jordanknott/taskcafe/internal/auth"
|
||||||
"github.com/jordanknott/taskcafe/internal/config"
|
"github.com/jordanknott/taskcafe/internal/config"
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHandler returns a new graphql endpoint handler.
|
// NewHandler returns a new graphql endpoint handler.
|
||||||
func NewHandler(config config.AppConfig, repo db.Repository) http.Handler {
|
func NewHandler(config config.AppConfig, repo db.Repository) http.Handler {
|
||||||
srv := handler.New(NewExecutableSchema(Config{
|
c := Config{
|
||||||
Resolvers: &Resolver{
|
Resolvers: &Resolver{
|
||||||
Config: config,
|
Config: config,
|
||||||
Repository: repo,
|
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{
|
srv.AddTransport(transport.Websocket{
|
||||||
KeepAlivePingInterval: 10 * time.Second,
|
KeepAlivePingInterval: 10 * time.Second,
|
||||||
})
|
})
|
||||||
@ -55,8 +113,80 @@ func GetUserID(ctx context.Context) (uuid.UUID, bool) {
|
|||||||
userID, ok := ctx.Value("userID").(uuid.UUID)
|
userID, ok := ctx.Value("userID").(uuid.UUID)
|
||||||
return userID, ok
|
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) {
|
func GetRestrictedMode(ctx context.Context) (auth.RestrictedMode, bool) {
|
||||||
restricted, ok := ctx.Value("restricted_mode").(auth.RestrictedMode)
|
restricted, ok := ctx.Value("restricted_mode").(auth.RestrictedMode)
|
||||||
return restricted, ok
|
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
|
||||||
|
}
|
||||||
|
@ -8,15 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) {
|
func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) {
|
||||||
ownedTeams, err := r.GetOwnedTeamsForUserID(ctx, user.UserID)
|
return &OwnedList{}, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) {
|
func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) {
|
||||||
projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID)
|
projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID)
|
||||||
|
@ -152,7 +152,7 @@ type DeleteUserAccountPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FindProject struct {
|
type FindProject struct {
|
||||||
ProjectID string `json:"projectId"`
|
ProjectID uuid.UUID `json:"projectID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindTask struct {
|
type FindTask struct {
|
||||||
@ -171,6 +171,12 @@ type LogoutUser struct {
|
|||||||
UserID string `json:"userID"`
|
UserID string `json:"userID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MePayload struct {
|
||||||
|
User *db.UserAccount `json:"user"`
|
||||||
|
TeamRoles []TeamRole `json:"teamRoles"`
|
||||||
|
ProjectRoles []ProjectRole `json:"projectRoles"`
|
||||||
|
}
|
||||||
|
|
||||||
type Member struct {
|
type Member struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Role *db.Role `json:"role"`
|
Role *db.Role `json:"role"`
|
||||||
@ -255,6 +261,11 @@ type ProfileIcon struct {
|
|||||||
BgColor *string `json:"bgColor"`
|
BgColor *string `json:"bgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectRole struct {
|
||||||
|
ProjectID uuid.UUID `json:"projectID"`
|
||||||
|
RoleCode RoleCode `json:"roleCode"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProjectsFilter struct {
|
type ProjectsFilter struct {
|
||||||
TeamID *uuid.UUID `json:"teamID"`
|
TeamID *uuid.UUID `json:"teamID"`
|
||||||
}
|
}
|
||||||
@ -263,17 +274,6 @@ type RemoveTaskLabelInput struct {
|
|||||||
TaskLabelID uuid.UUID `json:"taskLabelID"`
|
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 {
|
type SetTaskChecklistItemComplete struct {
|
||||||
TaskChecklistItemID uuid.UUID `json:"taskChecklistItemID"`
|
TaskChecklistItemID uuid.UUID `json:"taskChecklistItemID"`
|
||||||
Complete bool `json:"complete"`
|
Complete bool `json:"complete"`
|
||||||
@ -284,21 +284,15 @@ type SetTaskComplete struct {
|
|||||||
Complete bool `json:"complete"`
|
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 {
|
type TaskBadges struct {
|
||||||
Checklist *ChecklistBadge `json:"checklist"`
|
Checklist *ChecklistBadge `json:"checklist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamRole struct {
|
||||||
|
TeamID uuid.UUID `json:"teamID"`
|
||||||
|
RoleCode RoleCode `json:"roleCode"`
|
||||||
|
}
|
||||||
|
|
||||||
type ToggleTaskLabelInput struct {
|
type ToggleTaskLabelInput struct {
|
||||||
TaskID uuid.UUID `json:"taskID"`
|
TaskID uuid.UUID `json:"taskID"`
|
||||||
ProjectLabelID uuid.UUID `json:"projectLabelID"`
|
ProjectLabelID uuid.UUID `json:"projectLabelID"`
|
||||||
@ -410,6 +404,7 @@ type UpdateTeamMemberRole struct {
|
|||||||
|
|
||||||
type UpdateTeamMemberRolePayload struct {
|
type UpdateTeamMemberRolePayload struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
|
TeamID uuid.UUID `json:"teamID"`
|
||||||
Member *Member `json:"member"`
|
Member *Member `json:"member"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,6 +427,94 @@ type UpdateUserRolePayload struct {
|
|||||||
User *db.UserAccount `json:"user"`
|
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
|
type RoleCode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -476,3 +559,44 @@ func (e *RoleCode) UnmarshalGQL(v interface{}) error {
|
|||||||
func (e RoleCode) MarshalGQL(w io.Writer) {
|
func (e RoleCode) MarshalGQL(w io.Writer) {
|
||||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
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()))
|
||||||
|
}
|
||||||
|
@ -97,7 +97,6 @@ type Project {
|
|||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
name: String!
|
name: String!
|
||||||
team: Team!
|
team: Team!
|
||||||
owner: Member!
|
|
||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
labels: [ProjectLabel!]!
|
labels: [ProjectLabel!]!
|
||||||
@ -157,6 +156,26 @@ type TaskChecklist {
|
|||||||
items: [TaskChecklistItem!]!
|
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 {
|
type Query {
|
||||||
organizations: [Organization!]!
|
organizations: [Organization!]!
|
||||||
users: [UserAccount!]!
|
users: [UserAccount!]!
|
||||||
@ -168,11 +187,27 @@ type Query {
|
|||||||
teams: [Team!]!
|
teams: [Team!]!
|
||||||
labelColors: [LabelColor!]!
|
labelColors: [LabelColor!]!
|
||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
me: UserAccount!
|
me: MePayload!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation
|
type Mutation
|
||||||
|
|
||||||
|
type TeamRole {
|
||||||
|
teamID: UUID!
|
||||||
|
roleCode: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectRole {
|
||||||
|
projectID: UUID!
|
||||||
|
roleCode: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MePayload {
|
||||||
|
user: UserAccount!
|
||||||
|
teamRoles: [TeamRole!]!
|
||||||
|
projectRoles: [ProjectRole!]!
|
||||||
|
}
|
||||||
|
|
||||||
input ProjectsFilter {
|
input ProjectsFilter {
|
||||||
teamID: UUID
|
teamID: UUID
|
||||||
}
|
}
|
||||||
@ -182,7 +217,7 @@ input FindUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input FindProject {
|
input FindProject {
|
||||||
projectId: String!
|
projectID: UUID!
|
||||||
}
|
}
|
||||||
|
|
||||||
input FindTask {
|
input FindTask {
|
||||||
@ -194,9 +229,11 @@ input FindTeam {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createProject(input: NewProject!): Project!
|
createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
deleteProject(input: DeleteProject!): DeleteProjectPayload!
|
deleteProject(input: DeleteProject!):
|
||||||
updateProjectName(input: UpdateProjectName): Project!
|
DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
updateProjectName(input: UpdateProjectName):
|
||||||
|
Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
}
|
}
|
||||||
|
|
||||||
input NewProject {
|
input NewProject {
|
||||||
@ -221,11 +258,16 @@ type DeleteProjectPayload {
|
|||||||
|
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createProjectLabel(input: NewProjectLabel!): ProjectLabel!
|
createProjectLabel(input: NewProjectLabel!):
|
||||||
deleteProjectLabel(input: DeleteProjectLabel!): ProjectLabel!
|
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateProjectLabel(input: UpdateProjectLabel!): ProjectLabel!
|
deleteProjectLabel(input: DeleteProjectLabel!):
|
||||||
updateProjectLabelName(input: UpdateProjectLabelName!): ProjectLabel!
|
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateProjectLabelColor(input: UpdateProjectLabelColor!): ProjectLabel!
|
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 {
|
input NewProjectLabel {
|
||||||
@ -255,10 +297,12 @@ input UpdateProjectLabelColor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createProjectMember(input: CreateProjectMember!): CreateProjectMemberPayload!
|
createProjectMember(input: CreateProjectMember!):
|
||||||
deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload!
|
CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload!
|
deleteProjectMember(input: DeleteProjectMember!):
|
||||||
setProjectOwner(input: SetProjectOwner!): SetProjectOwnerPayload!
|
DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
updateProjectMemberRole(input: UpdateProjectMemberRole!):
|
||||||
|
UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateProjectMember {
|
input CreateProjectMember {
|
||||||
@ -293,16 +337,6 @@ type UpdateProjectMemberRolePayload {
|
|||||||
member: Member!
|
member: Member!
|
||||||
}
|
}
|
||||||
|
|
||||||
input SetProjectOwner {
|
|
||||||
projectID: UUID!
|
|
||||||
ownerID: UUID!
|
|
||||||
}
|
|
||||||
type SetProjectOwnerPayload {
|
|
||||||
ok: Boolean!
|
|
||||||
prevOwner: Member!
|
|
||||||
newOwner: Member!
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createTask(input: NewTask!): Task!
|
createTask(input: NewTask!): Task!
|
||||||
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
|
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
|
||||||
@ -506,8 +540,10 @@ extend type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
deleteTeam(input: DeleteTeam!): DeleteTeamPayload!
|
deleteTeam(input: DeleteTeam!):
|
||||||
createTeam(input: NewTeam!): Team!
|
DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM)
|
||||||
|
createTeam(input: NewTeam!):
|
||||||
|
Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
}
|
}
|
||||||
|
|
||||||
input NewTeam {
|
input NewTeam {
|
||||||
@ -526,10 +562,11 @@ type DeleteTeamPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
setTeamOwner(input: SetTeamOwner!): SetTeamOwnerPayload!
|
createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload!
|
updateTeamMemberRole(input: UpdateTeamMemberRole!):
|
||||||
updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload!
|
UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload!
|
deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input DeleteTeamMember {
|
input DeleteTeamMember {
|
||||||
@ -562,29 +599,23 @@ input UpdateTeamMemberRole {
|
|||||||
|
|
||||||
type UpdateTeamMemberRolePayload {
|
type UpdateTeamMemberRolePayload {
|
||||||
ok: Boolean!
|
ok: Boolean!
|
||||||
member: Member!
|
|
||||||
}
|
|
||||||
|
|
||||||
input SetTeamOwner {
|
|
||||||
teamID: UUID!
|
teamID: UUID!
|
||||||
userID: UUID!
|
member: Member!
|
||||||
}
|
|
||||||
|
|
||||||
type SetTeamOwnerPayload {
|
|
||||||
ok: Boolean!
|
|
||||||
prevOwner: Member!
|
|
||||||
newOwner: Member!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
||||||
createUserAccount(input: NewUserAccount!): UserAccount!
|
createUserAccount(input: NewUserAccount!):
|
||||||
deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
|
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
deleteUserAccount(input: DeleteUserAccount!):
|
||||||
|
DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
|
||||||
logoutUser(input: LogoutUser!): Boolean!
|
logoutUser(input: LogoutUser!): Boolean!
|
||||||
clearProfileAvatar: UserAccount!
|
clearProfileAvatar: UserAccount!
|
||||||
|
|
||||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||||
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
|
updateUserRole(input: UpdateUserRole!):
|
||||||
|
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateUserPassword {
|
input UpdateUserPassword {
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/jordanknott/taskcafe/internal/auth"
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"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) {
|
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*db.Project, error) {
|
||||||
createdAt := time.Now().UTC()
|
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
|
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) {
|
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)
|
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("get user account")
|
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
|
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) {
|
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
|
||||||
taskGroupID, err := uuid.Parse(input.TaskGroupID)
|
taskGroupID, err := uuid.Parse(input.TaskGroupID)
|
||||||
createdAt := time.Now().UTC()
|
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) {
|
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
|
||||||
userID, ok := GetUserID(ctx)
|
_, role, ok := GetUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &db.Team{}, fmt.Errorf("internal server error")
|
return &db.Team{}, nil
|
||||||
}
|
}
|
||||||
|
if role == auth.RoleAdmin {
|
||||||
createdAt := time.Now().UTC()
|
createdAt := time.Now().UTC()
|
||||||
team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name, Owner: userID})
|
team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
|
||||||
return &team, err
|
return &team, err
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mutationResolver) SetTeamOwner(ctx context.Context, input SetTeamOwner) (*SetTeamOwnerPayload, error) {
|
|
||||||
team, err := r.Repository.GetTeamByID(ctx, input.TeamID)
|
|
||||||
if team.Owner == input.UserID {
|
|
||||||
return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
|
|
||||||
}
|
}
|
||||||
_, err = r.Repository.SetTeamOwner(ctx, db.SetTeamOwnerParams{Owner: input.UserID, TeamID: input.TeamID})
|
return &db.Team{}, &gqlerror.Error{
|
||||||
if err != nil {
|
Message: "You must be an organization admin to create new teams",
|
||||||
return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
|
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) {
|
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) {
|
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)
|
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("get user account")
|
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,
|
member := Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
|
||||||
Role: &db.Role{Code: role.Code, Name: role.Name},
|
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) {
|
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})
|
err := r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{TeamID: input.TeamID, UserID: input.UserID})
|
||||||
if err != nil {
|
return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*db.RefreshToken, error) {
|
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) {
|
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()
|
createdAt := time.Now().UTC()
|
||||||
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
|
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
|
||||||
if err != nil {
|
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) {
|
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)
|
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &DeleteUserAccountPayload{Ok: false}, err
|
return &DeleteUserAccountPayload{Ok: false}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var newOwnerID uuid.UUID
|
// TODO(jordanknott) migrate admin ownership
|
||||||
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})
|
|
||||||
}
|
|
||||||
err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
|
err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &DeleteUserAccountPayload{Ok: false}, err
|
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) {
|
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})
|
user, err := r.Repository.UpdateUserRole(ctx, db.UpdateUserRoleParams{RoleCode: input.RoleCode.String(), UserID: input.UserID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &UpdateUserRolePayload{}, err
|
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) {
|
func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team, error) {
|
||||||
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
|
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 {
|
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
|
return &team, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectResolver) TaskGroups(ctx context.Context, obj *db.Project) ([]db.TaskGroup, error) {
|
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) {
|
func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Member, error) {
|
||||||
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
|
|
||||||
members := []Member{}
|
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)
|
projectMembers, err := r.Repository.GetProjectMembersForProjectID(ctx, obj.ProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("get project members for project id")
|
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 {
|
for _, projectMember := range projectMembers {
|
||||||
user, err = r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
|
user, err := r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("get user account by ID")
|
log.WithError(err).Error("get user account by ID")
|
||||||
return members, err
|
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) {
|
func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db.Project, error) {
|
||||||
projectID, err := uuid.Parse(input.ProjectID)
|
userID, role, ok := GetUser(ctx)
|
||||||
if err != nil {
|
log.WithFields(log.Fields{"userID": userID, "role": role}).Info("find project user")
|
||||||
return &db.Project{}, err
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return &db.Project{}, &gqlerror.Error{
|
return &db.Project{}, &gqlerror.Error{
|
||||||
Message: "Project not found",
|
Message: "Project not found",
|
||||||
@ -977,7 +831,26 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if role == auth.RoleAdmin {
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
projectRoles, err := GetProjectRoles(ctx, r.Repository, input.ProjectID)
|
||||||
|
log.WithFields(log.Fields{"projectID": input.ProjectID, "teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("get project roles ")
|
||||||
|
if err != nil {
|
||||||
return &project, err
|
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) {
|
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) {
|
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 {
|
if input != nil {
|
||||||
return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
|
return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if orgRole == "admin" {
|
||||||
|
log.Info("showing all projects for admin")
|
||||||
return r.Repository.GetAllProjects(ctx)
|
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) {
|
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) {
|
func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||||
|
userID, orgRole, ok := GetUser(ctx)
|
||||||
|
if !ok {
|
||||||
|
log.Error("userID or orgRole does not exist!")
|
||||||
|
return []db.Team{}, errors.New("internal error")
|
||||||
|
}
|
||||||
|
if orgRole == "admin" {
|
||||||
return r.Repository.GetAllTeams(ctx)
|
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) {
|
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)
|
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)
|
userID, ok := GetUserID(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &db.UserAccount{}, fmt.Errorf("internal server error")
|
return &MePayload{}, fmt.Errorf("internal server error")
|
||||||
}
|
}
|
||||||
user, err := r.Repository.GetUserAccountByID(ctx, userID)
|
user, err := r.Repository.GetUserAccountByID(ctx, userID)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
log.WithFields(log.Fields{"userID": userID}).Warning("can not find user for me query")
|
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 {
|
} 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) {
|
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) {
|
func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) {
|
||||||
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
|
|
||||||
members := []Member{}
|
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)
|
teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("get project members for project id")
|
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 {
|
for _, teamMember := range teamMembers {
|
||||||
user, err = r.Repository.GetUserAccountByID(ctx, teamMember.UserID)
|
user, err := r.Repository.GetUserAccountByID(ctx, teamMember.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("get user account by ID")
|
log.WithError(err).Error("get user account by ID")
|
||||||
return members, err
|
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) {
|
func (r *userAccountResolver) Owned(ctx context.Context, obj *db.UserAccount) (*OwnedList, error) {
|
||||||
ownedTeams, err := r.Repository.GetOwnedTeamsForUserID(ctx, obj.UserID)
|
return &OwnedList{}, nil // TODO(jordanknott)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userAccountResolver) Member(ctx context.Context, obj *db.UserAccount) (*MemberList, error) {
|
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} }
|
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
|
||||||
|
|
||||||
// TaskChecklistItem returns TaskChecklistItemResolver implementation.
|
// TaskChecklistItem returns TaskChecklistItemResolver implementation.
|
||||||
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver { return &taskChecklistItemResolver{r} }
|
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver {
|
||||||
|
return &taskChecklistItemResolver{r}
|
||||||
|
}
|
||||||
|
|
||||||
// TaskGroup returns TaskGroupResolver implementation.
|
// TaskGroup returns TaskGroupResolver implementation.
|
||||||
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
|
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
|
||||||
|
@ -97,7 +97,6 @@ type Project {
|
|||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
name: String!
|
name: String!
|
||||||
team: Team!
|
team: Team!
|
||||||
owner: Member!
|
|
||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
labels: [ProjectLabel!]!
|
labels: [ProjectLabel!]!
|
||||||
|
@ -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 {
|
type Query {
|
||||||
organizations: [Organization!]!
|
organizations: [Organization!]!
|
||||||
users: [UserAccount!]!
|
users: [UserAccount!]!
|
||||||
@ -9,11 +29,27 @@ type Query {
|
|||||||
teams: [Team!]!
|
teams: [Team!]!
|
||||||
labelColors: [LabelColor!]!
|
labelColors: [LabelColor!]!
|
||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
me: UserAccount!
|
me: MePayload!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation
|
type Mutation
|
||||||
|
|
||||||
|
type TeamRole {
|
||||||
|
teamID: UUID!
|
||||||
|
roleCode: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectRole {
|
||||||
|
projectID: UUID!
|
||||||
|
roleCode: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MePayload {
|
||||||
|
user: UserAccount!
|
||||||
|
teamRoles: [TeamRole!]!
|
||||||
|
projectRoles: [ProjectRole!]!
|
||||||
|
}
|
||||||
|
|
||||||
input ProjectsFilter {
|
input ProjectsFilter {
|
||||||
teamID: UUID
|
teamID: UUID
|
||||||
}
|
}
|
||||||
@ -23,7 +59,7 @@ input FindUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input FindProject {
|
input FindProject {
|
||||||
projectId: String!
|
projectID: UUID!
|
||||||
}
|
}
|
||||||
|
|
||||||
input FindTask {
|
input FindTask {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createProject(input: NewProject!): Project!
|
createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
deleteProject(input: DeleteProject!): DeleteProjectPayload!
|
deleteProject(input: DeleteProject!):
|
||||||
updateProjectName(input: UpdateProjectName): Project!
|
DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
updateProjectName(input: UpdateProjectName):
|
||||||
|
Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
}
|
}
|
||||||
|
|
||||||
input NewProject {
|
input NewProject {
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createProjectLabel(input: NewProjectLabel!): ProjectLabel!
|
createProjectLabel(input: NewProjectLabel!):
|
||||||
deleteProjectLabel(input: DeleteProjectLabel!): ProjectLabel!
|
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateProjectLabel(input: UpdateProjectLabel!): ProjectLabel!
|
deleteProjectLabel(input: DeleteProjectLabel!):
|
||||||
updateProjectLabelName(input: UpdateProjectLabelName!): ProjectLabel!
|
ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateProjectLabelColor(input: UpdateProjectLabelColor!): ProjectLabel!
|
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 {
|
input NewProjectLabel {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createProjectMember(input: CreateProjectMember!): CreateProjectMemberPayload!
|
createProjectMember(input: CreateProjectMember!):
|
||||||
deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload!
|
CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload!
|
deleteProjectMember(input: DeleteProjectMember!):
|
||||||
setProjectOwner(input: SetProjectOwner!): SetProjectOwnerPayload!
|
DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
updateProjectMemberRole(input: UpdateProjectMemberRole!):
|
||||||
|
UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateProjectMember {
|
input CreateProjectMember {
|
||||||
@ -36,13 +38,3 @@ type UpdateProjectMemberRolePayload {
|
|||||||
ok: Boolean!
|
ok: Boolean!
|
||||||
member: Member!
|
member: Member!
|
||||||
}
|
}
|
||||||
|
|
||||||
input SetProjectOwner {
|
|
||||||
projectID: UUID!
|
|
||||||
ownerID: UUID!
|
|
||||||
}
|
|
||||||
type SetProjectOwnerPayload {
|
|
||||||
ok: Boolean!
|
|
||||||
prevOwner: Member!
|
|
||||||
newOwner: Member!
|
|
||||||
}
|
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createTask(input: NewTask!): Task!
|
createTask(input: NewTask!):
|
||||||
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
deleteTask(input: DeleteTaskInput!):
|
||||||
|
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
|
||||||
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
|
updateTaskDescription(input: UpdateTaskDescriptionInput!):
|
||||||
updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload!
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateTaskName(input: UpdateTaskName!): Task!
|
updateTaskLocation(input: NewTaskLocation!):
|
||||||
setTaskComplete(input: SetTaskComplete!): Task!
|
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateTaskDueDate(input: UpdateTaskDueDate!): Task!
|
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!
|
assignTask(input: AssignTaskInput):
|
||||||
unassignTask(input: UnassignTaskInput): Task!
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
unassignTask(input: UnassignTaskInput):
|
||||||
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
}
|
}
|
||||||
|
|
||||||
input NewTask {
|
input NewTask {
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createTaskChecklist(input: CreateTaskChecklist!): TaskChecklist!
|
createTaskChecklist(input: CreateTaskChecklist!):
|
||||||
deleteTaskChecklist(input: DeleteTaskChecklist!): DeleteTaskChecklistPayload!
|
TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateTaskChecklistName(input: UpdateTaskChecklistName!): TaskChecklist!
|
deleteTaskChecklist(input: DeleteTaskChecklist!):
|
||||||
createTaskChecklistItem(input: CreateTaskChecklistItem!): TaskChecklistItem!
|
DeleteTaskChecklistPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem!
|
updateTaskChecklistName(input: UpdateTaskChecklistName!):
|
||||||
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem!
|
TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload!
|
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 {
|
input UpdateTaskChecklistItemLocation {
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createTaskGroup(input: NewTaskGroup!): TaskGroup!
|
createTaskGroup(input: NewTaskGroup!):
|
||||||
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
|
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
updateTaskGroupName(input: UpdateTaskGroupName!): TaskGroup!
|
updateTaskGroupLocation(input: NewTaskGroupLocation!):
|
||||||
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
|
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 {
|
input NewTaskGroupLocation {
|
||||||
|
@ -16,7 +16,11 @@ type ToggleTaskLabelPayload {
|
|||||||
task: Task!
|
task: Task!
|
||||||
}
|
}
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
addTaskLabel(input: AddTaskLabelInput): Task!
|
addTaskLabel(input: AddTaskLabelInput):
|
||||||
removeTaskLabel(input: RemoveTaskLabelInput): Task!
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
toggleTaskLabel(input: ToggleTaskLabelInput!): ToggleTaskLabelPayload!
|
removeTaskLabel(input: RemoveTaskLabelInput):
|
||||||
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
toggleTaskLabel(input: ToggleTaskLabelInput!):
|
||||||
|
ToggleTaskLabelPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
deleteTeam(input: DeleteTeam!): DeleteTeamPayload!
|
deleteTeam(input: DeleteTeam!):
|
||||||
createTeam(input: NewTeam!): Team!
|
DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM)
|
||||||
|
createTeam(input: NewTeam!):
|
||||||
|
Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
}
|
}
|
||||||
|
|
||||||
input NewTeam {
|
input NewTeam {
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
setTeamOwner(input: SetTeamOwner!): SetTeamOwnerPayload!
|
createTeamMember(input: CreateTeamMember!):
|
||||||
createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload!
|
CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload!
|
updateTeamMemberRole(input: UpdateTeamMemberRole!):
|
||||||
deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload!
|
UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
|
deleteTeamMember(input: DeleteTeamMember!):
|
||||||
|
DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input DeleteTeamMember {
|
input DeleteTeamMember {
|
||||||
@ -35,16 +38,6 @@ input UpdateTeamMemberRole {
|
|||||||
|
|
||||||
type UpdateTeamMemberRolePayload {
|
type UpdateTeamMemberRolePayload {
|
||||||
ok: Boolean!
|
ok: Boolean!
|
||||||
|
teamID: UUID!
|
||||||
member: Member!
|
member: Member!
|
||||||
}
|
}
|
||||||
|
|
||||||
input SetTeamOwner {
|
|
||||||
teamID: UUID!
|
|
||||||
userID: UUID!
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetTeamOwnerPayload {
|
|
||||||
ok: Boolean!
|
|
||||||
prevOwner: Member!
|
|
||||||
newOwner: Member!
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
||||||
createUserAccount(input: NewUserAccount!): UserAccount!
|
createUserAccount(input: NewUserAccount!):
|
||||||
deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
|
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
deleteUserAccount(input: DeleteUserAccount!):
|
||||||
|
DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
|
||||||
logoutUser(input: LogoutUser!): Boolean!
|
logoutUser(input: LogoutUser!): Boolean!
|
||||||
clearProfileAvatar: UserAccount!
|
clearProfileAvatar: UserAccount!
|
||||||
|
|
||||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||||
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
|
updateUserRole(input: UpdateUserRole!):
|
||||||
|
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateUserPassword {
|
input UpdateUserPassword {
|
||||||
|
@ -62,7 +62,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
|||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly)
|
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -100,6 +100,13 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
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()
|
refreshCreatedAt := time.Now().UTC()
|
||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{token.UserID, refreshCreatedAt, refreshExpiresAt})
|
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)
|
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 {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -175,7 +182,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
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 {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -235,7 +242,7 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
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 {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
ctx := context.WithValue(r.Context(), "userID", userID)
|
ctx := context.WithValue(r.Context(), "userID", userID)
|
||||||
ctx = context.WithValue(ctx, "restricted_mode", accessClaims.Restricted)
|
ctx = context.WithValue(ctx, "restricted_mode", accessClaims.Restricted)
|
||||||
|
ctx = context.WithValue(ctx, "org_role", accessClaims.OrgRole)
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
|
@ -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;
|
@ -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;
|
Loading…
Reference in New Issue
Block a user