feat: redesign project sharing & initial registration
redesigned the project sharing popup to be a multi select dropdown that populates the options by using the input as a fuzzy search filter on the current users & invited users. users can now also be directly invited by email from the project share window. if invited this way, then the user will receive an email that sends them to a registration page, then a confirmation page. the initial registration was always redone so that it uses a similar system to the above in that it now will accept the first registered user if there are no other accounts (besides 'system').
This commit is contained in:
parent
6c7203a4aa
commit
7b6624ecc3
47
conf/air.toml
Normal file
47
conf/air.toml
Normal file
@ -0,0 +1,47 @@
|
||||
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
|
||||
|
||||
# Working directory
|
||||
# . or absolute path, please note that the directories following must be under root.
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
# Just plain old shell command. You could use `make` as well.
|
||||
cmd = "go build -o ./dist/taskcafe cmd/taskcafe/main.go"
|
||||
# Binary file yields from `cmd`.
|
||||
bin = "dist/taskcafe"
|
||||
# Customize binary.
|
||||
full_bin = "./dist/taskcafe web"
|
||||
# Watch these filename extensions.
|
||||
include_ext = ["go"]
|
||||
# Ignore these filename extensions or directories.
|
||||
exclude_dir = ["dist", "frontend"]
|
||||
# Watch these directories if you specified.
|
||||
include_dir = []
|
||||
# Exclude files.
|
||||
exclude_file = []
|
||||
# This log file places in your tmp_dir.
|
||||
log = "air.log"
|
||||
# It's not necessary to trigger build each time file changes if it's too frequent.
|
||||
delay = 1000 # ms
|
||||
# Stop running old binary when build errors occur.
|
||||
stop_on_error = true
|
||||
# Send Interrupt signal before killing process (windows does not support this feature)
|
||||
send_interrupt = false
|
||||
# Delay after sending Interrupt signal
|
||||
kill_delay = 500 # ms
|
||||
|
||||
[log]
|
||||
# Show log time
|
||||
time = false
|
||||
|
||||
[color]
|
||||
# Customize each part's color. If no color found, use the raw app log.
|
||||
main = "magenta"
|
||||
watcher = "cyan"
|
||||
build = "yellow"
|
||||
runner = "green"
|
||||
|
||||
[misc]
|
||||
# Delete tmp directory on exit
|
||||
clean_on_exit = true
|
@ -12,7 +12,7 @@ services:
|
||||
volumes:
|
||||
- taskcafe-postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
- 8855:5432
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
restart: always
|
||||
|
@ -31,7 +31,9 @@
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
|
||||
"no-case-declarations": "off",
|
||||
"no-plusplus": "off",
|
||||
"react/prop-types": 0,
|
||||
"no-continue": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"no-param-reassign": "off",
|
||||
"import/extensions": [
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/query-string": "^6.3.0",
|
||||
"@types/react": "^16.9.21",
|
||||
"@types/react-beautiful-dnd": "^12.1.1",
|
||||
"@types/react-datepicker": "^2.11.0",
|
||||
@ -39,8 +40,9 @@
|
||||
"history": "^4.10.1",
|
||||
"immer": "^6.0.3",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.20",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^6.13.7",
|
||||
"react": "^16.12.0",
|
||||
"react-autosize-textarea": "^7.0.0",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
|
@ -5,6 +5,7 @@ import GlobalTopNavbar from 'App/TopNavbar';
|
||||
import {
|
||||
useUsersQuery,
|
||||
useDeleteUserAccountMutation,
|
||||
useDeleteInvitedUserAccountMutation,
|
||||
useCreateUserAccountMutation,
|
||||
UsersDocument,
|
||||
UsersQuery,
|
||||
@ -176,6 +177,17 @@ const AdminRoute = () => {
|
||||
const { loading, data } = useUsersQuery();
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const { user } = useCurrentUser();
|
||||
const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.invitedUsers = cache.invitedUsers.filter(
|
||||
u => u.id !== response.data.deleteInvitedUserAccount.invitedUser.id,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
const [deleteUser] = useDeleteUserAccountMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||
@ -215,11 +227,16 @@ const AdminRoute = () => {
|
||||
<Admin
|
||||
initialTab={0}
|
||||
users={data.users}
|
||||
invitedUsers={data.invitedUsers}
|
||||
canInviteUser={user.roles.org === 'admin'}
|
||||
onInviteUser={NOOP}
|
||||
onUpdateUserPassword={() => {
|
||||
hidePopup();
|
||||
}}
|
||||
onDeleteInvitedUser={invitedUserID => {
|
||||
deleteInvitedUser({ variables: { invitedUserID } });
|
||||
hidePopup();
|
||||
}}
|
||||
onDeleteUser={(userID, newOwnerID) => {
|
||||
deleteUser({ variables: { userID, newOwnerID } });
|
||||
hidePopup();
|
||||
|
@ -1,16 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Switch, Route, useHistory } from 'react-router-dom';
|
||||
import * as H from 'history';
|
||||
|
||||
import Dashboard from 'Dashboard';
|
||||
import Admin from 'Admin';
|
||||
import Confirm from 'Confirm';
|
||||
import Projects from 'Projects';
|
||||
import Project from 'Projects/Project';
|
||||
import Teams from 'Teams';
|
||||
import Login from 'Auth';
|
||||
import Install from 'Install';
|
||||
import Register from 'Register';
|
||||
import Profile from 'Profile';
|
||||
import styled from 'styled-components';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
|
||||
const MainContent = styled.div`
|
||||
padding: 0 0 0 0;
|
||||
@ -21,6 +25,50 @@ const MainContent = styled.div`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const AuthorizedRoutes = () => {
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { setUser } = useCurrentUser();
|
||||
useEffect(() => {
|
||||
fetch('/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 400) {
|
||||
history.replace('/login');
|
||||
} else {
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken, setup } = response;
|
||||
if (setup) {
|
||||
history.replace(`/register?confirmToken=${setup.confirmToken}`);
|
||||
} else {
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(accessToken);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
return loading ? null : (
|
||||
<Switch>
|
||||
<MainContent>
|
||||
<Route exact path="/" component={Dashboard} />
|
||||
<Route exact path="/projects" component={Projects} />
|
||||
<Route path="/projects/:projectID" component={Project} />
|
||||
<Route path="/teams/:teamID" component={Teams} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
<Route path="/admin" component={Admin} />
|
||||
</MainContent>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
type RoutesProps = {
|
||||
history: H.History;
|
||||
};
|
||||
@ -28,15 +76,9 @@ type RoutesProps = {
|
||||
const Routes: React.FC<RoutesProps> = () => (
|
||||
<Switch>
|
||||
<Route exact path="/login" component={Login} />
|
||||
<Route exact path="/install" component={Install} />
|
||||
<MainContent>
|
||||
<Route exact path="/" component={Dashboard} />
|
||||
<Route exact path="/projects" component={Projects} />
|
||||
<Route path="/projects/:projectID" component={Project} />
|
||||
<Route path="/teams/:teamID" component={Teams} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
<Route path="/admin" component={Admin} />
|
||||
</MainContent>
|
||||
<Route exact path="/register" component={Register} />
|
||||
<Route exact path="/confirm" component={Confirm} />
|
||||
<AuthorizedRoutes />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
|
@ -230,10 +230,12 @@ type GlobalTopNavbarProps = {
|
||||
menuType?: Array<MenuItem>;
|
||||
onChangeRole?: (userID: string, roleCode: RoleCode) => void;
|
||||
projectMembers?: null | Array<TaskUser>;
|
||||
projectInvitedMembers?: null | Array<InvitedUser>;
|
||||
onSaveProjectName?: (projectName: string) => void;
|
||||
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onSetTab?: (tab: number) => void;
|
||||
onRemoveFromBoard?: (userID: string) => void;
|
||||
onRemoveInvitedFromBoard?: (email: string) => void;
|
||||
};
|
||||
|
||||
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
@ -246,8 +248,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
name,
|
||||
popupContent,
|
||||
projectMembers,
|
||||
projectInvitedMembers,
|
||||
onInviteUser,
|
||||
onSaveProjectName,
|
||||
onRemoveInvitedFromBoard,
|
||||
onRemoveFromBoard,
|
||||
}) => {
|
||||
const { user, setUserRoles, setUser } = useCurrentUser();
|
||||
@ -333,6 +337,34 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
return null;
|
||||
}
|
||||
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
|
||||
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
|
||||
const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
|
||||
if (member) {
|
||||
showPopup(
|
||||
$targetRef,
|
||||
<MiniProfile
|
||||
onRemoveFromBoard={() => {
|
||||
if (onRemoveInvitedFromBoard) {
|
||||
onRemoveInvitedFromBoard(member.email);
|
||||
}
|
||||
}}
|
||||
invited
|
||||
user={{
|
||||
id: member.email,
|
||||
fullName: member.email,
|
||||
bio: 'Invited',
|
||||
profileIcon: {
|
||||
bgColor: '#000',
|
||||
url: null,
|
||||
initials: member.email.charAt(0),
|
||||
},
|
||||
}}
|
||||
bio=""
|
||||
/>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
||||
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
|
||||
const warning =
|
||||
@ -382,6 +414,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
canEditProjectName={userIsTeamOrProjectAdmin}
|
||||
canInviteUser={userIsTeamOrProjectAdmin}
|
||||
onMemberProfile={onMemberProfile}
|
||||
onInvitedMemberProfile={onInvitedMemberProfile}
|
||||
onInviteUser={onInviteUser}
|
||||
onChangeRole={onChangeRole}
|
||||
onChangeProjectOwner={onChangeProjectOwner}
|
||||
@ -392,6 +425,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
history.push('/');
|
||||
}}
|
||||
projectMembers={projectMembers}
|
||||
projectInvitedMembers={projectInvitedMembers}
|
||||
onProfileClick={onProfileClick}
|
||||
onSaveName={onSaveProjectName}
|
||||
onOpenSettings={onOpenSettings}
|
||||
|
@ -52,7 +52,6 @@ type RefreshTokenResponse = {
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
||||
const setUserRoles = (roles: CurrentUserRoles) => {
|
||||
if (user) {
|
||||
@ -63,32 +62,6 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 400) {
|
||||
history.replace('/login');
|
||||
} else {
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken, isInstalled } = response;
|
||||
const claims: JWTToken = jwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(accessToken);
|
||||
if (!isInstalled) {
|
||||
history.replace('/install');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
|
||||
@ -97,13 +70,7 @@ const App = () => {
|
||||
<BaseStyles />
|
||||
<Router history={history}>
|
||||
<PopupProvider>
|
||||
{loading ? (
|
||||
<div>loading</div>
|
||||
) : (
|
||||
<>
|
||||
<Routes history={history} />
|
||||
</>
|
||||
)}
|
||||
<Routes history={history} />
|
||||
</PopupProvider>
|
||||
</Router>
|
||||
<StyledContainer
|
||||
|
@ -52,7 +52,20 @@ const Auth = () => {
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 200) {
|
||||
history.replace('/projects');
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken, setup } = response;
|
||||
if (setup) {
|
||||
history.replace(`/register?confirmToken=${setup.confirmToken}`);
|
||||
} else {
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(accessToken);
|
||||
history.replace('/projects');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
61
frontend/src/Confirm/index.tsx
Normal file
61
frontend/src/Confirm/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import Confirm from 'shared/components/Confirm';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import * as QueryString from 'query-string';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
|
||||
const UsersConfirm = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [registered, setRegistered] = useState(false);
|
||||
const params = QueryString.parse(location.search);
|
||||
const { setUser } = useCurrentUser();
|
||||
return (
|
||||
<Container>
|
||||
<LoginWrapper>
|
||||
<Confirm
|
||||
hasConfirmToken={params.confirmToken !== undefined}
|
||||
onConfirmUser={setFailed => {
|
||||
fetch('/auth/confirm', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
confirmToken: params.confirmToken,
|
||||
}),
|
||||
})
|
||||
.then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 200) {
|
||||
const response = await x.json();
|
||||
const { accessToken } = response;
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: {
|
||||
org: claims.orgRole,
|
||||
teams: new Map<string, string>(),
|
||||
projects: new Map<string, string>(),
|
||||
},
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(accessToken);
|
||||
history.push('/');
|
||||
} else {
|
||||
setFailed();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setFailed();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</LoginWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersConfirm;
|
@ -1,88 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import Register from 'shared/components/Register';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getAccessToken, setAccessToken } from 'shared/utils/accessToken';
|
||||
import UserContext from 'App/context';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
|
||||
const Install = () => {
|
||||
const history = useHistory();
|
||||
const { setUser } = useContext(UserContext);
|
||||
useEffect(() => {
|
||||
fetch('/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { isInstalled } = response;
|
||||
if (status === 200 && isInstalled) {
|
||||
history.replace('/projects');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Container>
|
||||
<LoginWrapper>
|
||||
<Register
|
||||
onSubmit={(data, setComplete, setError) => {
|
||||
const accessToken = getAccessToken();
|
||||
if (data.password !== data.password_confirm) {
|
||||
setError('password', { type: 'error', message: 'Passwords must match' });
|
||||
setError('password_confirm', { type: 'error', message: 'Passwords must match' });
|
||||
} else {
|
||||
axios
|
||||
.post(
|
||||
'/auth/install',
|
||||
{
|
||||
user: {
|
||||
username: data.username,
|
||||
roleCode: 'admin',
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
initials: data.initials,
|
||||
fullname: data.fullname,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 400) {
|
||||
history.replace('/login');
|
||||
} else {
|
||||
const response: RefreshTokenResponse = await x.data;
|
||||
const { accessToken: newToken, isInstalled } = response;
|
||||
const claims: JWTToken = jwtDecode(newToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: {
|
||||
org: claims.orgRole,
|
||||
teams: new Map<string, string>(),
|
||||
projects: new Map<string, string>(),
|
||||
},
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(newToken);
|
||||
if (!isInstalled) {
|
||||
history.replace('/install');
|
||||
}
|
||||
}
|
||||
history.push('/projects');
|
||||
});
|
||||
}
|
||||
setComplete(true);
|
||||
}}
|
||||
/>
|
||||
</LoginWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Install;
|
@ -3,32 +3,11 @@ import updateApolloCache from 'shared/utils/cache';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
useSetTaskCompleteMutation,
|
||||
useToggleTaskLabelMutation,
|
||||
useUpdateProjectNameMutation,
|
||||
useFindProjectQuery,
|
||||
useUpdateTaskGroupNameMutation,
|
||||
useUpdateTaskNameMutation,
|
||||
useUpdateProjectLabelMutation,
|
||||
useCreateTaskMutation,
|
||||
useDeleteProjectLabelMutation,
|
||||
useDeleteTaskMutation,
|
||||
useUpdateTaskLocationMutation,
|
||||
useUpdateTaskGroupLocationMutation,
|
||||
useCreateTaskGroupMutation,
|
||||
useDeleteTaskGroupMutation,
|
||||
useUpdateTaskDescriptionMutation,
|
||||
useAssignTaskMutation,
|
||||
DeleteTaskDocument,
|
||||
FindProjectDocument,
|
||||
useCreateProjectLabelMutation,
|
||||
useUnassignTaskMutation,
|
||||
useUpdateTaskDueDateMutation,
|
||||
FindProjectQuery,
|
||||
useUsersQuery,
|
||||
} from 'shared/generated/graphql';
|
||||
import LabelManager from 'shared/components/PopupMenu/LabelManager';
|
||||
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
||||
|
@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect, useContext } from 'react';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar';
|
||||
import styled from 'styled-components/macro';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import {
|
||||
useParams,
|
||||
@ -15,11 +16,12 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useInviteProjectMembersMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
useToggleTaskLabelMutation,
|
||||
useUpdateProjectNameMutation,
|
||||
useFindProjectQuery,
|
||||
useDeleteInvitedProjectMemberMutation,
|
||||
useUpdateTaskNameMutation,
|
||||
useCreateTaskMutation,
|
||||
useDeleteTaskMutation,
|
||||
@ -37,12 +39,20 @@ import Input from 'shared/components/Input';
|
||||
import Member from 'shared/components/Member';
|
||||
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import { Lock, Cross } from 'shared/icons';
|
||||
import Button from 'shared/components/Button';
|
||||
import { useApolloClient } from '@apollo/react-hooks';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import gql from 'graphql-tag';
|
||||
import { colourStyles } from 'shared/components/Select';
|
||||
import Board, { BoardLoading } from './Board';
|
||||
import Details from './Details';
|
||||
import LabelManagerEditor from './LabelManagerEditor';
|
||||
|
||||
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
|
||||
|
||||
const RFC2822_EMAIL = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||
|
||||
const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
|
||||
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
|
||||
|
||||
@ -70,29 +80,299 @@ const MemberList = styled.div`
|
||||
margin: 8px 0;
|
||||
`;
|
||||
|
||||
type InviteUserData = {
|
||||
email?: string;
|
||||
suerID?: string;
|
||||
};
|
||||
type UserManagementPopupProps = {
|
||||
projectID: string;
|
||||
users: Array<User>;
|
||||
projectMembers: Array<TaskUser>;
|
||||
onAddProjectMember: (userID: string) => void;
|
||||
onInviteProjectMembers: (data: Array<InviteUserData>) => void;
|
||||
};
|
||||
|
||||
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, projectMembers, onAddProjectMember }) => {
|
||||
const VisibiltyPrivateIcon = styled(Lock)`
|
||||
padding-right: 4px;
|
||||
`;
|
||||
|
||||
const VisibiltyButtonText = styled.span`
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
const ShareActions = styled.div`
|
||||
border-top: 1px solid #414561;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const VisibiltyButton = styled.button`
|
||||
cursor: pointer;
|
||||
margin: 2px 4px;
|
||||
padding: 2px 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid transparent;
|
||||
&:hover ${VisibiltyButtonText} {
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
}
|
||||
&:hover ${VisibiltyPrivateIcon} {
|
||||
fill: rgba(${props => props.theme.colors.text.secondary});
|
||||
stroke: rgba(${props => props.theme.colors.text.secondary});
|
||||
}
|
||||
&:hover {
|
||||
border-bottom: 1px solid rgba(${props => props.theme.colors.primary});
|
||||
}
|
||||
`;
|
||||
|
||||
type MemberFilterOptions = {
|
||||
projectID?: null | string;
|
||||
teamID?: null | string;
|
||||
organization?: boolean;
|
||||
};
|
||||
|
||||
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
|
||||
console.log(input.trim().length < 3);
|
||||
if (input && input.trim().length < 3) {
|
||||
return [];
|
||||
}
|
||||
const res = await client.query({
|
||||
query: gql`
|
||||
query {
|
||||
searchMembers(input: {searchFilter:"${input}", projectID:"${projectID}"}) {
|
||||
id
|
||||
similarity
|
||||
status
|
||||
user {
|
||||
id
|
||||
fullName
|
||||
email
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
bgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
let results: any = [];
|
||||
const emails: Array<string> = [];
|
||||
console.log(res.data && res.data.searchMembers);
|
||||
if (res.data && res.data.searchMembers) {
|
||||
results = [
|
||||
...res.data.searchMembers.map((m: any) => {
|
||||
if (m.status === 'INVITED') {
|
||||
console.log(`${m.id} is added`);
|
||||
return {
|
||||
label: m.id,
|
||||
value: {
|
||||
id: m.id,
|
||||
type: 2,
|
||||
profileIcon: {
|
||||
bgColor: '#ccc',
|
||||
initials: m.id.charAt(0),
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
console.log(`${m.user.email} is added`);
|
||||
emails.push(m.user.email);
|
||||
return {
|
||||
label: m.user.fullName,
|
||||
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
||||
};
|
||||
}
|
||||
}),
|
||||
];
|
||||
console.log(results);
|
||||
}
|
||||
|
||||
if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
|
||||
results = [
|
||||
...results,
|
||||
{
|
||||
label: input,
|
||||
value: {
|
||||
id: input,
|
||||
type: 1,
|
||||
profileIcon: {
|
||||
bgColor: '#ccc',
|
||||
initials: input.charAt(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
type UserOptionProps = {
|
||||
innerProps: any;
|
||||
isDisabled: boolean;
|
||||
isFocused: boolean;
|
||||
label: string;
|
||||
data: any;
|
||||
getValue: any;
|
||||
};
|
||||
|
||||
const OptionWrapper = styled.div<{ isFocused: boolean }>`
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
${props => props.isFocused && `background: rgba(${props.theme.colors.primary});`}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
const OptionContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 12px;
|
||||
`;
|
||||
|
||||
const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${p => p.fontSize}px;
|
||||
color: rgba(${p => (p.quiet ? p.theme.colors.text.primary : p.theme.colors.text.primary)});
|
||||
`;
|
||||
|
||||
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
|
||||
console.log(data);
|
||||
return !isDisabled ? (
|
||||
<OptionWrapper {...innerProps} isFocused={isFocused}>
|
||||
<TaskAssignee
|
||||
size={32}
|
||||
member={{
|
||||
id: '',
|
||||
fullName: data.value.label,
|
||||
profileIcon: data.value.profileIcon,
|
||||
}}
|
||||
/>
|
||||
<OptionContent>
|
||||
<OptionLabel fontSize={16} quiet={false}>
|
||||
{label}
|
||||
</OptionLabel>
|
||||
{data.value.type === 2 && (
|
||||
<OptionLabel fontSize={14} quiet>
|
||||
Joined
|
||||
</OptionLabel>
|
||||
)}
|
||||
</OptionContent>
|
||||
</OptionWrapper>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const OptionValueWrapper = styled.div`
|
||||
background: rgba(${props => props.theme.colors.bg.primary});
|
||||
border-radius: 4px;
|
||||
margin: 2px;
|
||||
padding: 3px 6px 3px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const OptionValueLabel = styled.span`
|
||||
font-size: 12px;
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
`;
|
||||
|
||||
const OptionValueRemove = styled.button`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 4px;
|
||||
`;
|
||||
const OptionValue = ({ data, removeProps }: any) => {
|
||||
return (
|
||||
<OptionValueWrapper>
|
||||
<OptionValueLabel>{data.label}</OptionValueLabel>
|
||||
<OptionValueRemove {...removeProps}>
|
||||
<Cross width={14} height={14} />
|
||||
</OptionValueRemove>
|
||||
</OptionValueWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const InviteButton = styled(Button)`
|
||||
margin-top: 12px;
|
||||
height: 32px;
|
||||
padding: 4px 12px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const InviteContainer = styled.div`
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({
|
||||
projectID,
|
||||
users,
|
||||
projectMembers,
|
||||
onInviteProjectMembers,
|
||||
}) => {
|
||||
const client = useApolloClient();
|
||||
const [invitedUsers, setInvitedUsers] = useState<Array<any> | null>(null);
|
||||
return (
|
||||
<Popup tab={0} title="Invite a user">
|
||||
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
|
||||
<MemberList>
|
||||
{users
|
||||
.filter(u => u.id !== projectMembers.find(p => p.id === u.id)?.id)
|
||||
.map(user => (
|
||||
<UserMember
|
||||
key={user.id}
|
||||
onCardMemberClick={() => onAddProjectMember(user.id)}
|
||||
showName
|
||||
member={user}
|
||||
taskID=""
|
||||
/>
|
||||
))}
|
||||
</MemberList>
|
||||
<InviteContainer>
|
||||
<AsyncSelect
|
||||
getOptionValue={option => option.value.id}
|
||||
placeholder="Email address or username"
|
||||
noOptionsMessage={() => null}
|
||||
onChange={(e: any) => {
|
||||
setInvitedUsers(e);
|
||||
}}
|
||||
isMulti
|
||||
autoFocus
|
||||
cacheOptions
|
||||
styles={colourStyles}
|
||||
defaultOption
|
||||
components={{
|
||||
MultiValue: OptionValue,
|
||||
Option: UserOption,
|
||||
IndicatorSeparator: null,
|
||||
DropdownIndicator: null,
|
||||
}}
|
||||
loadOptions={(i, cb) => fetchMembers(client, projectID, {}, i, cb)}
|
||||
/>
|
||||
</InviteContainer>
|
||||
<InviteButton
|
||||
onClick={() => {
|
||||
if (invitedUsers) {
|
||||
onInviteProjectMembers(
|
||||
invitedUsers.map(user => {
|
||||
if (user.value.type === 0) {
|
||||
return {
|
||||
userID: user.value.id,
|
||||
};
|
||||
}
|
||||
return {
|
||||
email: user.value.id,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={invitedUsers === null}
|
||||
hoverVariant="none"
|
||||
fontSize="16px"
|
||||
>
|
||||
Send Invite
|
||||
</InviteButton>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
@ -176,14 +456,36 @@ const Project = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const [createProjectMember] = useCreateProjectMemberMutation({
|
||||
const [inviteProjectMembers] = useInviteProjectMembersMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<FindProjectQuery>(
|
||||
client,
|
||||
FindProjectDocument,
|
||||
cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.members.push({ ...response.data.createProjectMember.member });
|
||||
draftCache.findProject.members = [
|
||||
...cache.findProject.members,
|
||||
...response.data.inviteProjectMembers.members,
|
||||
];
|
||||
draftCache.findProject.invitedMembers = [
|
||||
...cache.findProject.invitedMembers,
|
||||
...response.data.inviteProjectMembers.invitedMembers,
|
||||
];
|
||||
}),
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
const [deleteInvitedProjectMember] = useDeleteInvitedProjectMemberMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<FindProjectQuery>(
|
||||
client,
|
||||
FindProjectDocument,
|
||||
cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
|
||||
m => m.email !== response.data.deleteInvitedProjectMember.invitedMember.email,
|
||||
);
|
||||
}),
|
||||
{ projectID },
|
||||
);
|
||||
@ -243,6 +545,10 @@ const Project = () => {
|
||||
deleteProjectMember({ variables: { userID, projectID } });
|
||||
hidePopup();
|
||||
}}
|
||||
onRemoveInvitedFromBoard={email => {
|
||||
deleteInvitedProjectMember({ variables: { projectID, email } });
|
||||
hidePopup();
|
||||
}}
|
||||
onSaveProjectName={projectName => {
|
||||
updateProjectName({ variables: { projectID, name: projectName } });
|
||||
}}
|
||||
@ -250,8 +556,10 @@ const Project = () => {
|
||||
showPopup(
|
||||
$target,
|
||||
<UserManagementPopup
|
||||
onAddProjectMember={userID => {
|
||||
createProjectMember({ variables: { userID, projectID } });
|
||||
projectID={projectID}
|
||||
onInviteProjectMembers={members => {
|
||||
inviteProjectMembers({ variables: { projectID, members } });
|
||||
hidePopup();
|
||||
}}
|
||||
users={data.users}
|
||||
projectMembers={data.findProject.members}
|
||||
@ -262,6 +570,7 @@ const Project = () => {
|
||||
menuType={[{ name: 'Board', link: location.pathname }]}
|
||||
currentTab={0}
|
||||
projectMembers={data.findProject.members}
|
||||
projectInvitedMembers={data.findProject.invitedMembers}
|
||||
projectID={projectID}
|
||||
teamID={data.findProject.team ? data.findProject.team.id : null}
|
||||
name={data.findProject.name}
|
||||
|
13
frontend/src/Register/Styles.ts
Normal file
13
frontend/src/Register/Styles.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
export const LoginWrapper = styled.div`
|
||||
width: 60%;
|
||||
`;
|
62
frontend/src/Register/index.tsx
Normal file
62
frontend/src/Register/index.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import Register from 'shared/components/Register';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import * as QueryString from 'query-string';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
|
||||
const UsersRegister = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [registered, setRegistered] = useState(false);
|
||||
const params = QueryString.parse(location.search);
|
||||
return (
|
||||
<Container>
|
||||
<LoginWrapper>
|
||||
<Register
|
||||
registered={registered}
|
||||
onSubmit={(data, setComplete, setError) => {
|
||||
if (data.password !== data.password_confirm) {
|
||||
setError('password', { type: 'error', message: 'Passwords must match' });
|
||||
setError('password_confirm', { type: 'error', message: 'Passwords must match' });
|
||||
} else {
|
||||
// TODO: change to fetch?
|
||||
fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
user: {
|
||||
username: data.username,
|
||||
roleCode: 'admin',
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
initials: data.initials,
|
||||
fullname: data.fullname,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then(async x => {
|
||||
const response = await x.json();
|
||||
const { setup } = response;
|
||||
console.log(response);
|
||||
if (setup) {
|
||||
history.replace(`/confirm?confirmToken=xxxx`);
|
||||
} else if (params.confirmToken) {
|
||||
history.replace(`/confirm?confirmToken=${params.confirmToken}`);
|
||||
} else {
|
||||
setRegistered(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast('There was an issue trying to register');
|
||||
});
|
||||
}
|
||||
setComplete(true);
|
||||
}}
|
||||
/>
|
||||
</LoginWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersRegister;
|
0
frontend/src/outline.d.ts
vendored
Normal file
0
frontend/src/outline.d.ts
vendored
Normal file
@ -51,7 +51,9 @@ export const Default = () => {
|
||||
},
|
||||
},
|
||||
]}
|
||||
invitedUsers={[]}
|
||||
onAddUser={action('add user')}
|
||||
onDeleteInvitedUser={action('delete invited user')}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
|
@ -104,8 +104,8 @@ type TeamRoleManagerPopupProps = {
|
||||
user: User;
|
||||
users: Array<User>;
|
||||
warning?: string | null;
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
canChangeRole?: boolean;
|
||||
onChangeRole?: (roleCode: RoleCode) => void;
|
||||
updateUserPassword?: (user: TaskUser, password: string) => void;
|
||||
onDeleteUser?: (userID: string, newOwnerID: string | null) => void;
|
||||
};
|
||||
@ -530,8 +530,10 @@ type AdminProps = {
|
||||
onDeleteUser: (userID: string, newOwnerID: string | null) => void;
|
||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
users: Array<User>;
|
||||
invitedUsers: Array<InvitedUserAccount>;
|
||||
canInviteUser: boolean;
|
||||
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
||||
onDeleteInvitedUser: (invitedUserID: string) => void;
|
||||
};
|
||||
|
||||
const Admin: React.FC<AdminProps> = ({
|
||||
@ -540,7 +542,9 @@ const Admin: React.FC<AdminProps> = ({
|
||||
onUpdateUserPassword,
|
||||
canInviteUser,
|
||||
onDeleteUser,
|
||||
onDeleteInvitedUser,
|
||||
onInviteUser,
|
||||
invitedUsers,
|
||||
users,
|
||||
}) => {
|
||||
const warning =
|
||||
@ -577,7 +581,7 @@ const Admin: React.FC<AdminProps> = ({
|
||||
<TabContent>
|
||||
<MemberListWrapper>
|
||||
<MemberListHeader>
|
||||
<ListTitle>{`Members (${users.length})`}</ListTitle>
|
||||
<ListTitle>{`Members (${users.length + invitedUsers.length})`}</ListTitle>
|
||||
<ListDesc>
|
||||
Organization admins can create / manage / delete all projects & teams. Members only have access to teams
|
||||
or projects they have been added to.
|
||||
@ -635,6 +639,65 @@ const Admin: React.FC<AdminProps> = ({
|
||||
</MemberListItem>
|
||||
);
|
||||
})}
|
||||
{invitedUsers.map(member => {
|
||||
return (
|
||||
<MemberListItem>
|
||||
<MemberProfile
|
||||
showRoleIcons
|
||||
size={32}
|
||||
onMemberProfile={NOOP}
|
||||
member={{
|
||||
id: member.id,
|
||||
fullName: member.email,
|
||||
profileIcon: {
|
||||
bgColor: '#fff',
|
||||
url: null,
|
||||
initials: member.email.charAt(0),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<MemberListItemDetails>
|
||||
<MemberItemName>{member.email}</MemberItemName>
|
||||
<MemberItemUsername>Invited</MemberItemUsername>
|
||||
</MemberListItemDetails>
|
||||
<MemberItemOptions>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
user={{
|
||||
id: member.id,
|
||||
fullName: member.email,
|
||||
profileIcon: {
|
||||
bgColor: '#fff',
|
||||
url: null,
|
||||
initials: member.email.charAt(0),
|
||||
},
|
||||
member: {
|
||||
teams: [],
|
||||
projects: [],
|
||||
},
|
||||
owned: {
|
||||
teams: [],
|
||||
projects: [],
|
||||
},
|
||||
}}
|
||||
users={users}
|
||||
onDeleteUser={() => {
|
||||
onDeleteInvitedUser(member.id);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MemberItemOption>
|
||||
</MemberItemOptions>
|
||||
</MemberListItem>
|
||||
);
|
||||
})}
|
||||
</MemberList>
|
||||
</MemberListWrapper>
|
||||
</TabContent>
|
||||
|
@ -35,11 +35,15 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
const Filled = styled(Base)`
|
||||
const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
|
||||
background: rgba(${props => props.theme.colors[props.color]});
|
||||
&:hover {
|
||||
box-shadow: 0 8px 25px -8px rgba(${props => props.theme.colors[props.color]});
|
||||
}
|
||||
${props =>
|
||||
props.hoverVariant === 'boxShadow' &&
|
||||
css`
|
||||
&:hover {
|
||||
box-shadow: 0 8px 25px -8px rgba(${props.theme.colors[props.color]});
|
||||
}
|
||||
`}
|
||||
`;
|
||||
const Outline = styled(Base)<{ invert: boolean }>`
|
||||
border: 1px solid rgba(${props => props.theme.colors[props.color]});
|
||||
@ -123,9 +127,11 @@ const Relief = styled(Base)`
|
||||
}
|
||||
`;
|
||||
|
||||
type HoverVariant = 'boxShadow' | 'none';
|
||||
type ButtonProps = {
|
||||
fontSize?: string;
|
||||
variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief';
|
||||
hoverVariant?: HoverVariant;
|
||||
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit';
|
||||
@ -142,6 +148,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
invert = false,
|
||||
color = 'primary',
|
||||
variant = 'filled',
|
||||
hoverVariant = 'boxShadow',
|
||||
type = 'button',
|
||||
justifyTextContent = 'center',
|
||||
icon,
|
||||
@ -158,7 +165,15 @@ const Button: React.FC<ButtonProps> = ({
|
||||
switch (variant) {
|
||||
case 'filled':
|
||||
return (
|
||||
<Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
|
||||
<Filled
|
||||
ref={$button}
|
||||
hoverVariant={hoverVariant}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
color={color}
|
||||
>
|
||||
{icon && icon}
|
||||
<Text hasIcon={typeof icon !== 'undefined'} justifyTextContent={justifyTextContent} fontSize={fontSize}>
|
||||
{children}
|
||||
|
103
frontend/src/shared/components/Confirm/Styles.ts
Normal file
103
frontend/src/shared/components/Confirm/Styles.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from 'shared/components/Button';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
background: #eff2f7;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const Column = styled.div`
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const LoginFormWrapper = styled.div`
|
||||
background: #10163a;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LoginFormContainer = styled.div`
|
||||
min-height: 505px;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
export const Title = styled.h1`
|
||||
color: #ebeefd;
|
||||
font-size: 18px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
|
||||
export const SubTitle = styled.h2`
|
||||
color: #c2c6dc;
|
||||
font-size: 14px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
export const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const FormLabel = styled.label`
|
||||
color: #c2c6dc;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
margin-top: 14px;
|
||||
`;
|
||||
|
||||
export const FormTextInput = styled.input`
|
||||
width: 100%;
|
||||
background: #262c49;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
margin-top: 4px;
|
||||
padding: 0.7rem 1rem 0.7rem 3rem;
|
||||
font-size: 1rem;
|
||||
color: #c2c6dc;
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
export const FormIcon = styled.div`
|
||||
top: 30px;
|
||||
left: 16px;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
export const FormError = styled.span`
|
||||
font-size: 0.875rem;
|
||||
color: rgb(234, 84, 85);
|
||||
`;
|
||||
|
||||
export const LoginButton = styled(Button)``;
|
||||
|
||||
export const ActionButtons = styled.div`
|
||||
margin-top: 17.5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const RegisterButton = styled(Button)``;
|
||||
|
||||
export const LogoTitle = styled.div`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-left: 12px;
|
||||
transition: visibility, opacity, transform 0.25s ease;
|
||||
color: #7367f0;
|
||||
`;
|
||||
|
||||
export const LogoWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
color: rgb(222, 235, 255);
|
||||
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
|
||||
`;
|
62
frontend/src/shared/components/Confirm/index.tsx
Normal file
62
frontend/src/shared/components/Confirm/index.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import AccessAccount from 'shared/undraw/AccessAccount';
|
||||
import { User, Lock, Taskcafe } from 'shared/icons';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import LoadingSpinner from 'shared/components/LoadingSpinner';
|
||||
import {
|
||||
Form,
|
||||
LogoWrapper,
|
||||
LogoTitle,
|
||||
ActionButtons,
|
||||
RegisterButton,
|
||||
FormError,
|
||||
FormIcon,
|
||||
FormLabel,
|
||||
FormTextInput,
|
||||
Wrapper,
|
||||
Column,
|
||||
LoginFormWrapper,
|
||||
LoginFormContainer,
|
||||
Title,
|
||||
SubTitle,
|
||||
} from './Styles';
|
||||
|
||||
const Confirm = ({ onConfirmUser, hasConfirmToken }: ConfirmProps) => {
|
||||
const [hasFailed, setFailed] = useState(false);
|
||||
const setHasFailed = () => {
|
||||
setFailed(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
onConfirmUser(setHasFailed);
|
||||
});
|
||||
return (
|
||||
<Wrapper>
|
||||
<Column>
|
||||
<AccessAccount width={275} height={250} />
|
||||
</Column>
|
||||
<Column>
|
||||
<LoginFormWrapper>
|
||||
<LoginFormContainer>
|
||||
<LogoWrapper>
|
||||
<Taskcafe width={42} height={42} />
|
||||
<LogoTitle>Taskcafé</LogoTitle>
|
||||
</LogoWrapper>
|
||||
{hasConfirmToken ? (
|
||||
<>
|
||||
<Title>Confirming user...</Title>
|
||||
{hasFailed ? <SubTitle>There was an error while confirming your user</SubTitle> : <LoadingSpinner />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Title>There is no confirmation token</Title>
|
||||
<SubTitle>There seems to have been an error.</SubTitle>
|
||||
</>
|
||||
)}
|
||||
</LoginFormContainer>
|
||||
</LoginFormWrapper>
|
||||
</Column>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Confirm;
|
@ -73,7 +73,6 @@ export const HeaderName = styled(TextareaAutosize)`
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
margin: -4px 0;
|
||||
padding: 4px 8px;
|
||||
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
|
@ -47,6 +47,7 @@ const permissions = [
|
||||
type MiniProfileProps = {
|
||||
bio: string;
|
||||
user: TaskUser;
|
||||
invited?: boolean;
|
||||
onRemoveFromTask?: () => void;
|
||||
onChangeRole?: (roleCode: RoleCode) => void;
|
||||
onRemoveFromBoard?: () => void;
|
||||
@ -56,6 +57,7 @@ type MiniProfileProps = {
|
||||
const MiniProfile: React.FC<MiniProfileProps> = ({
|
||||
user,
|
||||
bio,
|
||||
invited,
|
||||
canChangeRole,
|
||||
onRemoveFromTask,
|
||||
onChangeRole,
|
||||
@ -74,7 +76,7 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
||||
)}
|
||||
<ProfileInfo>
|
||||
<InfoTitle>{user.fullName}</InfoTitle>
|
||||
<InfoUsername>{`@${user.username}`}</InfoUsername>
|
||||
{invited ? <InfoUsername>Invited</InfoUsername> : <InfoUsername>{`@${user.username}`}</InfoUsername>}
|
||||
<InfoBio>{bio}</InfoBio>
|
||||
</ProfileInfo>
|
||||
</Profile>
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
|
||||
const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
|
||||
|
||||
const Register = ({ onSubmit }: RegisterProps) => {
|
||||
const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
||||
const [isComplete, setComplete] = useState(true);
|
||||
const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>();
|
||||
const loginSubmit = (data: RegisterFormData) => {
|
||||
@ -43,103 +43,112 @@ const Register = ({ onSubmit }: RegisterProps) => {
|
||||
<Taskcafe width={42} height={42} />
|
||||
<LogoTitle>Taskcafé</LogoTitle>
|
||||
</LogoWrapper>
|
||||
<Title>Register</Title>
|
||||
<SubTitle>Please create the system admin user</SubTitle>
|
||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||
<FormLabel htmlFor="fullname">
|
||||
Full name
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="fullname"
|
||||
name="fullname"
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="username">
|
||||
Username
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="email">
|
||||
Email
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
ref={register({
|
||||
required: 'Email is required',
|
||||
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
<FormLabel htmlFor="initials">
|
||||
Initials
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="initials"
|
||||
name="initials"
|
||||
ref={register({
|
||||
required: 'Initials is required',
|
||||
pattern: {
|
||||
value: INITIALS_PATTERN,
|
||||
message: 'Initials must be between 2 to 3 characters.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||
<FormLabel htmlFor="password">
|
||||
Password
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
<FormLabel htmlFor="password_confirm">
|
||||
Password (Confirm)
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password_confirm"
|
||||
name="password_confirm"
|
||||
ref={register({ required: 'Password (confirm) is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
|
||||
{registered ? (
|
||||
<>
|
||||
<Title>Thanks for registering</Title>
|
||||
<SubTitle>Please check your inbox for a confirmation email.</SubTitle>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Title>Register</Title>
|
||||
<SubTitle>Please create your user</SubTitle>
|
||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||
<FormLabel htmlFor="fullname">
|
||||
Full name
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="fullname"
|
||||
name="fullname"
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="username">
|
||||
Username
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="email">
|
||||
Email
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
ref={register({
|
||||
required: 'Email is required',
|
||||
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
<FormLabel htmlFor="initials">
|
||||
Initials
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="initials"
|
||||
name="initials"
|
||||
ref={register({
|
||||
required: 'Initials is required',
|
||||
pattern: {
|
||||
value: INITIALS_PATTERN,
|
||||
message: 'Initials must be between 2 to 3 characters.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||
<FormLabel htmlFor="password">
|
||||
Password
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
<FormLabel htmlFor="password_confirm">
|
||||
Password (Confirm)
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password_confirm"
|
||||
name="password_confirm"
|
||||
ref={register({ required: 'Password (confirm) is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
|
||||
|
||||
<ActionButtons>
|
||||
<RegisterButton type="submit" disabled={!isComplete}>
|
||||
Register
|
||||
</RegisterButton>
|
||||
</ActionButtons>
|
||||
</Form>
|
||||
<ActionButtons>
|
||||
<RegisterButton type="submit" disabled={!isComplete}>
|
||||
Register
|
||||
</RegisterButton>
|
||||
</ActionButtons>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</LoginFormContainer>
|
||||
</LoginFormWrapper>
|
||||
</Column>
|
||||
|
@ -16,7 +16,7 @@ function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused:
|
||||
return null;
|
||||
}
|
||||
|
||||
const colourStyles = {
|
||||
export const colourStyles = {
|
||||
control: (styles: any, data: any) => {
|
||||
return {
|
||||
...styles,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { DoubleChevronUp, Crown } from 'shared/icons';
|
||||
|
||||
export const AdminIcon = styled(DoubleChevronUp)`
|
||||
@ -24,7 +24,12 @@ const TaskDetailAssignee = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
||||
export const Wrapper = styled.div<{
|
||||
size: number | string;
|
||||
bgColor: string | null;
|
||||
backgroundURL: string | null;
|
||||
hasClick: boolean;
|
||||
}>`
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
border-radius: 9999px;
|
||||
@ -37,33 +42,60 @@ export const Wrapper = styled.div<{ size: number | string; bgColor: string | nul
|
||||
background-size: contain;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
${props =>
|
||||
props.hasClick &&
|
||||
css`
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
type TaskAssigneeProps = {
|
||||
size: number | string;
|
||||
showRoleIcons?: boolean;
|
||||
member: TaskUser;
|
||||
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
invited?: boolean;
|
||||
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ showRoleIcons, member, onMemberProfile, size, className }) => {
|
||||
const TaskAssignee: React.FC<TaskAssigneeProps> = ({
|
||||
showRoleIcons,
|
||||
member,
|
||||
invited,
|
||||
onMemberProfile,
|
||||
size,
|
||||
className,
|
||||
}) => {
|
||||
const $memberRef = useRef<HTMLDivElement>(null);
|
||||
let profileIcon: ProfileIcon = {
|
||||
url: null,
|
||||
bgColor: null,
|
||||
initials: null,
|
||||
};
|
||||
if (member.profileIcon) {
|
||||
profileIcon = member.profileIcon;
|
||||
}
|
||||
return (
|
||||
<TaskDetailAssignee
|
||||
className={className}
|
||||
ref={$memberRef}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onMemberProfile($memberRef, member.id);
|
||||
if (onMemberProfile) {
|
||||
onMemberProfile($memberRef, member.id);
|
||||
}
|
||||
}}
|
||||
key={member.id}
|
||||
>
|
||||
<Wrapper backgroundURL={member.profileIcon.url ?? null} bgColor={member.profileIcon.bgColor ?? null} size={size}>
|
||||
{(!member.profileIcon.url && member.profileIcon.initials) ?? ''}
|
||||
<Wrapper
|
||||
hasClick={typeof onMemberProfile !== undefined}
|
||||
backgroundURL={profileIcon.url ?? null}
|
||||
bgColor={profileIcon.bgColor ?? null}
|
||||
size={size}
|
||||
>
|
||||
{(!profileIcon.url && profileIcon.initials) ?? ''}
|
||||
</Wrapper>
|
||||
{showRoleIcons && member.role && member.role.code === 'admin' && <AdminIcon width={10} height={10} />}
|
||||
{showRoleIcons && member.role && member.role.code === 'owner' && <OwnerIcon width={10} height={10} />}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle } from 'shared/icons';
|
||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
|
||||
import styled from 'styled-components';
|
||||
import ProfileIcon from 'shared/components/ProfileIcon';
|
||||
import { usePopup } from 'shared/components/PopupMenu';
|
||||
import { RoleCode } from 'shared/generated/graphql';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import { useHistory } from 'react-router';
|
||||
import {
|
||||
TaskcafeLogo,
|
||||
TaskcafeTitle,
|
||||
@ -173,8 +174,11 @@ type NavBarProps = {
|
||||
user: TaskUser | null;
|
||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||
projectMembers?: Array<TaskUser> | null;
|
||||
projectInvitedMembers?: Array<InvitedUser> | null;
|
||||
|
||||
onRemoveFromBoard?: (userID: string) => void;
|
||||
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
|
||||
};
|
||||
|
||||
const NavBar: React.FC<NavBarProps> = ({
|
||||
@ -184,10 +188,12 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onChangeProjectOwner,
|
||||
currentTab,
|
||||
onMemberProfile,
|
||||
onInvitedMemberProfile,
|
||||
canEditProjectName = false,
|
||||
onOpenProjectFinder,
|
||||
onFavorite,
|
||||
onSetTab,
|
||||
projectInvitedMembers,
|
||||
onChangeRole,
|
||||
name,
|
||||
onRemoveFromBoard,
|
||||
@ -204,6 +210,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onProfileClick($target);
|
||||
}
|
||||
};
|
||||
const history = useHistory();
|
||||
const { showPopup } = usePopup();
|
||||
return (
|
||||
<NavbarWrapper>
|
||||
@ -245,19 +252,38 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<TaskcafeTitle>Taskcafé</TaskcafeTitle>
|
||||
</LogoContainer>
|
||||
<GlobalActions>
|
||||
{projectMembers && onMemberProfile && (
|
||||
{projectMembers && projectInvitedMembers && onMemberProfile && onInvitedMemberProfile && (
|
||||
<>
|
||||
<ProjectMembers>
|
||||
{projectMembers.map((member, idx) => (
|
||||
<ProjectMember
|
||||
showRoleIcons
|
||||
zIndex={projectMembers.length - idx}
|
||||
zIndex={projectMembers.length - idx + projectInvitedMembers.length}
|
||||
key={member.id}
|
||||
size={28}
|
||||
member={member}
|
||||
onMemberProfile={onMemberProfile}
|
||||
/>
|
||||
))}
|
||||
{projectInvitedMembers.map((member, idx) => (
|
||||
<ProjectMember
|
||||
showRoleIcons
|
||||
zIndex={projectInvitedMembers.length - idx}
|
||||
key={member.email}
|
||||
size={28}
|
||||
invited
|
||||
member={{
|
||||
id: member.email,
|
||||
fullName: member.email,
|
||||
profileIcon: {
|
||||
url: null,
|
||||
initials: member.email.charAt(0),
|
||||
bgColor: '#fff',
|
||||
},
|
||||
}}
|
||||
onMemberProfile={onInvitedMemberProfile}
|
||||
/>
|
||||
))}
|
||||
{canInviteUser && (
|
||||
<InviteButton
|
||||
onClick={$target => {
|
||||
@ -283,6 +309,9 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<CheckCircle width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<ListUnordered width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
</IconContainer>
|
||||
|
@ -113,6 +113,14 @@ export type UserAccount = {
|
||||
member: MemberList;
|
||||
};
|
||||
|
||||
export type InvitedUserAccount = {
|
||||
__typename?: 'InvitedUserAccount';
|
||||
id: Scalars['ID'];
|
||||
email: Scalars['String'];
|
||||
invitedOn: Scalars['Time'];
|
||||
member: MemberList;
|
||||
};
|
||||
|
||||
export type Team = {
|
||||
__typename?: 'Team';
|
||||
id: Scalars['ID'];
|
||||
@ -121,6 +129,12 @@ export type Team = {
|
||||
members: Array<Member>;
|
||||
};
|
||||
|
||||
export type InvitedMember = {
|
||||
__typename?: 'InvitedMember';
|
||||
email: Scalars['String'];
|
||||
invitedOn: Scalars['Time'];
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
__typename?: 'Project';
|
||||
id: Scalars['ID'];
|
||||
@ -129,6 +143,7 @@ export type Project = {
|
||||
team?: Maybe<Team>;
|
||||
taskGroups: Array<TaskGroup>;
|
||||
members: Array<Member>;
|
||||
invitedMembers: Array<InvitedMember>;
|
||||
labels: Array<ProjectLabel>;
|
||||
};
|
||||
|
||||
@ -221,11 +236,13 @@ export type Query = {
|
||||
findTask: Task;
|
||||
findTeam: Team;
|
||||
findUser: UserAccount;
|
||||
invitedUsers: Array<InvitedUserAccount>;
|
||||
labelColors: Array<LabelColor>;
|
||||
me: MePayload;
|
||||
notifications: Array<Notification>;
|
||||
organizations: Array<Organization>;
|
||||
projects: Array<Project>;
|
||||
searchMembers: Array<MemberSearchResult>;
|
||||
taskGroups: Array<TaskGroup>;
|
||||
teams: Array<Team>;
|
||||
users: Array<UserAccount>;
|
||||
@ -256,6 +273,11 @@ export type QueryProjectsArgs = {
|
||||
input?: Maybe<ProjectsFilter>;
|
||||
};
|
||||
|
||||
|
||||
export type QuerySearchMembersArgs = {
|
||||
input: MemberSearchFilter;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
addTaskLabel: Task;
|
||||
@ -263,7 +285,6 @@ export type Mutation = {
|
||||
clearProfileAvatar: UserAccount;
|
||||
createProject: Project;
|
||||
createProjectLabel: ProjectLabel;
|
||||
createProjectMember: CreateProjectMemberPayload;
|
||||
createRefreshToken: RefreshToken;
|
||||
createTask: Task;
|
||||
createTaskChecklist: TaskChecklist;
|
||||
@ -272,6 +293,8 @@ export type Mutation = {
|
||||
createTeam: Team;
|
||||
createTeamMember: CreateTeamMemberPayload;
|
||||
createUserAccount: UserAccount;
|
||||
deleteInvitedProjectMember: DeleteInvitedProjectMemberPayload;
|
||||
deleteInvitedUserAccount: DeleteInvitedUserAccountPayload;
|
||||
deleteProject: DeleteProjectPayload;
|
||||
deleteProjectLabel: ProjectLabel;
|
||||
deleteProjectMember: DeleteProjectMemberPayload;
|
||||
@ -284,6 +307,7 @@ export type Mutation = {
|
||||
deleteTeamMember: DeleteTeamMemberPayload;
|
||||
deleteUserAccount: DeleteUserAccountPayload;
|
||||
duplicateTaskGroup: DuplicateTaskGroupPayload;
|
||||
inviteProjectMembers: InviteProjectMembersPayload;
|
||||
logoutUser: Scalars['Boolean'];
|
||||
removeTaskLabel: Task;
|
||||
setTaskChecklistItemComplete: TaskChecklistItem;
|
||||
@ -333,11 +357,6 @@ export type MutationCreateProjectLabelArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateProjectMemberArgs = {
|
||||
input: CreateProjectMember;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateRefreshTokenArgs = {
|
||||
input: NewRefreshToken;
|
||||
};
|
||||
@ -378,6 +397,16 @@ export type MutationCreateUserAccountArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteInvitedProjectMemberArgs = {
|
||||
input: DeleteInvitedProjectMember;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteInvitedUserAccountArgs = {
|
||||
input: DeleteInvitedUserAccount;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteProjectArgs = {
|
||||
input: DeleteProject;
|
||||
};
|
||||
@ -438,6 +467,11 @@ export type MutationDuplicateTaskGroupArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationInviteProjectMembersArgs = {
|
||||
input: InviteProjectMembers;
|
||||
};
|
||||
|
||||
|
||||
export type MutationLogoutUserArgs = {
|
||||
input: LogoutUser;
|
||||
};
|
||||
@ -688,15 +722,32 @@ export type UpdateProjectLabelColor = {
|
||||
labelColorID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type CreateProjectMember = {
|
||||
export type DeleteInvitedProjectMember = {
|
||||
projectID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
email: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateProjectMemberPayload = {
|
||||
__typename?: 'CreateProjectMemberPayload';
|
||||
export type DeleteInvitedProjectMemberPayload = {
|
||||
__typename?: 'DeleteInvitedProjectMemberPayload';
|
||||
invitedMember: InvitedMember;
|
||||
};
|
||||
|
||||
export type MemberInvite = {
|
||||
userID?: Maybe<Scalars['UUID']>;
|
||||
email?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type InviteProjectMembers = {
|
||||
projectID: Scalars['UUID'];
|
||||
members: Array<MemberInvite>;
|
||||
};
|
||||
|
||||
export type InviteProjectMembersPayload = {
|
||||
__typename?: 'InviteProjectMembersPayload';
|
||||
ok: Scalars['Boolean'];
|
||||
member: Member;
|
||||
projectID: Scalars['UUID'];
|
||||
members: Array<Member>;
|
||||
invitedMembers: Array<InvitedMember>;
|
||||
};
|
||||
|
||||
export type DeleteProjectMember = {
|
||||
@ -989,6 +1040,29 @@ export type UpdateTeamMemberRolePayload = {
|
||||
member: Member;
|
||||
};
|
||||
|
||||
export type DeleteInvitedUserAccount = {
|
||||
invitedUserID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteInvitedUserAccountPayload = {
|
||||
__typename?: 'DeleteInvitedUserAccountPayload';
|
||||
invitedUser: InvitedUserAccount;
|
||||
};
|
||||
|
||||
export type MemberSearchFilter = {
|
||||
SearchFilter: Scalars['String'];
|
||||
projectID?: Maybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type MemberSearchResult = {
|
||||
__typename?: 'MemberSearchResult';
|
||||
similarity: Scalars['Int'];
|
||||
user: UserAccount;
|
||||
confirmed: Scalars['Boolean'];
|
||||
invited: Scalars['Boolean'];
|
||||
joined: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UpdateUserInfoPayload = {
|
||||
__typename?: 'UpdateUserInfoPayload';
|
||||
user: UserAccount;
|
||||
@ -1205,6 +1279,9 @@ export type FindProjectQuery = (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||
) }
|
||||
)>, invitedMembers: Array<(
|
||||
{ __typename?: 'InvitedMember' }
|
||||
& Pick<InvitedMember, 'email' | 'invitedOn'>
|
||||
)>, labels: Array<(
|
||||
{ __typename?: 'ProjectLabel' }
|
||||
& Pick<ProjectLabel, 'id' | 'createdDate' | 'name'>
|
||||
@ -1390,31 +1467,6 @@ export type MeQuery = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateProjectMemberMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type CreateProjectMemberMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { createProjectMember: (
|
||||
{ __typename?: 'CreateProjectMemberPayload' }
|
||||
& Pick<CreateProjectMemberPayload, 'ok'>
|
||||
& { member: (
|
||||
{ __typename?: 'Member' }
|
||||
& Pick<Member, 'id' | 'fullName' | 'username'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||
), role: (
|
||||
{ __typename?: 'Role' }
|
||||
& Pick<Role, 'code' | 'name'>
|
||||
) }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteProjectMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
};
|
||||
@ -1432,6 +1484,23 @@ export type DeleteProjectMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteInvitedProjectMemberMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
email: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type DeleteInvitedProjectMemberMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { deleteInvitedProjectMember: (
|
||||
{ __typename?: 'DeleteInvitedProjectMemberPayload' }
|
||||
& { invitedMember: (
|
||||
{ __typename?: 'InvitedMember' }
|
||||
& Pick<InvitedMember, 'email'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteProjectMemberMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
@ -1450,6 +1519,34 @@ export type DeleteProjectMemberMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type InviteProjectMembersMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
members: Array<MemberInvite>;
|
||||
};
|
||||
|
||||
|
||||
export type InviteProjectMembersMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { inviteProjectMembers: (
|
||||
{ __typename?: 'InviteProjectMembersPayload' }
|
||||
& Pick<InviteProjectMembersPayload, 'ok'>
|
||||
& { invitedMembers: Array<(
|
||||
{ __typename?: 'InvitedMember' }
|
||||
& Pick<InvitedMember, 'email' | 'invitedOn'>
|
||||
)>, members: Array<(
|
||||
{ __typename?: 'Member' }
|
||||
& Pick<Member, 'id' | 'fullName' | 'username'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||
), role: (
|
||||
{ __typename?: 'Role' }
|
||||
& Pick<Role, 'code' | 'name'>
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type UpdateProjectMemberRoleMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
@ -2130,6 +2227,22 @@ export type CreateUserAccountMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteInvitedUserAccountMutationVariables = {
|
||||
invitedUserID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type DeleteInvitedUserAccountMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { deleteInvitedUserAccount: (
|
||||
{ __typename?: 'DeleteInvitedUserAccountPayload' }
|
||||
& { invitedUser: (
|
||||
{ __typename?: 'InvitedUserAccount' }
|
||||
& Pick<InvitedUserAccount, 'id'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteUserAccountMutationVariables = {
|
||||
userID: Scalars['UUID'];
|
||||
newOwnerID?: Maybe<Scalars['UUID']>;
|
||||
@ -2211,7 +2324,10 @@ export type UsersQueryVariables = {};
|
||||
|
||||
export type UsersQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { users: Array<(
|
||||
& { invitedUsers: Array<(
|
||||
{ __typename?: 'InvitedUserAccount' }
|
||||
& Pick<InvitedUserAccount, 'id' | 'email' | 'invitedOn'>
|
||||
)>, users: Array<(
|
||||
{ __typename?: 'UserAccount' }
|
||||
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
|
||||
& { role: (
|
||||
@ -2603,6 +2719,10 @@ export const FindProjectDocument = gql`
|
||||
bgColor
|
||||
}
|
||||
}
|
||||
invitedMembers {
|
||||
email
|
||||
invitedOn
|
||||
}
|
||||
labels {
|
||||
id
|
||||
createdDate
|
||||
@ -2884,53 +3004,6 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio
|
||||
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
|
||||
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
|
||||
export type MeQueryResult = ApolloReactCommon.QueryResult<MeQuery, MeQueryVariables>;
|
||||
export const CreateProjectMemberDocument = gql`
|
||||
mutation createProjectMember($projectID: UUID!, $userID: UUID!) {
|
||||
createProjectMember(input: {projectID: $projectID, userID: $userID}) {
|
||||
ok
|
||||
member {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
bgColor
|
||||
}
|
||||
username
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateProjectMemberMutationFn = ApolloReactCommon.MutationFunction<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateProjectMemberMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateProjectMemberMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateProjectMemberMutation` 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 [createProjectMemberMutation, { data, loading, error }] = useCreateProjectMemberMutation({
|
||||
* variables: {
|
||||
* projectID: // value for 'projectID'
|
||||
* userID: // value for 'userID'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateProjectMemberMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>) {
|
||||
return ApolloReactHooks.useMutation<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>(CreateProjectMemberDocument, baseOptions);
|
||||
}
|
||||
export type CreateProjectMemberMutationHookResult = ReturnType<typeof useCreateProjectMemberMutation>;
|
||||
export type CreateProjectMemberMutationResult = ApolloReactCommon.MutationResult<CreateProjectMemberMutation>;
|
||||
export type CreateProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>;
|
||||
export const DeleteProjectDocument = gql`
|
||||
mutation deleteProject($projectID: UUID!) {
|
||||
deleteProject(input: {projectID: $projectID}) {
|
||||
@ -2966,6 +3039,41 @@ export function useDeleteProjectMutation(baseOptions?: ApolloReactHooks.Mutation
|
||||
export type DeleteProjectMutationHookResult = ReturnType<typeof useDeleteProjectMutation>;
|
||||
export type DeleteProjectMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMutation>;
|
||||
export type DeleteProjectMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMutation, DeleteProjectMutationVariables>;
|
||||
export const DeleteInvitedProjectMemberDocument = gql`
|
||||
mutation deleteInvitedProjectMember($projectID: UUID!, $email: String!) {
|
||||
deleteInvitedProjectMember(input: {projectID: $projectID, email: $email}) {
|
||||
invitedMember {
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteInvitedProjectMemberMutationFn = ApolloReactCommon.MutationFunction<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteInvitedProjectMemberMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteInvitedProjectMemberMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteInvitedProjectMemberMutation` 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 [deleteInvitedProjectMemberMutation, { data, loading, error }] = useDeleteInvitedProjectMemberMutation({
|
||||
* variables: {
|
||||
* projectID: // value for 'projectID'
|
||||
* email: // value for 'email'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteInvitedProjectMemberMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>) {
|
||||
return ApolloReactHooks.useMutation<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>(DeleteInvitedProjectMemberDocument, baseOptions);
|
||||
}
|
||||
export type DeleteInvitedProjectMemberMutationHookResult = ReturnType<typeof useDeleteInvitedProjectMemberMutation>;
|
||||
export type DeleteInvitedProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteInvitedProjectMemberMutation>;
|
||||
export type DeleteInvitedProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>;
|
||||
export const DeleteProjectMemberDocument = gql`
|
||||
mutation deleteProjectMember($projectID: UUID!, $userID: UUID!) {
|
||||
deleteProjectMember(input: {projectID: $projectID, userID: $userID}) {
|
||||
@ -3003,6 +3111,57 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu
|
||||
export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>;
|
||||
export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>;
|
||||
export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMemberMutation, DeleteProjectMemberMutationVariables>;
|
||||
export const InviteProjectMembersDocument = gql`
|
||||
mutation inviteProjectMembers($projectID: UUID!, $members: [MemberInvite!]!) {
|
||||
inviteProjectMembers(input: {projectID: $projectID, members: $members}) {
|
||||
ok
|
||||
invitedMembers {
|
||||
email
|
||||
invitedOn
|
||||
}
|
||||
members {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
bgColor
|
||||
}
|
||||
username
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type InviteProjectMembersMutationFn = ApolloReactCommon.MutationFunction<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useInviteProjectMembersMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useInviteProjectMembersMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useInviteProjectMembersMutation` 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 [inviteProjectMembersMutation, { data, loading, error }] = useInviteProjectMembersMutation({
|
||||
* variables: {
|
||||
* projectID: // value for 'projectID'
|
||||
* members: // value for 'members'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useInviteProjectMembersMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>) {
|
||||
return ApolloReactHooks.useMutation<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>(InviteProjectMembersDocument, baseOptions);
|
||||
}
|
||||
export type InviteProjectMembersMutationHookResult = ReturnType<typeof useInviteProjectMembersMutation>;
|
||||
export type InviteProjectMembersMutationResult = ApolloReactCommon.MutationResult<InviteProjectMembersMutation>;
|
||||
export type InviteProjectMembersMutationOptions = ApolloReactCommon.BaseMutationOptions<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>;
|
||||
export const UpdateProjectMemberRoleDocument = gql`
|
||||
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
||||
updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) {
|
||||
@ -4381,6 +4540,40 @@ export function useCreateUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
|
||||
export type CreateUserAccountMutationHookResult = ReturnType<typeof useCreateUserAccountMutation>;
|
||||
export type CreateUserAccountMutationResult = ApolloReactCommon.MutationResult<CreateUserAccountMutation>;
|
||||
export type CreateUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateUserAccountMutation, CreateUserAccountMutationVariables>;
|
||||
export const DeleteInvitedUserAccountDocument = gql`
|
||||
mutation deleteInvitedUserAccount($invitedUserID: UUID!) {
|
||||
deleteInvitedUserAccount(input: {invitedUserID: $invitedUserID}) {
|
||||
invitedUser {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteInvitedUserAccountMutationFn = ApolloReactCommon.MutationFunction<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteInvitedUserAccountMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteInvitedUserAccountMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteInvitedUserAccountMutation` 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 [deleteInvitedUserAccountMutation, { data, loading, error }] = useDeleteInvitedUserAccountMutation({
|
||||
* variables: {
|
||||
* invitedUserID: // value for 'invitedUserID'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteInvitedUserAccountMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>) {
|
||||
return ApolloReactHooks.useMutation<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>(DeleteInvitedUserAccountDocument, baseOptions);
|
||||
}
|
||||
export type DeleteInvitedUserAccountMutationHookResult = ReturnType<typeof useDeleteInvitedUserAccountMutation>;
|
||||
export type DeleteInvitedUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteInvitedUserAccountMutation>;
|
||||
export type DeleteInvitedUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>;
|
||||
export const DeleteUserAccountDocument = gql`
|
||||
mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) {
|
||||
deleteUserAccount(input: {userID: $userID, newOwnerID: $newOwnerID}) {
|
||||
@ -4534,6 +4727,11 @@ export type UpdateUserRoleMutationResult = ApolloReactCommon.MutationResult<Upda
|
||||
export type UpdateUserRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>;
|
||||
export const UsersDocument = gql`
|
||||
query users {
|
||||
invitedUsers {
|
||||
id
|
||||
email
|
||||
invitedOn
|
||||
}
|
||||
users {
|
||||
id
|
||||
email
|
||||
@ -4595,4 +4793,4 @@ export function useUsersLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOp
|
||||
}
|
||||
export type UsersQueryHookResult = ReturnType<typeof useUsersQuery>;
|
||||
export type UsersLazyQueryHookResult = ReturnType<typeof useUsersLazyQuery>;
|
||||
export type UsersQueryResult = ApolloReactCommon.QueryResult<UsersQuery, UsersQueryVariables>;
|
||||
export type UsersQueryResult = ApolloReactCommon.QueryResult<UsersQuery, UsersQueryVariables>;
|
@ -22,6 +22,10 @@ query findProject($projectID: UUID!) {
|
||||
bgColor
|
||||
}
|
||||
}
|
||||
invitedMembers {
|
||||
email
|
||||
invitedOn
|
||||
}
|
||||
labels {
|
||||
id
|
||||
createdDate
|
||||
|
@ -1,25 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const CREATE_PROJECT_MEMBER_MUTATION = gql`
|
||||
mutation createProjectMember($projectID: UUID!, $userID: UUID!) {
|
||||
createProjectMember(input: { projectID: $projectID, userID: $userID }) {
|
||||
ok
|
||||
member {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
bgColor
|
||||
}
|
||||
username
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default CREATE_PROJECT_MEMBER_MUTATION;
|
@ -0,0 +1,13 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const DELETE_PROJECT_INVITED_MEMBER_MUTATION = gql`
|
||||
mutation deleteInvitedProjectMember($projectID: UUID!, $email: String!) {
|
||||
deleteInvitedProjectMember(input: { projectID: $projectID, email: $email }) {
|
||||
invitedMember {
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default DELETE_PROJECT_INVITED_MEMBER_MUTATION;
|
29
frontend/src/shared/graphql/project/inviteProjectMembers.ts
Normal file
29
frontend/src/shared/graphql/project/inviteProjectMembers.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const INVITE_PROJECT_MEMBERS_MUTATION = gql`
|
||||
mutation inviteProjectMembers($projectID: UUID!, $members: [MemberInvite!]!) {
|
||||
inviteProjectMembers(input: { projectID: $projectID, members: $members }) {
|
||||
ok
|
||||
invitedMembers {
|
||||
email
|
||||
invitedOn
|
||||
}
|
||||
members {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
bgColor
|
||||
}
|
||||
username
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default INVITE_PROJECT_MEMBERS_MUTATION;
|
13
frontend/src/shared/graphql/user/deleteInvitedUser.ts
Normal file
13
frontend/src/shared/graphql/user/deleteInvitedUser.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const DELETE_INVITED_USER_MUTATION = gql`
|
||||
mutation deleteInvitedUserAccount($invitedUserID: UUID!) {
|
||||
deleteInvitedUserAccount(input: { invitedUserID: $invitedUserID }) {
|
||||
invitedUser {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default DELETE_INVITED_USER_MUTATION;
|
@ -1,4 +1,9 @@
|
||||
query users {
|
||||
invitedUsers {
|
||||
id
|
||||
email
|
||||
invitedOn
|
||||
}
|
||||
users {
|
||||
id
|
||||
email
|
||||
|
12
frontend/src/shared/icons/ArrowDown.tsx
Normal file
12
frontend/src/shared/icons/ArrowDown.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const ArrowDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 448 512">
|
||||
<path d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArrowDown;
|
12
frontend/src/shared/icons/CaretDown.tsx
Normal file
12
frontend/src/shared/icons/CaretDown.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const CaretDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 320 512">
|
||||
<path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaretDown;
|
12
frontend/src/shared/icons/CaretRight.tsx
Normal file
12
frontend/src/shared/icons/CaretRight.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const CaretRight: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 192 512">
|
||||
<path d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaretRight;
|
12
frontend/src/shared/icons/Dot.tsx
Normal file
12
frontend/src/shared/icons/Dot.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const Dot: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 18 18">
|
||||
<circle cx="9" cy="9" r="3.5" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dot;
|
12
frontend/src/shared/icons/DotCircle.tsx
Normal file
12
frontend/src/shared/icons/DotCircle.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const DotCircle: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
|
||||
<path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm80 248c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotCircle;
|
12
frontend/src/shared/icons/EyeSlash.tsx
Normal file
12
frontend/src/shared/icons/EyeSlash.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const EyeSlash: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 640 512">
|
||||
<path d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default EyeSlash;
|
12
frontend/src/shared/icons/ListUnordered.tsx
Normal file
12
frontend/src/shared/icons/ListUnordered.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const ListUnordered: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
|
||||
<path d="M48 48a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0 160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0 160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm448 16H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListUnordered;
|
@ -1,9 +1,16 @@
|
||||
import Cross from './Cross';
|
||||
import Cog from './Cog';
|
||||
import ArrowDown from './ArrowDown';
|
||||
import ListUnordered from './ListUnordered';
|
||||
import Dot from './Dot';
|
||||
import CaretDown from './CaretDown';
|
||||
import Eye from './Eye';
|
||||
import EyeSlash from './EyeSlash';
|
||||
import CaretRight from './CaretRight';
|
||||
import List from './List';
|
||||
import At from './At';
|
||||
import Task from './Task';
|
||||
import DotCircle from './DotCircle';
|
||||
import Smile from './Smile';
|
||||
import Paperclip from './Paperclip';
|
||||
import Calendar from './Calendar';
|
||||
@ -88,5 +95,12 @@ export {
|
||||
Paperclip,
|
||||
Share,
|
||||
Eye,
|
||||
ListUnordered,
|
||||
EyeSlash,
|
||||
List,
|
||||
CaretDown,
|
||||
Dot,
|
||||
ArrowDown,
|
||||
CaretRight,
|
||||
DotCircle,
|
||||
};
|
||||
|
147
frontend/src/taskcafe.d.ts
vendored
147
frontend/src/taskcafe.d.ts
vendored
@ -61,7 +61,7 @@ type User = TaskUser & {
|
||||
|
||||
type RefreshTokenResponse = {
|
||||
accessToken: string;
|
||||
isInstalled: boolean;
|
||||
setup?: null | { confirmToken: string };
|
||||
};
|
||||
|
||||
type LoginFormData = {
|
||||
@ -91,7 +91,14 @@ type ErrorOption =
|
||||
type: string;
|
||||
};
|
||||
|
||||
type SetFailedFn = () => void;
|
||||
type ConfirmProps = {
|
||||
hasConfirmToken: boolean;
|
||||
onConfirmUser: (setFailed: SetFailedFn) => void;
|
||||
};
|
||||
|
||||
type RegisterProps = {
|
||||
registered?: boolean;
|
||||
onSubmit: (
|
||||
data: RegisterFormData,
|
||||
setComplete: (val: boolean) => void,
|
||||
@ -127,3 +134,141 @@ type ElementBounds = {
|
||||
};
|
||||
|
||||
type CardLabelVariant = 'large' | 'small';
|
||||
|
||||
type InvitedUser = {
|
||||
email: string;
|
||||
invitedOn: string;
|
||||
};
|
||||
|
||||
type InvitedUserAccount = {
|
||||
id: string;
|
||||
email: string;
|
||||
invitedOn: string;
|
||||
};
|
||||
|
||||
type NodeDimensions = {
|
||||
entry: React.RefObject<HTMLElement>;
|
||||
children: React.RefObject<HTMLElement> | null;
|
||||
};
|
||||
|
||||
type OutlineNode = {
|
||||
id: string;
|
||||
parent: string;
|
||||
depth: number;
|
||||
position: number;
|
||||
ancestors: Array<string>;
|
||||
collapsed: boolean;
|
||||
children: number;
|
||||
};
|
||||
|
||||
type RelationshipChild = {
|
||||
position: number;
|
||||
id: string;
|
||||
depth: number;
|
||||
children: number;
|
||||
};
|
||||
|
||||
type NodeRelationships = {
|
||||
self: { id: string; depth: number };
|
||||
children: Array<RelationshipChild>;
|
||||
numberOfSubChildren: number;
|
||||
};
|
||||
|
||||
type OutlineData = {
|
||||
published: Map<string, string>;
|
||||
nodes: Map<number, Map<string, OutlineNode>>;
|
||||
relationships: Map<string, NodeRelationships>;
|
||||
dimensions: Map<string, NodeDimensions>;
|
||||
};
|
||||
|
||||
type ImpactZoneData = {
|
||||
node: OutlineNode;
|
||||
dimensions: NodeDimensions;
|
||||
};
|
||||
|
||||
type ImpactZone = {
|
||||
above: ImpactZoneData | null;
|
||||
below: ImpactZoneData | null;
|
||||
};
|
||||
|
||||
type ImpactData = {
|
||||
zone: ImpactZone;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
type ImpactPosition = 'before' | 'after' | 'beforeChildren' | 'afterChildren';
|
||||
|
||||
type ImpactAction = {
|
||||
on: 'children' | 'entry';
|
||||
position: ImpactPosition;
|
||||
};
|
||||
|
||||
type ItemElement = {
|
||||
id: string;
|
||||
parent: null | string;
|
||||
position: number;
|
||||
collapsed: boolean;
|
||||
children?: Array<ItemElement>;
|
||||
};
|
||||
type NodeDimensions = {
|
||||
entry: React.RefObject<HTMLElement>;
|
||||
children: React.RefObject<HTMLElement> | null;
|
||||
};
|
||||
|
||||
type OutlineNode = {
|
||||
id: string;
|
||||
parent: string;
|
||||
depth: number;
|
||||
position: number;
|
||||
ancestors: Array<string>;
|
||||
children: number;
|
||||
};
|
||||
|
||||
type RelationshipChild = {
|
||||
position: number;
|
||||
id: string;
|
||||
depth: number;
|
||||
children: number;
|
||||
};
|
||||
|
||||
type NodeRelationships = {
|
||||
self: { id: string; depth: number };
|
||||
children: Array<RelationshipChild>;
|
||||
numberOfSubChildren: number;
|
||||
};
|
||||
|
||||
type OutlineData = {
|
||||
published: Map<string, string>;
|
||||
nodes: Map<number, Map<string, OutlineNode>>;
|
||||
relationships: Map<string, NodeRelationships>;
|
||||
dimensions: Map<string, NodeDimensions>;
|
||||
};
|
||||
|
||||
type ImpactZoneData = {
|
||||
node: OutlineNode;
|
||||
dimensions: NodeDimensions;
|
||||
};
|
||||
|
||||
type ImpactZone = {
|
||||
above: ImpactZoneData | null;
|
||||
below: ImpactZoneData | null;
|
||||
};
|
||||
|
||||
type ImpactData = {
|
||||
zone: ImpactZone;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
type ImpactPosition = 'before' | 'after' | 'beforeChildren' | 'afterChildren';
|
||||
|
||||
type ImpactAction = {
|
||||
on: 'children' | 'entry';
|
||||
position: ImpactPosition;
|
||||
};
|
||||
|
||||
type ItemElement = {
|
||||
id: string;
|
||||
parent: null | string;
|
||||
position: number;
|
||||
children?: Array<ItemElement>;
|
||||
};
|
||||
|
@ -3112,6 +3112,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
||||
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
|
||||
|
||||
"@types/query-string@^6.3.0":
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.3.0.tgz#b6fa172a01405abcaedac681118e78429d62ea39"
|
||||
integrity sha512-yuIv/WRffRzL7cBW+sla4HwBZrEXRNf1MKQ5SklPEadth+BKbDxiVG8A3iISN5B3yC4EeSCzMZP8llHTcUhOzQ==
|
||||
dependencies:
|
||||
query-string "*"
|
||||
|
||||
"@types/reach__router@^1.2.3":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.0.tgz#4c05a947ccecca05c72bb335a0f7bb43fec12446"
|
||||
@ -10600,6 +10607,11 @@ lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@^4.17.20:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||
|
||||
log-symbols@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
|
||||
@ -13351,6 +13363,15 @@ qs@~6.5.2:
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
|
||||
query-string@*, query-string@^6.13.7:
|
||||
version "6.13.7"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.7.tgz#af53802ff6ed56f3345f92d40a056f93681026ee"
|
||||
integrity sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
query-string@^4.1.0:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
|
||||
@ -15249,6 +15270,11 @@ speedometer@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.0.0.tgz#cd671cb06752c22bca3370e2f334440be4fc62e2"
|
||||
integrity sha1-zWccsGdSwivKM3Di8zREC+T8YuI=
|
||||
|
||||
split-on-first@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
||||
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
@ -15402,6 +15428,11 @@ strict-uri-encode@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
|
3
go.mod
3
go.mod
@ -11,7 +11,9 @@ require (
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/lib/pq v1.3.0
|
||||
github.com/lithammer/fuzzysearch v1.1.0
|
||||
github.com/magefile/mage v1.9.0
|
||||
github.com/matcornic/hermes/v2 v2.1.0
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0
|
||||
@ -21,4 +23,5 @@ require (
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/vektah/gqlparser/v2 v2.0.1
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
)
|
||||
|
41
go.sum
41
go.sum
@ -50,11 +50,17 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs=
|
||||
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
|
||||
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
|
||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae h1:DcFpTQBYQ9Ct2d6sC7ol0/ynxc2pO1cpGUM+f4t5adg=
|
||||
github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae/go.mod h1:rJJ84PyA/Wlmw1hO+xTzV2wsSUon6J5ktg0g8BF2PuU=
|
||||
github.com/RichardKnop/machinery v1.9.1 h1:Q4WInk0OWGMbXDH3Q8dm8uadN5Wcyquc+7IcM4p9ECs=
|
||||
@ -70,6 +76,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
@ -109,6 +119,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk=
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -150,6 +161,7 @@ github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
@ -257,6 +269,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@ -264,6 +277,8 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
@ -290,7 +305,11 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
@ -317,6 +336,8 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
||||
@ -357,6 +378,8 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A=
|
||||
github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
|
||||
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
@ -365,6 +388,9 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
|
||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/matcornic/hermes v1.2.0 h1:AuqZpYcTOtTB7cahdevLfnhIpfzmpqw5Czv8vpdnFDU=
|
||||
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
|
||||
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
@ -373,6 +399,8 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
@ -391,6 +419,8 @@ github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86w
|
||||
github.com/neo4j-drivers/gobolt v1.7.4/go.mod h1:O9AUbip4Dgre+CD3p40dnMD4a4r52QBIfblg5k7CTbE=
|
||||
github.com/neo4j/neo4j-go-driver v1.7.4/go.mod h1:aPO0vVr+WnhEJne+FgFjfsjzAnssPFLucHgGZ76Zb/U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
|
||||
@ -479,6 +509,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
|
||||
github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -502,6 +534,10 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
|
||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 h1:L0rPdfzq43+NV8rfIx2kA4iSSLRj2jN5ijYHoeXRwvQ=
|
||||
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
|
||||
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy+ad6BM+JCLJb2ZV7/TNiE5l7SNKfumYKgc=
|
||||
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
|
||||
@ -540,6 +576,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
@ -591,6 +628,7 @@ golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -654,6 +692,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -862,6 +901,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
|
@ -62,10 +62,7 @@ func initConfig() {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the root cobra command
|
||||
func Execute() {
|
||||
viper.SetDefault("server.hostname", "0.0.0.0:3333")
|
||||
viper.SetDefault("database.host", "127.0.0.1")
|
||||
viper.SetDefault("database.name", "taskcafe")
|
||||
@ -75,6 +72,20 @@ func Execute() {
|
||||
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
|
||||
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
||||
|
||||
}
|
||||
|
||||
// Execute the root cobra command
|
||||
func Execute() {
|
||||
viper.SetDefault("server.hostname", "0.0.0.0:3333")
|
||||
viper.SetDefault("database.host", "127.0.0.1")
|
||||
viper.SetDefault("database.name", "taskcafe")
|
||||
viper.SetDefault("database.user", "taskcafe")
|
||||
viper.SetDefault("database.password", "taskcafe_test")
|
||||
viper.SetDefault("database.port", "5432")
|
||||
|
||||
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
|
||||
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
||||
|
||||
rootCmd.SetVersionTemplate(versionTemplate)
|
||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd(), newWorkerCmd(), newResetPasswordCmd())
|
||||
rootCmd.Execute()
|
||||
|
@ -34,11 +34,12 @@ func newMigrateCmd() *cobra.Command {
|
||||
Short: "Run the database schema migrations",
|
||||
Long: "Run the database schema migrations",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=disable",
|
||||
viper.GetString("database.user"),
|
||||
viper.GetString("database.password"),
|
||||
viper.GetString("database.host"),
|
||||
viper.GetString("database.name"),
|
||||
viper.GetString("database.port"),
|
||||
)
|
||||
db, err := sqlx.Connect("postgres", connection)
|
||||
if err != nil {
|
||||
|
@ -32,11 +32,12 @@ func newWebCmd() *cobra.Command {
|
||||
log.SetFormatter(Formatter)
|
||||
log.SetLevel(log.InfoLevel)
|
||||
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=disable",
|
||||
viper.GetString("database.user"),
|
||||
viper.GetString("database.password"),
|
||||
viper.GetString("database.host"),
|
||||
viper.GetString("database.name"),
|
||||
viper.GetString("database.port"),
|
||||
)
|
||||
var db *sqlx.DB
|
||||
var err error
|
||||
@ -78,8 +79,11 @@ func newWebCmd() *cobra.Command {
|
||||
return http.ListenAndServe(viper.GetString("server.hostname"), r)
|
||||
},
|
||||
}
|
||||
|
||||
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
|
||||
|
||||
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
|
||||
|
||||
viper.SetDefault("migrate", false)
|
||||
return cc
|
||||
}
|
||||
|
@ -67,6 +67,12 @@ type ProjectMember struct {
|
||||
RoleCode string `json:"role_code"`
|
||||
}
|
||||
|
||||
type ProjectMemberInvited struct {
|
||||
ProjectMemberInvitedID uuid.UUID `json:"project_member_invited_id"`
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
UserAccountInvitedID uuid.UUID `json:"user_account_invited_id"`
|
||||
}
|
||||
|
||||
type RefreshToken struct {
|
||||
TokenID uuid.UUID `json:"token_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
@ -164,4 +170,17 @@ type UserAccount struct {
|
||||
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
||||
RoleCode string `json:"role_code"`
|
||||
Bio string `json:"bio"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type UserAccountConfirmToken struct {
|
||||
ConfirmTokenID uuid.UUID `json:"confirm_token_id"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type UserAccountInvited struct {
|
||||
UserAccountInvitedID uuid.UUID `json:"user_account_invited_id"`
|
||||
Email string `json:"email"`
|
||||
InvitedOn time.Time `json:"invited_on"`
|
||||
HasJoined bool `json:"has_joined"`
|
||||
}
|
||||
|
@ -99,6 +99,15 @@ func (q *Queries) CreateTeamProject(ctx context.Context, arg CreateTeamProjectPa
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteInvitedProjectMemberByID = `-- name: DeleteInvitedProjectMemberByID :exec
|
||||
DELETE FROM project_member_invited WHERE project_member_invited_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteInvitedProjectMemberByID, projectMemberInvitedID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteProjectByID = `-- name: DeleteProjectByID :exec
|
||||
DELETE FROM project WHERE project_id = $1
|
||||
`
|
||||
@ -122,12 +131,12 @@ func (q *Queries) DeleteProjectMember(ctx context.Context, arg DeleteProjectMemb
|
||||
return err
|
||||
}
|
||||
|
||||
const getAllProjects = `-- name: GetAllProjects :many
|
||||
SELECT project_id, team_id, created_at, name FROM project
|
||||
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many
|
||||
SELECT project_id, team_id, created_at, name FROM project WHERE team_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllProjects)
|
||||
func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllProjectsForTeam, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -154,12 +163,12 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many
|
||||
SELECT project_id, team_id, created_at, name FROM project WHERE team_id = $1
|
||||
const getAllTeamProjects = `-- name: GetAllTeamProjects :many
|
||||
SELECT project_id, team_id, created_at, name FROM project WHERE team_id IS NOT null
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllProjectsForTeam, teamID)
|
||||
func (q *Queries) GetAllTeamProjects(ctx context.Context) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllTeamProjects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -219,6 +228,42 @@ func (q *Queries) GetAllVisibleProjectsForUserID(ctx context.Context, userID uui
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getInvitedMembersForProjectID = `-- name: GetInvitedMembersForProjectID :many
|
||||
SELECT uai.user_account_invited_id, email, invited_on FROM project_member_invited AS pmi
|
||||
INNER JOIN user_account_invited AS uai
|
||||
ON uai.user_account_invited_id = pmi.user_account_invited_id
|
||||
WHERE project_id = $1
|
||||
`
|
||||
|
||||
type GetInvitedMembersForProjectIDRow struct {
|
||||
UserAccountInvitedID uuid.UUID `json:"user_account_invited_id"`
|
||||
Email string `json:"email"`
|
||||
InvitedOn time.Time `json:"invited_on"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getInvitedMembersForProjectID, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetInvitedMembersForProjectIDRow
|
||||
for rows.Next() {
|
||||
var i GetInvitedMembersForProjectIDRow
|
||||
if err := rows.Scan(&i.UserAccountInvitedID, &i.Email, &i.InvitedOn); 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 getMemberProjectIDsForUserID = `-- name: GetMemberProjectIDsForUserID :many
|
||||
SELECT project_id FROM project_member WHERE user_id = $1
|
||||
`
|
||||
@ -296,6 +341,26 @@ func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Proj
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getProjectMemberInvitedIDByEmail = `-- name: GetProjectMemberInvitedIDByEmail :one
|
||||
SELECT email, invited_on, project_member_invited_id FROM user_account_invited AS uai
|
||||
inner join project_member_invited AS pmi
|
||||
ON pmi.user_account_invited_id = uai.user_account_invited_id
|
||||
WHERE email = $1
|
||||
`
|
||||
|
||||
type GetProjectMemberInvitedIDByEmailRow struct {
|
||||
Email string `json:"email"`
|
||||
InvitedOn time.Time `json:"invited_on"`
|
||||
ProjectMemberInvitedID uuid.UUID `json:"project_member_invited_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getProjectMemberInvitedIDByEmail, email)
|
||||
var i GetProjectMemberInvitedIDByEmailRow
|
||||
err := row.Scan(&i.Email, &i.InvitedOn, &i.ProjectMemberInvitedID)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getProjectMembersForProjectID = `-- name: GetProjectMembersForProjectID :many
|
||||
SELECT project_member_id, project_id, user_id, added_at, role_code FROM project_member WHERE project_id = $1
|
||||
`
|
||||
|
@ -9,6 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
||||
CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error)
|
||||
CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
|
||||
CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
|
||||
CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error)
|
||||
CreateNotificationObject(ctx context.Context, arg CreateNotificationObjectParams) (NotificationObject, error)
|
||||
@ -30,10 +33,14 @@ type Querier interface {
|
||||
CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
|
||||
CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
|
||||
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
|
||||
DeleteConfirmTokenForEmail(ctx context.Context, email string) error
|
||||
DeleteExpiredTokens(ctx context.Context) error
|
||||
DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error
|
||||
DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error)
|
||||
DeleteProjectByID(ctx context.Context, projectID uuid.UUID) error
|
||||
DeleteProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) error
|
||||
DeleteProjectMember(ctx context.Context, arg DeleteProjectMemberParams) error
|
||||
DeleteProjectMemberInvitedForEmail(ctx context.Context, email string) error
|
||||
DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error
|
||||
DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error)
|
||||
@ -47,20 +54,27 @@ type Querier interface {
|
||||
DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
|
||||
DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
|
||||
DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
|
||||
GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
|
||||
GetAllOrganizations(ctx context.Context) ([]Organization, error)
|
||||
GetAllProjects(ctx context.Context) ([]Project, error)
|
||||
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
|
||||
GetAllTaskGroups(ctx context.Context) ([]TaskGroup, error)
|
||||
GetAllTasks(ctx context.Context) ([]Task, error)
|
||||
GetAllTeamProjects(ctx context.Context) ([]Project, error)
|
||||
GetAllTeams(ctx context.Context) ([]Team, error)
|
||||
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
||||
GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
|
||||
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
|
||||
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
||||
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
|
||||
GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error)
|
||||
GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error)
|
||||
GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error)
|
||||
GetInvitedUserAccounts(ctx context.Context) ([]UserAccountInvited, error)
|
||||
GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error)
|
||||
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
|
||||
GetLabelColors(ctx context.Context) ([]LabelColor, error)
|
||||
GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error)
|
||||
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||
GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error)
|
||||
@ -72,8 +86,10 @@ type Querier interface {
|
||||
GetProjectIDForTaskGroup(ctx context.Context, taskGroupID uuid.UUID) (uuid.UUID, error)
|
||||
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
|
||||
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
|
||||
GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error)
|
||||
GetProjectMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectMember, error)
|
||||
GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error)
|
||||
GetProjectsForInvitedMember(ctx context.Context, email string) ([]uuid.UUID, error)
|
||||
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
|
||||
GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
|
||||
GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (Role, error)
|
||||
@ -97,12 +113,17 @@ type Querier interface {
|
||||
GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error)
|
||||
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
|
||||
GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error)
|
||||
GetUserAccountByEmail(ctx context.Context, email string) (UserAccount, error)
|
||||
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
|
||||
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
|
||||
GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error)
|
||||
HasActiveUser(ctx context.Context) (bool, error)
|
||||
HasAnyUser(ctx context.Context) (bool, error)
|
||||
SetFirstUserActive(ctx context.Context) (UserAccount, error)
|
||||
SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
|
||||
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
|
||||
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
|
||||
SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error)
|
||||
SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
|
||||
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
|
||||
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
|
||||
|
@ -43,6 +43,21 @@ SELECT project_id, role_code FROM project_member WHERE user_id = $1;
|
||||
-- name: GetMemberProjectIDsForUserID :many
|
||||
SELECT project_id FROM project_member WHERE user_id = $1;
|
||||
|
||||
-- name: GetInvitedMembersForProjectID :many
|
||||
SELECT uai.user_account_invited_id, email, invited_on FROM project_member_invited AS pmi
|
||||
INNER JOIN user_account_invited AS uai
|
||||
ON uai.user_account_invited_id = pmi.user_account_invited_id
|
||||
WHERE project_id = $1;
|
||||
|
||||
-- name: GetProjectMemberInvitedIDByEmail :one
|
||||
SELECT email, invited_on, project_member_invited_id FROM user_account_invited AS uai
|
||||
inner join project_member_invited AS pmi
|
||||
ON pmi.user_account_invited_id = uai.user_account_invited_id
|
||||
WHERE email = $1;
|
||||
|
||||
-- name: DeleteInvitedProjectMemberByID :exec
|
||||
DELETE FROM project_member_invited WHERE project_member_invited_id = $1;
|
||||
|
||||
-- name: GetAllVisibleProjectsForUserID :many
|
||||
SELECT project.* FROM project LEFT JOIN
|
||||
project_member ON project_member.project_id = project.project_id WHERE project_member.user_id = $1;
|
||||
|
@ -7,14 +7,22 @@ SELECT * FROM user_account WHERE username != 'system';
|
||||
-- name: GetUserAccountByUsername :one
|
||||
SELECT * FROM user_account WHERE username = $1;
|
||||
|
||||
-- name: GetUserAccountByEmail :one
|
||||
SELECT * FROM user_account WHERE email = $1;
|
||||
|
||||
-- name: CreateUserAccount :one
|
||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *;
|
||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;
|
||||
|
||||
-- name: UpdateUserAccountProfileAvatarURL :one
|
||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetMemberData :many
|
||||
SELECT * FROM user_account
|
||||
WHERE username != 'system'
|
||||
AND user_id NOT IN (SELECT user_id FROM project_member WHERE project_id = $1);
|
||||
|
||||
-- name: UpdateUserAccountInfo :one
|
||||
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
|
||||
WHERE user_id = $1 RETURNING *;
|
||||
@ -32,3 +40,64 @@ UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING *;
|
||||
|
||||
-- name: SetUserPassword :one
|
||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING *;
|
||||
|
||||
-- name: CreateInvitedUser :one
|
||||
INSERT INTO user_account_invited (email) VALUES ($1) RETURNING *;
|
||||
|
||||
-- name: GetInvitedUserByEmail :one
|
||||
SELECT * FROM user_account_invited WHERE email = $1;
|
||||
|
||||
-- name: CreateInvitedProjectMember :one
|
||||
INSERT INTO project_member_invited (project_id, user_account_invited_id) VALUES ($1, $2)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetInvitedUserAccounts :many
|
||||
SELECT * FROM user_account_invited;
|
||||
|
||||
-- name: DeleteInvitedUserAccount :one
|
||||
DELETE FROM user_account_invited WHERE user_account_invited_id = $1 RETURNING *;
|
||||
|
||||
-- name: HasAnyUser :one
|
||||
SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system');
|
||||
|
||||
-- name: HasActiveUser :one
|
||||
SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true);
|
||||
|
||||
-- name: CreateConfirmToken :one
|
||||
INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING *;
|
||||
|
||||
-- name: GetConfirmTokenByEmail :one
|
||||
SELECT * FROM user_account_confirm_token WHERE email = $1;
|
||||
|
||||
-- name: GetConfirmTokenByID :one
|
||||
SELECT * FROM user_account_confirm_token WHERE confirm_token_id = $1;
|
||||
|
||||
-- name: SetFirstUserActive :one
|
||||
UPDATE user_account SET active = true WHERE user_id = (
|
||||
SELECT user_id from user_account WHERE active = false LIMIT 1
|
||||
) RETURNING *;
|
||||
|
||||
-- name: SetUserActiveByEmail :one
|
||||
UPDATE user_account SET active = true WHERE email = $1 RETURNING *;
|
||||
|
||||
-- name: GetProjectsForInvitedMember :many
|
||||
SELECT project_id FROM user_account_invited AS uai
|
||||
INNER JOIN project_member_invited AS pmi
|
||||
ON pmi.user_account_invited_id = uai.user_account_invited_id
|
||||
WHERE uai.email = $1;
|
||||
|
||||
-- name: DeleteProjectMemberInvitedForEmail :exec
|
||||
DELETE FROM project_member_invited WHERE project_member_invited_id IN (
|
||||
SELECT pmi.project_member_invited_id FROM user_account_invited AS uai
|
||||
INNER JOIN project_member_invited AS pmi
|
||||
ON pmi.user_account_invited_id = uai.user_account_invited_id
|
||||
WHERE uai.email = $1
|
||||
);
|
||||
|
||||
-- name: DeleteUserAccountInvitedForEmail :exec
|
||||
DELETE FROM user_account_invited WHERE email = $1;
|
||||
|
||||
-- name: DeleteConfirmTokenForEmail :exec
|
||||
DELETE FROM user_account_confirm_token WHERE email = $1;
|
||||
|
||||
|
||||
|
@ -11,9 +11,53 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createConfirmToken = `-- name: CreateConfirmToken :one
|
||||
INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING confirm_token_id, email
|
||||
`
|
||||
|
||||
func (q *Queries) CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error) {
|
||||
row := q.db.QueryRowContext(ctx, createConfirmToken, email)
|
||||
var i UserAccountConfirmToken
|
||||
err := row.Scan(&i.ConfirmTokenID, &i.Email)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createInvitedProjectMember = `-- name: CreateInvitedProjectMember :one
|
||||
INSERT INTO project_member_invited (project_id, user_account_invited_id) VALUES ($1, $2)
|
||||
RETURNING project_member_invited_id, project_id, user_account_invited_id
|
||||
`
|
||||
|
||||
type CreateInvitedProjectMemberParams struct {
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
UserAccountInvitedID uuid.UUID `json:"user_account_invited_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error) {
|
||||
row := q.db.QueryRowContext(ctx, createInvitedProjectMember, arg.ProjectID, arg.UserAccountInvitedID)
|
||||
var i ProjectMemberInvited
|
||||
err := row.Scan(&i.ProjectMemberInvitedID, &i.ProjectID, &i.UserAccountInvitedID)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createInvitedUser = `-- name: CreateInvitedUser :one
|
||||
INSERT INTO user_account_invited (email) VALUES ($1) RETURNING user_account_invited_id, email, invited_on, has_joined
|
||||
`
|
||||
|
||||
func (q *Queries) CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error) {
|
||||
row := q.db.QueryRowContext(ctx, createInvitedUser, email)
|
||||
var i UserAccountInvited
|
||||
err := row.Scan(
|
||||
&i.UserAccountInvitedID,
|
||||
&i.Email,
|
||||
&i.InvitedOn,
|
||||
&i.HasJoined,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createUserAccount = `-- name: CreateUserAccount :one
|
||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
|
||||
`
|
||||
|
||||
type CreateUserAccountParams struct {
|
||||
@ -24,6 +68,7 @@ type CreateUserAccountParams struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
RoleCode string `json:"role_code"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) {
|
||||
@ -35,6 +80,7 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
|
||||
arg.CreatedAt,
|
||||
arg.PasswordHash,
|
||||
arg.RoleCode,
|
||||
arg.Active,
|
||||
)
|
||||
var i UserAccount
|
||||
err := row.Scan(
|
||||
@ -49,10 +95,50 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteConfirmTokenForEmail = `-- name: DeleteConfirmTokenForEmail :exec
|
||||
DELETE FROM user_account_confirm_token WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteConfirmTokenForEmail(ctx context.Context, email string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteConfirmTokenForEmail, email)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteInvitedUserAccount = `-- name: DeleteInvitedUserAccount :one
|
||||
DELETE FROM user_account_invited WHERE user_account_invited_id = $1 RETURNING user_account_invited_id, email, invited_on, has_joined
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error) {
|
||||
row := q.db.QueryRowContext(ctx, deleteInvitedUserAccount, userAccountInvitedID)
|
||||
var i UserAccountInvited
|
||||
err := row.Scan(
|
||||
&i.UserAccountInvitedID,
|
||||
&i.Email,
|
||||
&i.InvitedOn,
|
||||
&i.HasJoined,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteProjectMemberInvitedForEmail = `-- name: DeleteProjectMemberInvitedForEmail :exec
|
||||
DELETE FROM project_member_invited WHERE project_member_invited_id IN (
|
||||
SELECT pmi.project_member_invited_id FROM user_account_invited AS uai
|
||||
INNER JOIN project_member_invited AS pmi
|
||||
ON pmi.user_account_invited_id = uai.user_account_invited_id
|
||||
WHERE uai.email = $1
|
||||
)
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteProjectMemberInvitedForEmail(ctx context.Context, email string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteProjectMemberInvitedForEmail, email)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteUserAccountByID = `-- name: DeleteUserAccountByID :exec
|
||||
DELETE FROM user_account WHERE user_id = $1
|
||||
`
|
||||
@ -62,8 +148,17 @@ func (q *Queries) DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) e
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteUserAccountInvitedForEmail = `-- name: DeleteUserAccountInvitedForEmail :exec
|
||||
DELETE FROM user_account_invited WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteUserAccountInvitedForEmail, email)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username != 'system'
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username != 'system'
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
|
||||
@ -87,6 +182,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -101,6 +197,148 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getConfirmTokenByEmail = `-- name: GetConfirmTokenByEmail :one
|
||||
SELECT confirm_token_id, email FROM user_account_confirm_token WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error) {
|
||||
row := q.db.QueryRowContext(ctx, getConfirmTokenByEmail, email)
|
||||
var i UserAccountConfirmToken
|
||||
err := row.Scan(&i.ConfirmTokenID, &i.Email)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getConfirmTokenByID = `-- name: GetConfirmTokenByID :one
|
||||
SELECT confirm_token_id, email FROM user_account_confirm_token WHERE confirm_token_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error) {
|
||||
row := q.db.QueryRowContext(ctx, getConfirmTokenByID, confirmTokenID)
|
||||
var i UserAccountConfirmToken
|
||||
err := row.Scan(&i.ConfirmTokenID, &i.Email)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getInvitedUserAccounts = `-- name: GetInvitedUserAccounts :many
|
||||
SELECT user_account_invited_id, email, invited_on, has_joined FROM user_account_invited
|
||||
`
|
||||
|
||||
func (q *Queries) GetInvitedUserAccounts(ctx context.Context) ([]UserAccountInvited, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getInvitedUserAccounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []UserAccountInvited
|
||||
for rows.Next() {
|
||||
var i UserAccountInvited
|
||||
if err := rows.Scan(
|
||||
&i.UserAccountInvitedID,
|
||||
&i.Email,
|
||||
&i.InvitedOn,
|
||||
&i.HasJoined,
|
||||
); 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 getInvitedUserByEmail = `-- name: GetInvitedUserByEmail :one
|
||||
SELECT user_account_invited_id, email, invited_on, has_joined FROM user_account_invited WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error) {
|
||||
row := q.db.QueryRowContext(ctx, getInvitedUserByEmail, email)
|
||||
var i UserAccountInvited
|
||||
err := row.Scan(
|
||||
&i.UserAccountInvitedID,
|
||||
&i.Email,
|
||||
&i.InvitedOn,
|
||||
&i.HasJoined,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMemberData = `-- name: GetMemberData :many
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account
|
||||
WHERE username != 'system'
|
||||
AND user_id NOT IN (SELECT user_id FROM project_member WHERE project_id = $1)
|
||||
`
|
||||
|
||||
func (q *Queries) GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getMemberData, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []UserAccount
|
||||
for rows.Next() {
|
||||
var i UserAccount
|
||||
if err := rows.Scan(
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
); 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 getProjectsForInvitedMember = `-- name: GetProjectsForInvitedMember :many
|
||||
SELECT project_id FROM user_account_invited AS uai
|
||||
INNER JOIN project_member_invited AS pmi
|
||||
ON pmi.user_account_invited_id = uai.user_account_invited_id
|
||||
WHERE uai.email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetProjectsForInvitedMember(ctx context.Context, email string) ([]uuid.UUID, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getProjectsForInvitedMember, email)
|
||||
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 getRoleForUserID = `-- name: GetRoleForUserID :one
|
||||
SELECT username, role.code, role.name FROM user_account
|
||||
INNER JOIN role ON role.code = user_account.role_code
|
||||
@ -120,8 +358,32 @@ func (q *Queries) GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRo
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserAccountByEmail = `-- name: GetUserAccountByEmail :one
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserAccountByEmail(ctx context.Context, email string) (UserAccount, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserAccountByEmail, email)
|
||||
var i UserAccount
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserAccountByID = `-- name: GetUserAccountByID :one
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE user_id = $1
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE user_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
|
||||
@ -139,12 +401,13 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username = $1
|
||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
|
||||
@ -162,12 +425,85 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const hasActiveUser = `-- name: HasActiveUser :one
|
||||
SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true)
|
||||
`
|
||||
|
||||
func (q *Queries) HasActiveUser(ctx context.Context) (bool, error) {
|
||||
row := q.db.QueryRowContext(ctx, hasActiveUser)
|
||||
var exists bool
|
||||
err := row.Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
const hasAnyUser = `-- name: HasAnyUser :one
|
||||
SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system')
|
||||
`
|
||||
|
||||
func (q *Queries) HasAnyUser(ctx context.Context) (bool, error) {
|
||||
row := q.db.QueryRowContext(ctx, hasAnyUser)
|
||||
var exists bool
|
||||
err := row.Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
const setFirstUserActive = `-- name: SetFirstUserActive :one
|
||||
UPDATE user_account SET active = true WHERE user_id = (
|
||||
SELECT user_id from user_account WHERE active = false LIMIT 1
|
||||
) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
|
||||
`
|
||||
|
||||
func (q *Queries) SetFirstUserActive(ctx context.Context) (UserAccount, error) {
|
||||
row := q.db.QueryRowContext(ctx, setFirstUserActive)
|
||||
var i UserAccount
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const setUserActiveByEmail = `-- name: SetUserActiveByEmail :one
|
||||
UPDATE user_account SET active = true WHERE email = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
|
||||
`
|
||||
|
||||
func (q *Queries) SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error) {
|
||||
row := q.db.QueryRowContext(ctx, setUserActiveByEmail, email)
|
||||
var i UserAccount
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const setUserPassword = `-- name: SetUserPassword :one
|
||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
|
||||
`
|
||||
|
||||
type SetUserPasswordParams struct {
|
||||
@ -190,13 +526,14 @@ func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserAccountInfo = `-- name: UpdateUserAccountInfo :one
|
||||
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
|
||||
WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||
WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
|
||||
`
|
||||
|
||||
type UpdateUserAccountInfoParams struct {
|
||||
@ -228,13 +565,14 @@ func (q *Queries) UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccou
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
|
||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
||||
RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||
RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
|
||||
`
|
||||
|
||||
type UpdateUserAccountProfileAvatarURLParams struct {
|
||||
@ -257,12 +595,13 @@ func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg Upd
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserRole = `-- name: UpdateUserRole :one
|
||||
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
|
||||
`
|
||||
|
||||
type UpdateUserRoleParams struct {
|
||||
@ -285,6 +624,7 @@ func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams)
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/taskcafe/internal/auth"
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
"github.com/jordanknott/taskcafe/internal/logger"
|
||||
"github.com/jordanknott/taskcafe/internal/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
@ -63,10 +64,10 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
default:
|
||||
fieldName = "ProjectID"
|
||||
}
|
||||
log.WithFields(log.Fields{"typeArg": typeArg, "fieldName": fieldName}).Info("getting field by name")
|
||||
logger.New(ctx).WithFields(log.Fields{"typeArg": typeArg, "fieldName": fieldName}).Info("getting field by name")
|
||||
subjectField := val.FieldByName(fieldName)
|
||||
if !subjectField.IsValid() {
|
||||
log.Error("subject field name does not exist on input type")
|
||||
logger.New(ctx).Error("subject field name does not exist on input type")
|
||||
return nil, errors.New("subject field name does not exist on input type")
|
||||
}
|
||||
if fieldName == "TeamID" && subjectField.IsNil() {
|
||||
@ -76,13 +77,13 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
}
|
||||
subjectID, ok = subjectField.Interface().(uuid.UUID)
|
||||
if !ok {
|
||||
log.Error("error while casting subject UUID")
|
||||
logger.New(ctx).Error("error while casting subject UUID")
|
||||
return nil, errors.New("error while casting subject uuid")
|
||||
}
|
||||
|
||||
var err error
|
||||
if level == ActionLevelProject {
|
||||
log.WithFields(log.Fields{"subjectID": subjectID}).Info("fetching subject ID by typeArg")
|
||||
logger.New(ctx).WithFields(log.Fields{"subjectID": subjectID}).Info("fetching subject ID by typeArg")
|
||||
if typeArg == ObjectTypeTask {
|
||||
subjectID, err = repo.GetProjectIDForTask(ctx, subjectID)
|
||||
}
|
||||
@ -96,7 +97,7 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
subjectID, err = repo.GetProjectIDForTaskChecklistItem(ctx, subjectID)
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting subject ID")
|
||||
logger.New(ctx).WithError(err).Error("error while getting subject ID")
|
||||
return nil, err
|
||||
}
|
||||
projectRoles, err := GetProjectRoles(ctx, repo, subjectID)
|
||||
@ -109,13 +110,13 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
},
|
||||
}
|
||||
}
|
||||
log.WithError(err).Error("error while getting project roles")
|
||||
logger.New(ctx).WithError(err).Error("error while getting project roles")
|
||||
return nil, err
|
||||
}
|
||||
for _, validRole := range roles {
|
||||
log.WithFields(log.Fields{"validRole": validRole}).Info("checking role")
|
||||
logger.New(ctx).WithFields(log.Fields{"validRole": validRole}).Info("checking role")
|
||||
if CompareRoleLevel(projectRoles.TeamRole, validRole) || CompareRoleLevel(projectRoles.ProjectRole, validRole) {
|
||||
log.WithFields(log.Fields{"teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("is team or project role")
|
||||
logger.New(ctx).WithFields(log.Fields{"teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("is team or project role")
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
@ -132,7 +133,7 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
}
|
||||
role, err := repo.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: subjectID})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting team roles for user ID")
|
||||
logger.New(ctx).WithError(err).Error("error while getting team roles for user ID")
|
||||
return nil, err
|
||||
}
|
||||
for _, validRole := range roles {
|
||||
|
@ -27,16 +27,6 @@ type ChecklistBadge struct {
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type CreateProjectMember struct {
|
||||
ProjectID uuid.UUID `json:"projectID"`
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
}
|
||||
|
||||
type CreateProjectMemberPayload struct {
|
||||
Ok bool `json:"ok"`
|
||||
Member *Member `json:"member"`
|
||||
}
|
||||
|
||||
type CreateTaskChecklist struct {
|
||||
TaskID uuid.UUID `json:"taskID"`
|
||||
Name string `json:"name"`
|
||||
@ -59,6 +49,23 @@ type CreateTeamMemberPayload struct {
|
||||
TeamMember *Member `json:"teamMember"`
|
||||
}
|
||||
|
||||
type DeleteInvitedProjectMember struct {
|
||||
ProjectID uuid.UUID `json:"projectID"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type DeleteInvitedProjectMemberPayload struct {
|
||||
InvitedMember *InvitedMember `json:"invitedMember"`
|
||||
}
|
||||
|
||||
type DeleteInvitedUserAccount struct {
|
||||
InvitedUserID uuid.UUID `json:"invitedUserID"`
|
||||
}
|
||||
|
||||
type DeleteInvitedUserAccountPayload struct {
|
||||
InvitedUser *InvitedUserAccount `json:"invitedUser"`
|
||||
}
|
||||
|
||||
type DeleteProject struct {
|
||||
ProjectID uuid.UUID `json:"projectID"`
|
||||
}
|
||||
@ -187,6 +194,30 @@ type FindUser struct {
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
}
|
||||
|
||||
type InviteProjectMembers struct {
|
||||
ProjectID uuid.UUID `json:"projectID"`
|
||||
Members []MemberInvite `json:"members"`
|
||||
}
|
||||
|
||||
type InviteProjectMembersPayload struct {
|
||||
Ok bool `json:"ok"`
|
||||
ProjectID uuid.UUID `json:"projectID"`
|
||||
Members []Member `json:"members"`
|
||||
InvitedMembers []InvitedMember `json:"invitedMembers"`
|
||||
}
|
||||
|
||||
type InvitedMember struct {
|
||||
Email string `json:"email"`
|
||||
InvitedOn time.Time `json:"invitedOn"`
|
||||
}
|
||||
|
||||
type InvitedUserAccount struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
InvitedOn time.Time `json:"invitedOn"`
|
||||
Member *MemberList `json:"member"`
|
||||
}
|
||||
|
||||
type LogoutUser struct {
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
}
|
||||
@ -207,11 +238,28 @@ type Member struct {
|
||||
Member *MemberList `json:"member"`
|
||||
}
|
||||
|
||||
type MemberInvite struct {
|
||||
UserID *uuid.UUID `json:"userID"`
|
||||
Email *string `json:"email"`
|
||||
}
|
||||
|
||||
type MemberList struct {
|
||||
Teams []db.Team `json:"teams"`
|
||||
Projects []db.Project `json:"projects"`
|
||||
}
|
||||
|
||||
type MemberSearchFilter struct {
|
||||
SearchFilter string `json:"searchFilter"`
|
||||
ProjectID *uuid.UUID `json:"projectID"`
|
||||
}
|
||||
|
||||
type MemberSearchResult struct {
|
||||
Similarity int `json:"similarity"`
|
||||
ID string `json:"id"`
|
||||
User *db.UserAccount `json:"user"`
|
||||
Status ShareStatus `json:"status"`
|
||||
}
|
||||
|
||||
type NewProject struct {
|
||||
TeamID *uuid.UUID `json:"teamID"`
|
||||
Name string `json:"name"`
|
||||
@ -781,3 +829,44 @@ func (e *RoleLevel) UnmarshalGQL(v interface{}) error {
|
||||
func (e RoleLevel) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type ShareStatus string
|
||||
|
||||
const (
|
||||
ShareStatusInvited ShareStatus = "INVITED"
|
||||
ShareStatusJoined ShareStatus = "JOINED"
|
||||
)
|
||||
|
||||
var AllShareStatus = []ShareStatus{
|
||||
ShareStatusInvited,
|
||||
ShareStatusJoined,
|
||||
}
|
||||
|
||||
func (e ShareStatus) IsValid() bool {
|
||||
switch e {
|
||||
case ShareStatusInvited, ShareStatusJoined:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e ShareStatus) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *ShareStatus) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = ShareStatus(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid ShareStatus", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ShareStatus) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
@ -86,6 +86,13 @@ type UserAccount {
|
||||
member: MemberList!
|
||||
}
|
||||
|
||||
type InvitedUserAccount {
|
||||
id: ID!
|
||||
email: String!
|
||||
invitedOn: Time!
|
||||
member: MemberList!
|
||||
}
|
||||
|
||||
type Team {
|
||||
id: ID!
|
||||
createdAt: Time!
|
||||
@ -93,6 +100,12 @@ type Team {
|
||||
members: [Member!]!
|
||||
}
|
||||
|
||||
|
||||
type InvitedMember {
|
||||
email: String!
|
||||
invitedOn: Time!
|
||||
}
|
||||
|
||||
type Project {
|
||||
id: ID!
|
||||
createdAt: Time!
|
||||
@ -100,6 +113,7 @@ type Project {
|
||||
team: Team
|
||||
taskGroups: [TaskGroup!]!
|
||||
members: [Member!]!
|
||||
invitedMembers: [InvitedMember!]!
|
||||
labels: [ProjectLabel!]!
|
||||
}
|
||||
|
||||
@ -158,6 +172,11 @@ type TaskChecklist {
|
||||
items: [TaskChecklistItem!]!
|
||||
}
|
||||
|
||||
enum ShareStatus {
|
||||
INVITED
|
||||
JOINED
|
||||
}
|
||||
|
||||
enum RoleLevel {
|
||||
ADMIN
|
||||
MEMBER
|
||||
@ -184,6 +203,7 @@ directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!)
|
||||
type Query {
|
||||
organizations: [Organization!]!
|
||||
users: [UserAccount!]!
|
||||
invitedUsers: [InvitedUserAccount!]!
|
||||
findUser(input: FindUser!): UserAccount!
|
||||
findProject(input: FindProject!):
|
||||
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||
@ -338,22 +358,41 @@ input UpdateProjectLabelColor {
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createProjectMember(input: CreateProjectMember!):
|
||||
CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
inviteProjectMembers(input: InviteProjectMembers!):
|
||||
InviteProjectMembersPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
deleteProjectMember(input: DeleteProjectMember!):
|
||||
DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
updateProjectMemberRole(input: UpdateProjectMemberRole!):
|
||||
UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
|
||||
deleteInvitedProjectMember(input: DeleteInvitedProjectMember!):
|
||||
DeleteInvitedProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
}
|
||||
|
||||
input CreateProjectMember {
|
||||
input DeleteInvitedProjectMember {
|
||||
projectID: UUID!
|
||||
userID: UUID!
|
||||
email: String!
|
||||
}
|
||||
|
||||
type CreateProjectMemberPayload {
|
||||
type DeleteInvitedProjectMemberPayload {
|
||||
invitedMember: InvitedMember!
|
||||
}
|
||||
|
||||
input MemberInvite {
|
||||
userID: UUID
|
||||
email: String
|
||||
}
|
||||
|
||||
input InviteProjectMembers {
|
||||
projectID: UUID!
|
||||
members: [MemberInvite!]!
|
||||
}
|
||||
|
||||
type InviteProjectMembersPayload {
|
||||
ok: Boolean!
|
||||
member: Member!
|
||||
projectID: UUID!
|
||||
members: [Member!]!
|
||||
invitedMembers: [InvitedMember!]!
|
||||
}
|
||||
|
||||
input DeleteProjectMember {
|
||||
@ -723,6 +762,8 @@ extend type Mutation {
|
||||
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
deleteUserAccount(input: DeleteUserAccount!):
|
||||
DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
deleteInvitedUserAccount(input: DeleteInvitedUserAccount!):
|
||||
DeleteInvitedUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
|
||||
logoutUser(input: LogoutUser!): Boolean!
|
||||
clearProfileAvatar: UserAccount!
|
||||
@ -734,6 +775,31 @@ extend type Mutation {
|
||||
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
searchMembers(input: MemberSearchFilter!): [MemberSearchResult!]!
|
||||
}
|
||||
|
||||
input DeleteInvitedUserAccount {
|
||||
invitedUserID: UUID!
|
||||
}
|
||||
|
||||
type DeleteInvitedUserAccountPayload {
|
||||
invitedUser: InvitedUserAccount!
|
||||
}
|
||||
|
||||
input MemberSearchFilter {
|
||||
searchFilter: String!
|
||||
projectID: UUID
|
||||
}
|
||||
|
||||
|
||||
type MemberSearchResult {
|
||||
similarity: Int!
|
||||
id: String!
|
||||
user: UserAccount
|
||||
status: ShareStatus!
|
||||
}
|
||||
|
||||
type UpdateUserInfoPayload {
|
||||
user: UserAccount!
|
||||
}
|
||||
@ -790,3 +856,4 @@ type DeleteUserAccountPayload {
|
||||
ok: Boolean!
|
||||
userAccount: UserAccount!
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,9 @@ package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@ -13,6 +15,11 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/taskcafe/internal/auth"
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
"github.com/jordanknott/taskcafe/internal/logger"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
|
||||
hermes "github.com/matcornic/hermes/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@ -28,7 +35,7 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject)
|
||||
return &db.Project{}, errors.New("user id is missing")
|
||||
}
|
||||
createdAt := time.Now().UTC()
|
||||
log.WithFields(log.Fields{"name": input.Name, "teamID": input.TeamID}).Info("creating new project")
|
||||
logger.New(ctx).WithFields(log.Fields{"name": input.Name, "teamID": input.TeamID}).Info("creating new project")
|
||||
var project db.Project
|
||||
var err error
|
||||
if input.TeamID == nil {
|
||||
@ -37,10 +44,10 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject)
|
||||
Name: input.Name,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating project")
|
||||
logger.New(ctx).WithError(err).Error("error while creating project")
|
||||
return &db.Project{}, err
|
||||
}
|
||||
log.WithFields(log.Fields{"userID": userID, "projectID": project.ProjectID}).Info("creating personal project link")
|
||||
logger.New(ctx).WithField("projectID", project.ProjectID).Info("creating personal project link")
|
||||
} else {
|
||||
project, err = r.Repository.CreateTeamProject(ctx, db.CreateTeamProjectParams{
|
||||
CreatedAt: createdAt,
|
||||
@ -48,13 +55,13 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject)
|
||||
TeamID: *input.TeamID,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating project")
|
||||
logger.New(ctx).WithError(err).Error("error while creating project")
|
||||
return &db.Project{}, err
|
||||
}
|
||||
}
|
||||
_, err = r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: project.ProjectID, UserID: userID, AddedAt: createdAt, RoleCode: "admin"})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating initial project member")
|
||||
logger.New(ctx).WithError(err).Error("error while creating initial project member")
|
||||
return &db.Project{}, err
|
||||
}
|
||||
return &project, nil
|
||||
@ -123,33 +130,162 @@ func (r *mutationResolver) UpdateProjectLabelColor(ctx context.Context, input Up
|
||||
return &label, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateProjectMember(ctx context.Context, input CreateProjectMember) (*CreateProjectMemberPayload, error) {
|
||||
addedAt := time.Now().UTC()
|
||||
_, err := r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID, UserID: input.UserID, AddedAt: addedAt, RoleCode: "member"})
|
||||
if err != nil {
|
||||
return &CreateProjectMemberPayload{Ok: false}, err
|
||||
}
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
|
||||
if err != nil {
|
||||
return &CreateProjectMemberPayload{Ok: false}, err
|
||||
}
|
||||
var url *string
|
||||
if user.ProfileAvatarUrl.Valid {
|
||||
url = &user.ProfileAvatarUrl.String
|
||||
}
|
||||
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
|
||||
func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input InviteProjectMembers) (*InviteProjectMembersPayload, error) {
|
||||
members := []Member{}
|
||||
invitedMembers := []InvitedMember{}
|
||||
for _, invitedMember := range input.Members {
|
||||
if invitedMember.Email != nil && invitedMember.UserID != nil {
|
||||
return &InviteProjectMembersPayload{Ok: false}, &gqlerror.Error{
|
||||
Message: "Both email and userID can not be used to invite a project member",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "403",
|
||||
},
|
||||
}
|
||||
} else if invitedMember.Email == nil && invitedMember.UserID == nil {
|
||||
return &InviteProjectMembersPayload{Ok: false}, &gqlerror.Error{
|
||||
Message: "Either email or userID must be set to invite a project member",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "403",
|
||||
},
|
||||
}
|
||||
}
|
||||
if invitedMember.UserID != nil {
|
||||
// Invite by user ID
|
||||
addedAt := time.Now().UTC()
|
||||
_, err := r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID, UserID: *invitedMember.UserID, AddedAt: addedAt, RoleCode: "member"})
|
||||
if err != nil {
|
||||
return &InviteProjectMembersPayload{Ok: false}, err
|
||||
}
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, *invitedMember.UserID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return &InviteProjectMembersPayload{Ok: false}, err
|
||||
|
||||
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: input.UserID, ProjectID: input.ProjectID})
|
||||
if err != nil {
|
||||
return &CreateProjectMemberPayload{Ok: false}, err
|
||||
}
|
||||
var url *string
|
||||
if user.ProfileAvatarUrl.Valid {
|
||||
url = &user.ProfileAvatarUrl.String
|
||||
}
|
||||
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
|
||||
|
||||
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: *invitedMember.UserID, ProjectID: input.ProjectID})
|
||||
if err != nil {
|
||||
return &InviteProjectMembersPayload{Ok: false}, err
|
||||
}
|
||||
members = append(members, Member{
|
||||
ID: *invitedMember.UserID,
|
||||
FullName: user.FullName,
|
||||
Username: user.Username,
|
||||
ProfileIcon: profileIcon,
|
||||
Role: &db.Role{Code: role.Code, Name: role.Name},
|
||||
})
|
||||
} else {
|
||||
// Invite by email
|
||||
|
||||
// if invited user does not exist, create entry
|
||||
invitedUser, err := r.Repository.GetInvitedUserByEmail(ctx, *invitedMember.Email)
|
||||
now := time.Now().UTC()
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
invitedUser, err = r.Repository.CreateInvitedUser(ctx, *invitedMember.Email)
|
||||
if err != nil {
|
||||
return &InviteProjectMembersPayload{Ok: false}, err
|
||||
}
|
||||
confirmToken, err := r.Repository.CreateConfirmToken(ctx, *invitedMember.Email)
|
||||
if err != nil {
|
||||
return &InviteProjectMembersPayload{Ok: false}, err
|
||||
}
|
||||
// send out invitation
|
||||
// add project invite entry
|
||||
// send out notification?
|
||||
h := hermes.Hermes{
|
||||
// Optional Theme
|
||||
Product: hermes.Product{
|
||||
// Appears in header & footer of e-mails
|
||||
Name: "Taskscafe",
|
||||
Link: "http://localhost:3333/",
|
||||
// Optional product logo
|
||||
Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png",
|
||||
},
|
||||
}
|
||||
|
||||
email := hermes.Email{
|
||||
Body: hermes.Body{
|
||||
Name: "Jordan Knott",
|
||||
Intros: []string{
|
||||
"You have been invited to join Taskcafe",
|
||||
},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "To get started with Taskcafe, please click here:",
|
||||
Button: hermes.Button{
|
||||
Color: "#7367F0", // Optional action button color
|
||||
TextColor: "#FFFFFF",
|
||||
Text: "Register your account",
|
||||
Link: "http://localhost:3000/register?confirmToken=" + confirmToken.ConfirmTokenID.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Outros: []string{
|
||||
"Need help, or have questions? Just reply to this email, we'd love to help.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Generate an HTML email with the provided contents (for modern clients)
|
||||
emailBody, err := h.GenerateHTML(email)
|
||||
if err != nil {
|
||||
panic(err) // Tip: Handle error with something else than a panic ;)
|
||||
}
|
||||
emailBodyPlain, err := h.GeneratePlainText(email)
|
||||
if err != nil {
|
||||
panic(err) // Tip: Handle error with something else than a panic ;)
|
||||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
|
||||
// Set E-Mail sender
|
||||
m.SetHeader("From", "no-reply@taskcafe.com")
|
||||
|
||||
// Set E-Mail receivers
|
||||
m.SetHeader("To", invitedUser.Email)
|
||||
|
||||
// Set E-Mail subject
|
||||
m.SetHeader("Subject", "You have been invited to Taskcafe")
|
||||
|
||||
// Set E-Mail body. You can set plain text or html with text/html
|
||||
m.SetBody("text/html", emailBody)
|
||||
m.AddAlternative("text/plain", emailBodyPlain)
|
||||
|
||||
// Settings for SMTP server
|
||||
d := gomail.NewDialer("127.0.0.1", 11500, "no-reply@taskcafe.com", "")
|
||||
|
||||
// This is only needed when SSL/TLS certificate is not valid on server.
|
||||
// In production this should be set to false.
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
// Now send E-Mail
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
fmt.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
return &InviteProjectMembersPayload{Ok: false}, err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = r.Repository.CreateInvitedProjectMember(ctx, db.CreateInvitedProjectMemberParams{
|
||||
ProjectID: input.ProjectID,
|
||||
UserAccountInvitedID: invitedUser.UserAccountInvitedID,
|
||||
})
|
||||
if err != nil {
|
||||
return &InviteProjectMembersPayload{Ok: false}, err
|
||||
}
|
||||
logger.New(ctx).Info("adding invited member")
|
||||
invitedMembers = append(invitedMembers, InvitedMember{Email: *invitedMember.Email, InvitedOn: now})
|
||||
|
||||
}
|
||||
}
|
||||
return &CreateProjectMemberPayload{Ok: true, Member: &Member{
|
||||
ID: input.UserID,
|
||||
FullName: user.FullName,
|
||||
Username: user.Username,
|
||||
ProfileIcon: profileIcon,
|
||||
Role: &db.Role{Code: role.Code, Name: role.Name},
|
||||
}}, nil
|
||||
return &InviteProjectMembersPayload{Ok: false, ProjectID: input.ProjectID, Members: members, InvitedMembers: invitedMembers}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) {
|
||||
@ -181,18 +317,18 @@ func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input Delete
|
||||
func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) {
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get user account")
|
||||
logger.New(ctx).WithError(err).Error("get user account")
|
||||
return &UpdateProjectMemberRolePayload{Ok: false}, err
|
||||
}
|
||||
_, err = r.Repository.UpdateProjectMemberRole(ctx, db.UpdateProjectMemberRoleParams{ProjectID: input.ProjectID,
|
||||
UserID: input.UserID, RoleCode: input.RoleCode.String()})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("update project member role")
|
||||
logger.New(ctx).WithError(err).Error("update project member role")
|
||||
return &UpdateProjectMemberRolePayload{Ok: false}, err
|
||||
}
|
||||
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: input.ProjectID})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get role for project member")
|
||||
logger.New(ctx).WithError(err).Error("get role for project member")
|
||||
return &UpdateProjectMemberRolePayload{Ok: false}, err
|
||||
}
|
||||
var url *string
|
||||
@ -209,19 +345,33 @@ func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input Up
|
||||
return &UpdateProjectMemberRolePayload{Ok: true, Member: &member}, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteInvitedProjectMember(ctx context.Context, input DeleteInvitedProjectMember) (*DeleteInvitedProjectMemberPayload, error) {
|
||||
member, err := r.Repository.GetProjectMemberInvitedIDByEmail(ctx, input.Email)
|
||||
if err != nil {
|
||||
return &DeleteInvitedProjectMemberPayload{}, err
|
||||
}
|
||||
err = r.Repository.DeleteInvitedProjectMemberByID(ctx, member.ProjectMemberInvitedID)
|
||||
if err != nil {
|
||||
return &DeleteInvitedProjectMemberPayload{}, err
|
||||
}
|
||||
return &DeleteInvitedProjectMemberPayload{
|
||||
InvitedMember: &InvitedMember{Email: member.Email, InvitedOn: member.InvitedOn},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
|
||||
createdAt := time.Now().UTC()
|
||||
log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task")
|
||||
logger.New(ctx).WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task")
|
||||
task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{input.TaskGroupID, createdAt, input.Name, input.Position})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue while creating task")
|
||||
logger.New(ctx).WithError(err).Error("issue while creating task")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) {
|
||||
log.WithFields(log.Fields{
|
||||
logger.New(ctx).WithFields(log.Fields{
|
||||
"taskID": input.TaskID,
|
||||
}).Info("deleting task")
|
||||
err := r.Repository.DeleteTaskByID(ctx, input.TaskID)
|
||||
@ -278,8 +428,8 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
|
||||
func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInput) (*db.Task, error) {
|
||||
assignedDate := time.Now().UTC()
|
||||
assignedTask, err := r.Repository.CreateTaskAssigned(ctx, db.CreateTaskAssignedParams{input.TaskID, input.UserID, assignedDate})
|
||||
log.WithFields(log.Fields{
|
||||
"userID": assignedTask.UserID,
|
||||
logger.New(ctx).WithFields(log.Fields{
|
||||
"assignedUserID": assignedTask.UserID,
|
||||
"taskID": assignedTask.TaskID,
|
||||
"assignedTaskID": assignedTask.TaskAssignedID,
|
||||
}).Info("assigned task")
|
||||
@ -589,7 +739,7 @@ func (r *mutationResolver) ToggleTaskLabel(ctx context.Context, input ToggleTask
|
||||
createdAt := time.Now().UTC()
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
log.WithFields(log.Fields{"err": err}).Warning("no rows")
|
||||
logger.New(ctx).WithFields(log.Fields{"err": err}).Warning("no rows")
|
||||
_, err := r.Repository.CreateTaskLabelForTask(ctx, db.CreateTaskLabelForTaskParams{
|
||||
TaskID: input.TaskID,
|
||||
ProjectLabelID: input.ProjectLabelID,
|
||||
@ -622,17 +772,17 @@ func (r *mutationResolver) ToggleTaskLabel(ctx context.Context, input ToggleTask
|
||||
func (r *mutationResolver) DeleteTeam(ctx context.Context, input DeleteTeam) (*DeleteTeamPayload, error) {
|
||||
team, err := r.Repository.GetTeamByID(ctx, input.TeamID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
logger.New(ctx).Error(err)
|
||||
return &DeleteTeamPayload{Ok: false}, err
|
||||
}
|
||||
projects, err := r.Repository.GetAllProjectsForTeam(ctx, input.TeamID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
logger.New(ctx).Error(err)
|
||||
return &DeleteTeamPayload{Ok: false}, err
|
||||
}
|
||||
err = r.Repository.DeleteTeamByID(ctx, input.TeamID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
logger.New(ctx).Error(err)
|
||||
return &DeleteTeamPayload{Ok: false}, err
|
||||
}
|
||||
|
||||
@ -687,18 +837,18 @@ func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTea
|
||||
func (r *mutationResolver) UpdateTeamMemberRole(ctx context.Context, input UpdateTeamMemberRole) (*UpdateTeamMemberRolePayload, error) {
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get user account")
|
||||
logger.New(ctx).WithError(err).Error("get user account")
|
||||
return &UpdateTeamMemberRolePayload{Ok: false}, err
|
||||
}
|
||||
_, err = r.Repository.UpdateTeamMemberRole(ctx, db.UpdateTeamMemberRoleParams{TeamID: input.TeamID,
|
||||
UserID: input.UserID, RoleCode: input.RoleCode.String()})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("update project member role")
|
||||
logger.New(ctx).WithError(err).Error("update project member role")
|
||||
return &UpdateTeamMemberRolePayload{Ok: false}, err
|
||||
}
|
||||
role, err := r.Repository.GetRoleForTeamMember(ctx, db.GetRoleForTeamMemberParams{UserID: user.UserID, TeamID: input.TeamID})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get role for project member")
|
||||
logger.New(ctx).WithError(err).Error("get role for project member")
|
||||
return &UpdateTeamMemberRolePayload{Ok: false}, err
|
||||
}
|
||||
var url *string
|
||||
@ -785,6 +935,25 @@ func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUs
|
||||
return &DeleteUserAccountPayload{UserAccount: &user, Ok: true}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteInvitedUserAccount(ctx context.Context, input DeleteInvitedUserAccount) (*DeleteInvitedUserAccountPayload, error) {
|
||||
user, err := r.Repository.DeleteInvitedUserAccount(ctx, input.InvitedUserID)
|
||||
if err != nil {
|
||||
return &DeleteInvitedUserAccountPayload{}, err
|
||||
}
|
||||
err = r.Repository.DeleteConfirmTokenForEmail(ctx, user.Email)
|
||||
if err != nil {
|
||||
logger.New(ctx).WithError(err).Error("issue deleting confirm token")
|
||||
return &DeleteInvitedUserAccountPayload{}, err
|
||||
}
|
||||
return &DeleteInvitedUserAccountPayload{
|
||||
InvitedUser: &InvitedUserAccount{
|
||||
Email: user.Email,
|
||||
ID: user.UserAccountInvitedID,
|
||||
InvitedOn: user.InvitedOn,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
|
||||
err := r.Repository.DeleteRefreshTokenByUserID(ctx, input.UserID)
|
||||
return true, err
|
||||
@ -850,9 +1019,9 @@ func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uu
|
||||
}
|
||||
|
||||
func (r *notificationResolver) Entity(ctx context.Context, obj *db.Notification) (*NotificationEntity, error) {
|
||||
log.WithFields(log.Fields{"notificationID": obj.NotificationID}).Info("fetching entity for notification")
|
||||
logger.New(ctx).WithFields(log.Fields{"notificationID": obj.NotificationID}).Info("fetching entity for notification")
|
||||
entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
|
||||
log.WithFields(log.Fields{"entityID": entity.EntityID}).Info("fetched entity")
|
||||
logger.New(ctx).WithFields(log.Fields{"entityID": entity.EntityID}).Info("fetched entity")
|
||||
if err != nil {
|
||||
return &NotificationEntity{}, err
|
||||
}
|
||||
@ -884,7 +1053,7 @@ func (r *notificationResolver) Actor(ctx context.Context, obj *db.Notification)
|
||||
if err != nil {
|
||||
return &NotificationActor{}, err
|
||||
}
|
||||
log.WithFields(log.Fields{"entityID": entity.ActorID}).Info("fetching actor")
|
||||
logger.New(ctx).WithFields(log.Fields{"entityID": entity.ActorID}).Info("fetching actor")
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, entity.ActorID)
|
||||
if err != nil {
|
||||
return &NotificationActor{}, err
|
||||
@ -914,7 +1083,7 @@ func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team,
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
log.WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
|
||||
logger.New(ctx).WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
|
||||
return &team, err
|
||||
}
|
||||
return &team, nil
|
||||
@ -928,14 +1097,14 @@ func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Membe
|
||||
members := []Member{}
|
||||
projectMembers, err := r.Repository.GetProjectMembersForProjectID(ctx, obj.ProjectID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get project members for project id")
|
||||
logger.New(ctx).WithError(err).Error("get project members for project id")
|
||||
return members, err
|
||||
}
|
||||
|
||||
for _, projectMember := range projectMembers {
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get user account by ID")
|
||||
logger.New(ctx).WithError(err).Error("get user account by ID")
|
||||
return members, err
|
||||
}
|
||||
var url *string
|
||||
@ -944,7 +1113,7 @@ func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Membe
|
||||
}
|
||||
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: obj.ProjectID})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get role for projet member by user ID")
|
||||
logger.New(ctx).WithError(err).Error("get role for projet member by user ID")
|
||||
return members, err
|
||||
}
|
||||
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
|
||||
@ -955,6 +1124,18 @@ func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Membe
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (r *projectResolver) InvitedMembers(ctx context.Context, obj *db.Project) ([]InvitedMember, error) {
|
||||
members, err := r.Repository.GetInvitedMembersForProjectID(ctx, obj.ProjectID)
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
return []InvitedMember{}, nil
|
||||
}
|
||||
invited := []InvitedMember{}
|
||||
for _, member := range members {
|
||||
invited = append(invited, InvitedMember{Email: member.Email, InvitedOn: member.InvitedOn})
|
||||
}
|
||||
return invited, err
|
||||
}
|
||||
|
||||
func (r *projectResolver) Labels(ctx context.Context, obj *db.Project) ([]db.ProjectLabel, error) {
|
||||
labels, err := r.Repository.GetProjectLabelsForProject(ctx, obj.ProjectID)
|
||||
return labels, err
|
||||
@ -988,6 +1169,25 @@ func (r *queryResolver) Users(ctx context.Context) ([]db.UserAccount, error) {
|
||||
return r.Repository.GetAllUserAccounts(ctx)
|
||||
}
|
||||
|
||||
func (r *queryResolver) InvitedUsers(ctx context.Context) ([]InvitedUserAccount, error) {
|
||||
invitedMembers, err := r.Repository.GetInvitedUserAccounts(ctx)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return []InvitedUserAccount{}, nil
|
||||
}
|
||||
return []InvitedUserAccount{}, err
|
||||
}
|
||||
members := []InvitedUserAccount{}
|
||||
for _, invitedMember := range invitedMembers {
|
||||
members = append(members, InvitedUserAccount{
|
||||
ID: invitedMember.UserAccountInvitedID,
|
||||
Email: invitedMember.Email,
|
||||
InvitedOn: invitedMember.InvitedOn,
|
||||
})
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindUser(ctx context.Context, input FindUser) (*db.UserAccount, error) {
|
||||
account, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
@ -1002,11 +1202,7 @@ func (r *queryResolver) FindUser(ctx context.Context, input FindUser) (*db.UserA
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db.Project, error) {
|
||||
userID, role, ok := GetUser(ctx)
|
||||
log.WithFields(log.Fields{"userID": userID, "role": role}).Info("find project user")
|
||||
if !ok {
|
||||
return &db.Project{}, nil
|
||||
}
|
||||
logger.New(ctx).Info("finding project user")
|
||||
project, err := r.Repository.GetProjectByID(ctx, input.ProjectID)
|
||||
if err == sql.ErrNoRows {
|
||||
return &db.Project{}, &gqlerror.Error{
|
||||
@ -1027,10 +1223,10 @@ func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task,
|
||||
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]db.Project, error) {
|
||||
userID, orgRole, ok := GetUser(ctx)
|
||||
if !ok {
|
||||
log.Info("user id was not found from middleware")
|
||||
logger.New(ctx).Info("user id was not found from middleware")
|
||||
return []db.Project{}, nil
|
||||
}
|
||||
log.WithFields(log.Fields{"userID": userID}).Info("fetching projects")
|
||||
logger.New(ctx).Info("fetching projects")
|
||||
|
||||
if input != nil {
|
||||
return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
|
||||
@ -1046,37 +1242,36 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]
|
||||
|
||||
projects := make(map[string]db.Project)
|
||||
for _, team := range teams {
|
||||
log.WithFields(log.Fields{"teamID": team.TeamID}).Info("found team")
|
||||
logger.New(ctx).WithField("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")
|
||||
logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding team project")
|
||||
projects[project.ProjectID.String()] = project
|
||||
}
|
||||
}
|
||||
|
||||
visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
|
||||
if err != nil {
|
||||
log.WithField("userID", userID).Info("error getting visible projects for user")
|
||||
logger.New(ctx).Info("error getting visible projects for user")
|
||||
return []db.Project{}, nil
|
||||
}
|
||||
for _, project := range visibleProjects {
|
||||
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project")
|
||||
logger.New(ctx).WithField("projectID", project.ProjectID).Info("found visible project")
|
||||
if _, ok := projects[project.ProjectID.String()]; !ok {
|
||||
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
|
||||
logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding visible project")
|
||||
projects[project.ProjectID.String()] = project
|
||||
}
|
||||
}
|
||||
log.WithFields(log.Fields{"projectLength": len(projects)}).Info("making projects")
|
||||
logger.New(ctx).WithField("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")
|
||||
logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding project to final list")
|
||||
allProjects = append(allProjects, project)
|
||||
}
|
||||
log.Info(allProjects)
|
||||
return allProjects, nil
|
||||
}
|
||||
|
||||
@ -1091,7 +1286,7 @@ func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team,
|
||||
func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||
userID, orgRole, ok := GetUser(ctx)
|
||||
if !ok {
|
||||
log.Error("userID or orgRole does not exist!")
|
||||
logger.New(ctx).Error("userID or org role does not exist")
|
||||
return []db.Team{}, errors.New("internal error")
|
||||
}
|
||||
if orgRole == "admin" {
|
||||
@ -1102,7 +1297,7 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||
teams := make(map[string]db.Team)
|
||||
adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting teams for user ID")
|
||||
logger.New(ctx).WithError(err).Error("error while getting teams for user ID")
|
||||
return []db.Team{}, err
|
||||
}
|
||||
|
||||
@ -1112,19 +1307,19 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||
|
||||
visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
|
||||
if err != nil {
|
||||
log.WithField("userID", userID).WithError(err).Error("error while getting visible projects for user ID")
|
||||
logger.New(ctx).WithError(err).Error("error while getting visible projects for user ID")
|
||||
return []db.Team{}, err
|
||||
}
|
||||
for _, project := range visibleProjects {
|
||||
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project")
|
||||
logger.New(ctx).WithField("projectID", project.ProjectID).Info("found visible project")
|
||||
if _, ok := teams[project.ProjectID.String()]; !ok {
|
||||
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
|
||||
logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding visible project")
|
||||
team, err := r.Repository.GetTeamByID(ctx, project.TeamID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
continue
|
||||
}
|
||||
log.WithField("teamID", project.TeamID).WithError(err).Error("error getting team by id")
|
||||
logger.New(ctx).WithField("teamID", project.TeamID).WithError(err).Error("error getting team by id")
|
||||
return []db.Team{}, err
|
||||
}
|
||||
teams[project.TeamID.String()] = team
|
||||
@ -1152,7 +1347,7 @@ func (r *queryResolver) Me(ctx context.Context) (*MePayload, error) {
|
||||
}
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, userID)
|
||||
if err == sql.ErrNoRows {
|
||||
log.WithFields(log.Fields{"userID": userID}).Warning("can not find user for me query")
|
||||
logger.New(ctx).Warning("can not find user for me query")
|
||||
return &MePayload{}, nil
|
||||
} else if err != nil {
|
||||
return &MePayload{}, err
|
||||
@ -1180,7 +1375,7 @@ func (r *queryResolver) Me(ctx context.Context) (*MePayload, error) {
|
||||
|
||||
func (r *queryResolver) Notifications(ctx context.Context) ([]db.Notification, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
log.WithFields(log.Fields{"userID": userID}).Info("fetching notifications")
|
||||
logger.New(ctx).Info("fetching notifications")
|
||||
if !ok {
|
||||
return []db.Notification{}, errors.New("user id is missing")
|
||||
}
|
||||
@ -1193,6 +1388,65 @@ func (r *queryResolver) Notifications(ctx context.Context) ([]db.Notification, e
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFilter) ([]MemberSearchResult, error) {
|
||||
availableMembers, err := r.Repository.GetMemberData(ctx, *input.ProjectID)
|
||||
if err != nil {
|
||||
logger.New(ctx).WithField("projectID", input.ProjectID).WithError(err).Error("error while getting member data")
|
||||
return []MemberSearchResult{}, err
|
||||
}
|
||||
|
||||
invitedMembers, err := r.Repository.GetInvitedMembersForProjectID(ctx, *input.ProjectID)
|
||||
if err != nil {
|
||||
logger.New(ctx).WithField("projectID", input.ProjectID).WithError(err).Error("error while getting member data")
|
||||
return []MemberSearchResult{}, err
|
||||
}
|
||||
|
||||
sortList := []string{}
|
||||
masterList := map[string]MasterEntry{}
|
||||
for _, member := range availableMembers {
|
||||
sortList = append(sortList, member.Username)
|
||||
sortList = append(sortList, member.Email)
|
||||
masterList[member.Username] = MasterEntry{ID: member.UserID, MemberType: MemberTypeJoined}
|
||||
masterList[member.Email] = MasterEntry{ID: member.UserID, MemberType: MemberTypeJoined}
|
||||
}
|
||||
for _, member := range invitedMembers {
|
||||
sortList = append(sortList, member.Email)
|
||||
logger.New(ctx).WithField("Email", member.Email).Info("adding member")
|
||||
masterList[member.Email] = MasterEntry{ID: member.UserAccountInvitedID, MemberType: MemberTypeInvited}
|
||||
}
|
||||
|
||||
logger.New(ctx).WithField("searchFilter", input.SearchFilter).Info(sortList)
|
||||
rankedList := fuzzy.RankFind(input.SearchFilter, sortList)
|
||||
logger.New(ctx).Info(rankedList)
|
||||
results := []MemberSearchResult{}
|
||||
memberList := map[uuid.UUID]bool{}
|
||||
for _, rank := range rankedList {
|
||||
entry, _ := masterList[rank.Target]
|
||||
_, ok := memberList[entry.ID]
|
||||
logger.New(ctx).WithField("ok", ok).WithField("target", rank.Target).Info("checking rank")
|
||||
if !ok {
|
||||
if entry.MemberType == MemberTypeJoined {
|
||||
logger.New(ctx).WithFields(log.Fields{"source": rank.Source, "target": rank.Target}).Info("searching")
|
||||
entry := masterList[rank.Target]
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, entry.ID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
continue
|
||||
}
|
||||
return []MemberSearchResult{}, err
|
||||
}
|
||||
results = append(results, MemberSearchResult{ID: user.UserID.String(), User: &user, Status: ShareStatusJoined, Similarity: rank.Distance})
|
||||
} else {
|
||||
logger.New(ctx).WithField("id", rank.Target).Info("adding target")
|
||||
results = append(results, MemberSearchResult{ID: rank.Target, Status: ShareStatusInvited, Similarity: rank.Distance})
|
||||
|
||||
}
|
||||
memberList[entry.ID] = true
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
|
||||
return obj.TokenID, nil
|
||||
}
|
||||
@ -1257,7 +1511,7 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er
|
||||
if err == sql.ErrNoRows {
|
||||
role = db.Role{Code: "owner", Name: "Owner"}
|
||||
} else {
|
||||
log.WithFields(log.Fields{"userID": user.UserID}).WithError(err).Error("get role for project member")
|
||||
logger.New(ctx).WithError(err).Error("get role for project member")
|
||||
return taskMembers, err
|
||||
}
|
||||
}
|
||||
@ -1351,14 +1605,14 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
|
||||
|
||||
teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get project members for project id")
|
||||
logger.New(ctx).Error("get project members for project id")
|
||||
return members, err
|
||||
}
|
||||
|
||||
for _, teamMember := range teamMembers {
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, teamMember.UserID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get user account by ID")
|
||||
logger.New(ctx).WithError(err).Error("get user account by ID")
|
||||
return members, err
|
||||
}
|
||||
var url *string
|
||||
@ -1367,7 +1621,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
|
||||
}
|
||||
role, err := r.Repository.GetRoleForTeamMember(ctx, db.GetRoleForTeamMemberParams{UserID: user.UserID, TeamID: obj.TeamID})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get role for projet member by user ID")
|
||||
logger.New(ctx).WithError(err).Error("get role for projet member by user ID")
|
||||
return members, err
|
||||
}
|
||||
|
||||
@ -1395,8 +1649,7 @@ func (r *userAccountResolver) ID(ctx context.Context, obj *db.UserAccount) (uuid
|
||||
func (r *userAccountResolver) Role(ctx context.Context, obj *db.UserAccount) (*db.Role, error) {
|
||||
role, err := r.Repository.GetRoleForUserID(ctx, obj.UserID)
|
||||
if err != nil {
|
||||
log.Info("beep!")
|
||||
log.WithError(err).Error("get role for user id")
|
||||
logger.New(ctx).WithError(err).Error("get role for user id")
|
||||
return &db.Role{}, err
|
||||
}
|
||||
return &db.Role{Code: role.Code, Name: role.Name}, nil
|
||||
@ -1506,3 +1759,21 @@ type taskGroupResolver struct{ *Resolver }
|
||||
type taskLabelResolver struct{ *Resolver }
|
||||
type teamResolver struct{ *Resolver }
|
||||
type userAccountResolver struct{ *Resolver }
|
||||
|
||||
// !!! WARNING !!!
|
||||
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
|
||||
// one last chance to move it out of harms way if you want. There are two reasons this happens:
|
||||
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
|
||||
// it when you're done.
|
||||
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
||||
type MemberType string
|
||||
|
||||
const (
|
||||
MemberTypeInvited MemberType = "INVITED"
|
||||
MemberTypeJoined MemberType = "JOINED"
|
||||
)
|
||||
|
||||
type MasterEntry struct {
|
||||
MemberType MemberType
|
||||
ID uuid.UUID
|
||||
}
|
||||
|
@ -86,6 +86,13 @@ type UserAccount {
|
||||
member: MemberList!
|
||||
}
|
||||
|
||||
type InvitedUserAccount {
|
||||
id: ID!
|
||||
email: String!
|
||||
invitedOn: Time!
|
||||
member: MemberList!
|
||||
}
|
||||
|
||||
type Team {
|
||||
id: ID!
|
||||
createdAt: Time!
|
||||
@ -93,6 +100,12 @@ type Team {
|
||||
members: [Member!]!
|
||||
}
|
||||
|
||||
|
||||
type InvitedMember {
|
||||
email: String!
|
||||
invitedOn: Time!
|
||||
}
|
||||
|
||||
type Project {
|
||||
id: ID!
|
||||
createdAt: Time!
|
||||
@ -100,6 +113,7 @@ type Project {
|
||||
team: Team
|
||||
taskGroups: [TaskGroup!]!
|
||||
members: [Member!]!
|
||||
invitedMembers: [InvitedMember!]!
|
||||
labels: [ProjectLabel!]!
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
enum ShareStatus {
|
||||
INVITED
|
||||
JOINED
|
||||
}
|
||||
|
||||
enum RoleLevel {
|
||||
ADMIN
|
||||
MEMBER
|
||||
@ -24,6 +29,7 @@ directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!)
|
||||
type Query {
|
||||
organizations: [Organization!]!
|
||||
users: [UserAccount!]!
|
||||
invitedUsers: [InvitedUserAccount!]!
|
||||
findUser(input: FindUser!): UserAccount!
|
||||
findProject(input: FindProject!):
|
||||
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||
|
@ -1,20 +1,39 @@
|
||||
extend type Mutation {
|
||||
createProjectMember(input: CreateProjectMember!):
|
||||
CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
inviteProjectMembers(input: InviteProjectMembers!):
|
||||
InviteProjectMembersPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
deleteProjectMember(input: DeleteProjectMember!):
|
||||
DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
updateProjectMemberRole(input: UpdateProjectMemberRole!):
|
||||
UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
|
||||
deleteInvitedProjectMember(input: DeleteInvitedProjectMember!):
|
||||
DeleteInvitedProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
}
|
||||
|
||||
input CreateProjectMember {
|
||||
input DeleteInvitedProjectMember {
|
||||
projectID: UUID!
|
||||
userID: UUID!
|
||||
email: String!
|
||||
}
|
||||
|
||||
type CreateProjectMemberPayload {
|
||||
type DeleteInvitedProjectMemberPayload {
|
||||
invitedMember: InvitedMember!
|
||||
}
|
||||
|
||||
input MemberInvite {
|
||||
userID: UUID
|
||||
email: String
|
||||
}
|
||||
|
||||
input InviteProjectMembers {
|
||||
projectID: UUID!
|
||||
members: [MemberInvite!]!
|
||||
}
|
||||
|
||||
type InviteProjectMembersPayload {
|
||||
ok: Boolean!
|
||||
member: Member!
|
||||
projectID: UUID!
|
||||
members: [Member!]!
|
||||
invitedMembers: [InvitedMember!]!
|
||||
}
|
||||
|
||||
input DeleteProjectMember {
|
||||
|
@ -4,6 +4,8 @@ extend type Mutation {
|
||||
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
deleteUserAccount(input: DeleteUserAccount!):
|
||||
DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
deleteInvitedUserAccount(input: DeleteInvitedUserAccount!):
|
||||
DeleteInvitedUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
|
||||
logoutUser(input: LogoutUser!): Boolean!
|
||||
clearProfileAvatar: UserAccount!
|
||||
@ -15,6 +17,31 @@ extend type Mutation {
|
||||
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
searchMembers(input: MemberSearchFilter!): [MemberSearchResult!]!
|
||||
}
|
||||
|
||||
input DeleteInvitedUserAccount {
|
||||
invitedUserID: UUID!
|
||||
}
|
||||
|
||||
type DeleteInvitedUserAccountPayload {
|
||||
invitedUser: InvitedUserAccount!
|
||||
}
|
||||
|
||||
input MemberSearchFilter {
|
||||
searchFilter: String!
|
||||
projectID: UUID
|
||||
}
|
||||
|
||||
|
||||
type MemberSearchResult {
|
||||
similarity: Int!
|
||||
id: String!
|
||||
user: UserAccount
|
||||
status: ShareStatus!
|
||||
}
|
||||
|
||||
type UpdateUserInfoPayload {
|
||||
user: UserAccount!
|
||||
}
|
||||
|
@ -1,89 +1,21 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"context"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/taskcafe/internal/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewStructuredLogger creates a new logger for chi router
|
||||
func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler {
|
||||
return middleware.RequestLogger(&StructuredLogger{logger})
|
||||
}
|
||||
|
||||
// StructuredLogger is a logger for chi router
|
||||
type StructuredLogger struct {
|
||||
Logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewLogEntry creates a new log entry for the given HTTP request
|
||||
func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
entry := &StructuredLoggerEntry{Logger: logrus.NewEntry(l.Logger)}
|
||||
logFields := logrus.Fields{}
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
logFields["req_id"] = reqID
|
||||
// New returns a log entry with the reqID and userID fields populated if they exist
|
||||
func New(ctx context.Context) *log.Entry {
|
||||
entry := log.NewEntry(log.StandardLogger())
|
||||
if reqID, ok := ctx.Value(utils.ReqIDKey).(uuid.UUID); ok {
|
||||
entry = entry.WithField("reqID", reqID)
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
if userID, ok := ctx.Value(utils.UserIDKey).(uuid.UUID); ok {
|
||||
entry = entry.WithField("userID", userID)
|
||||
}
|
||||
logFields["http_scheme"] = scheme
|
||||
logFields["http_proto"] = r.Proto
|
||||
logFields["http_method"] = r.Method
|
||||
|
||||
logFields["remote_addr"] = r.RemoteAddr
|
||||
logFields["user_agent"] = r.UserAgent()
|
||||
|
||||
logFields["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||
|
||||
entry.Logger = entry.Logger.WithFields(logFields)
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
// StructuredLoggerEntry is a log entry will all relevant information about a specific http request
|
||||
type StructuredLoggerEntry struct {
|
||||
Logger logrus.FieldLogger
|
||||
}
|
||||
|
||||
// Write logs information about http request response body
|
||||
func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) {
|
||||
l.Logger = l.Logger.WithFields(logrus.Fields{
|
||||
"resp_status": status, "resp_bytes_length": bytes,
|
||||
"resp_elapsed_ms": float64(elapsed.Nanoseconds()) / 1000000.0,
|
||||
})
|
||||
l.Logger.Debugln("request complete")
|
||||
}
|
||||
|
||||
// Panic logs if the request panics
|
||||
func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
|
||||
l.Logger = l.Logger.WithFields(logrus.Fields{
|
||||
"stack": string(stack),
|
||||
"panic": fmt.Sprintf("%+v", v),
|
||||
})
|
||||
}
|
||||
|
||||
// GetLogEntry helper function for getting log entry for request
|
||||
func GetLogEntry(r *http.Request) logrus.FieldLogger {
|
||||
entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry)
|
||||
return entry.Logger
|
||||
}
|
||||
|
||||
// LogEntrySetField sets a key's value
|
||||
func LogEntrySetField(r *http.Request, key string, value interface{}) {
|
||||
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
|
||||
entry.Logger = entry.Logger.WithField(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// LogEntrySetFields sets the log entry's fields
|
||||
func LogEntrySetFields(r *http.Request, fields map[string]interface{}) {
|
||||
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
|
||||
entry.Logger = entry.Logger.WithFields(fields)
|
||||
}
|
||||
}
|
||||
|
89
internal/logger/route_logger.go
Normal file
89
internal/logger/route_logger.go
Normal file
@ -0,0 +1,89 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewStructuredLogger creates a new logger for chi router
|
||||
func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler {
|
||||
return middleware.RequestLogger(&StructuredLogger{logger})
|
||||
}
|
||||
|
||||
// StructuredLogger is a logger for chi router
|
||||
type StructuredLogger struct {
|
||||
Logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewLogEntry creates a new log entry for the given HTTP request
|
||||
func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
entry := &StructuredLoggerEntry{Logger: logrus.NewEntry(l.Logger)}
|
||||
logFields := logrus.Fields{}
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
logFields["req_id"] = reqID
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
logFields["http_scheme"] = scheme
|
||||
logFields["http_proto"] = r.Proto
|
||||
logFields["http_method"] = r.Method
|
||||
|
||||
logFields["remote_addr"] = r.RemoteAddr
|
||||
logFields["user_agent"] = r.UserAgent()
|
||||
|
||||
logFields["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||
|
||||
entry.Logger = entry.Logger.WithFields(logFields)
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
// StructuredLoggerEntry is a log entry will all relevant information about a specific http request
|
||||
type StructuredLoggerEntry struct {
|
||||
Logger logrus.FieldLogger
|
||||
}
|
||||
|
||||
// Write logs information about http request response body
|
||||
func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) {
|
||||
l.Logger = l.Logger.WithFields(logrus.Fields{
|
||||
"resp_status": status, "resp_bytes_length": bytes,
|
||||
"resp_elapsed_ms": float64(elapsed.Nanoseconds()) / 1000000.0,
|
||||
})
|
||||
l.Logger.Debugln("request complete")
|
||||
}
|
||||
|
||||
// Panic logs if the request panics
|
||||
func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
|
||||
l.Logger = l.Logger.WithFields(logrus.Fields{
|
||||
"stack": string(stack),
|
||||
"panic": fmt.Sprintf("%+v", v),
|
||||
})
|
||||
}
|
||||
|
||||
// GetLogEntry helper function for getting log entry for request
|
||||
func GetLogEntry(r *http.Request) logrus.FieldLogger {
|
||||
entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry)
|
||||
return entry.Logger
|
||||
}
|
||||
|
||||
// LogEntrySetField sets a key's value
|
||||
func LogEntrySetField(r *http.Request, key string, value interface{}) {
|
||||
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
|
||||
entry.Logger = entry.Logger.WithField(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// LogEntrySetFields sets the log entry's fields
|
||||
func LogEntrySetFields(r *http.Request, fields map[string]interface{}) {
|
||||
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
|
||||
entry.Logger = entry.Logger.WithFields(fields)
|
||||
}
|
||||
}
|
@ -31,15 +31,33 @@ type NewUserAccount struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
// RegisterUserRequestData is the request data for registering a new user (duh)
|
||||
type RegisterUserRequestData struct {
|
||||
User NewUserAccount
|
||||
}
|
||||
|
||||
type RegisteredUserResponseData struct {
|
||||
Setup bool `json:"setup"`
|
||||
}
|
||||
|
||||
// ConfirmUserRequestData is the request data for upgrading an invited user to a normal user
|
||||
type ConfirmUserRequestData struct {
|
||||
ConfirmToken string
|
||||
}
|
||||
|
||||
// InstallRequestData is the request data for installing new Taskcafe app
|
||||
type InstallRequestData struct {
|
||||
User NewUserAccount
|
||||
}
|
||||
|
||||
type Setup struct {
|
||||
ConfirmToken string `json:"confirmToken"`
|
||||
}
|
||||
|
||||
// LoginResponseData is the response data for when a user logs in
|
||||
type LoginResponseData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
IsInstalled bool `json:"isInstalled"`
|
||||
Setup bool `json:"setup"`
|
||||
}
|
||||
|
||||
// LogoutResponseData is the response data for when a user logs out
|
||||
@ -60,30 +78,24 @@ type AvatarUploadResponseData struct {
|
||||
|
||||
// RefreshTokenHandler handles when a user attempts to refresh token
|
||||
func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := h.repo.GetSystemOptionByKey(r.Context(), "is_installed")
|
||||
if err == sql.ErrNoRows {
|
||||
user, err := h.repo.GetUserAccountByUsername(r.Context(), "system")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode, h.jwtKey)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: false})
|
||||
|
||||
userExists, err := h.repo.HasAnyUser(r.Context())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.WithError(err).Error("issue while fetching if user accounts exist")
|
||||
return
|
||||
} else if err != nil {
|
||||
log.WithError(err).Error("get system option")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
log.WithField("userExists", userExists).Info("checking if setup")
|
||||
if !userExists {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: "", Setup: true})
|
||||
return
|
||||
}
|
||||
|
||||
c, err := r.Cookie("refreshToken")
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie {
|
||||
log.Warn("no cookie")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@ -112,6 +124,14 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.WithFields(log.Fields{
|
||||
"username": user.Username,
|
||||
}).Warn("attempt to refresh token with inactive user")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
refreshCreatedAt := time.Now().UTC()
|
||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{token.UserID, refreshCreatedAt, refreshExpiresAt})
|
||||
@ -119,13 +139,17 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
||||
err = h.repo.DeleteRefreshTokenByID(r.Context(), token.TokenID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("here 1")
|
||||
accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("here 2")
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refreshToken",
|
||||
@ -133,7 +157,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
||||
Expires: refreshExpiresAt,
|
||||
HttpOnly: true,
|
||||
})
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: true})
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, Setup: false})
|
||||
}
|
||||
|
||||
// LogoutHandler removes all refresh tokens to log out user
|
||||
@ -175,6 +199,14 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.WithFields(log.Fields{
|
||||
"username": requestData.Username,
|
||||
}).Warn("attempt to login with inactive user")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(requestData.Password))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
@ -203,6 +235,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
// InstallHandler creates first user on fresh install
|
||||
func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if restricted, ok := r.Context().Value("restricted_mode").(auth.RestrictedMode); ok {
|
||||
@ -266,6 +299,172 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
func (h *TaskcafeHandler) ConfirmUser(w http.ResponseWriter, r *http.Request) {
|
||||
usersExist, err := h.repo.HasActiveUser(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue checking if user accounts exist")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var user db.UserAccount
|
||||
if !usersExist {
|
||||
log.Info("setting first inactive user to active")
|
||||
user, err = h.repo.SetFirstUserActive(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue checking if user accounts exist")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var requestData ConfirmUserRequestData
|
||||
err = json.NewDecoder(r.Body).Decode(&requestData)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue decoding request data")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
confirmTokenID, err := uuid.Parse(requestData.ConfirmToken)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue parsing confirm token")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
confirmToken, err := h.repo.GetConfirmTokenByID(r.Context(), confirmTokenID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue getting token by id")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err = h.repo.SetUserActiveByEmail(r.Context(), confirmToken.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue getting account by email")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
projects, err := h.repo.GetProjectsForInvitedMember(r.Context(), user.Email)
|
||||
for _, project := range projects {
|
||||
member, err := h.repo.CreateProjectMember(r.Context(),
|
||||
db.CreateProjectMemberParams{
|
||||
ProjectID: project,
|
||||
UserID: user.UserID,
|
||||
AddedAt: now,
|
||||
RoleCode: "member",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue creating project member")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.WithField("memberID", member.ProjectMemberID).Info("creating project member")
|
||||
err = h.repo.DeleteProjectMemberInvitedForEmail(r.Context(), user.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue deleting project member invited")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = h.repo.DeleteUserAccountInvitedForEmail(r.Context(), user.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue deleting user account invited")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = h.repo.DeleteConfirmTokenForEmail(r.Context(), user.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue deleting confirm token")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refreshCreatedAt := time.Now().UTC()
|
||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
||||
|
||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refreshToken",
|
||||
Value: refreshTokenString.TokenID.String(),
|
||||
Expires: refreshExpiresAt,
|
||||
HttpOnly: true,
|
||||
})
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
||||
userExists, err := h.repo.HasAnyUser(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue checking if user accounts exist")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var requestData RegisterUserRequestData
|
||||
err = json.NewDecoder(r.Body).Decode(&requestData)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue decoding register user request data")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if userExists {
|
||||
_, err := h.repo.GetInvitedUserByEmail(r.Context(), requestData.User.Email)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
hasActiveUser, err := h.repo.HasActiveUser(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error checking for active user")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if !hasActiveUser {
|
||||
json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: true})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.WithError(err).Error("error while retrieving invited user by email")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: accept user if public registration is enabled
|
||||
|
||||
createdAt := time.Now().UTC()
|
||||
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(requestData.User.Password), 14)
|
||||
if err != nil {
|
||||
log.Error("issue generating passoed")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user, err := h.repo.CreateUserAccount(r.Context(), db.CreateUserAccountParams{
|
||||
FullName: requestData.User.FullName,
|
||||
Username: requestData.User.Username,
|
||||
Initials: requestData.User.Initials,
|
||||
Email: requestData.User.Email,
|
||||
PasswordHash: string(hashedPwd),
|
||||
CreatedAt: createdAt,
|
||||
RoleCode: "admin",
|
||||
Active: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("issue registering user account")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.WithField("username", user.UserID).Info("registered new user account")
|
||||
json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: !userExists})
|
||||
}
|
||||
|
||||
// Routes registers all authentication routes
|
||||
func (rs authResource) Routes(taskcafeHandler TaskcafeHandler) chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
@ -19,6 +19,7 @@ type AuthenticationMiddleware struct {
|
||||
// Middleware returns the middleware handler
|
||||
func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestID := uuid.New()
|
||||
bearerTokenRaw := r.Header.Get("Authorization")
|
||||
splitToken := strings.Split(bearerTokenRaw, "Bearer")
|
||||
if len(splitToken) != 2 {
|
||||
@ -61,6 +62,7 @@ func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
ctx := context.WithValue(r.Context(), utils.UserIDKey, userID)
|
||||
ctx = context.WithValue(ctx, utils.RestrictedModeKey, accessClaims.Restricted)
|
||||
ctx = context.WithValue(ctx, utils.OrgRoleKey, accessClaims.OrgRole)
|
||||
ctx = context.WithValue(ctx, utils.ReqIDKey, requestID)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
|
@ -87,13 +87,13 @@ func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
|
||||
mux.Mount("/auth", authResource{}.Routes(taskcafeHandler))
|
||||
mux.Handle("/__graphql", graph.NewPlaygroundHandler("/graphql"))
|
||||
mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
|
||||
|
||||
mux.Post("/auth/confirm", taskcafeHandler.ConfirmUser)
|
||||
mux.Post("/auth/register", taskcafeHandler.RegisterUser)
|
||||
})
|
||||
auth := AuthenticationMiddleware{jwtKey}
|
||||
r.Group(func(mux chi.Router) {
|
||||
mux.Use(auth.Middleware)
|
||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
||||
mux.Post("/auth/install", taskcafeHandler.InstallHandler)
|
||||
mux.Handle("/graphql", graph.NewHandler(*repository))
|
||||
})
|
||||
|
||||
|
@ -6,6 +6,8 @@ type ContextKey string
|
||||
const (
|
||||
// UserIDKey is the key for the user id of the authenticated user
|
||||
UserIDKey ContextKey = "userID"
|
||||
// ReqIDKey is the unique ID key for current request
|
||||
ReqIDKey ContextKey = "reqID"
|
||||
//RestrictedModeKey is the key for whether the authenticated user only has access to install route
|
||||
RestrictedModeKey ContextKey = "restricted_mode"
|
||||
// OrgRoleKey is the key for the organization role code of the authenticated user
|
||||
|
6
migrations/0056_add-user_account_invited-table.up.sql
Normal file
6
migrations/0056_add-user_account_invited-table.up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE user_account_invited (
|
||||
user_account_invited_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email text NOT NULL UNIQUE,
|
||||
invited_on timestamptz NOT NULL DEFAULT NOW(),
|
||||
has_joined boolean NOT NULL DEFAULT false
|
||||
);
|
7
migrations/0057_add-project_member_invited-table.up.sql
Normal file
7
migrations/0057_add-project_member_invited-table.up.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE project_member_invited (
|
||||
project_member_invited_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
project_id uuid NOT NULL
|
||||
REFERENCES project(project_id) ON DELETE CASCADE,
|
||||
user_account_invited_id uuid NOT NULL
|
||||
REFERENCES user_account_invited(user_account_invited_id) ON DELETE CASCADE
|
||||
);
|
2
migrations/0058_add-active-column-to-user_account.up.sql
Normal file
2
migrations/0058_add-active-column-to-user_account.up.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE user_account ADD COLUMN active boolean NOT NULL DEFAULT false;
|
||||
UPDATE user_account SET active = true;
|
4
migrations/0059_add-confirm_token_table.up.sql
Normal file
4
migrations/0059_add-confirm_token_table.up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE user_account_confirm_token (
|
||||
confirm_token_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email text NOT NULL UNIQUE
|
||||
);
|
Loading…
Reference in New Issue
Block a user