refactor: replace refresh & access token with auth token only
changes authentication to no longer use a refresh token & access token for accessing protected endpoints. Instead only an auth token is used. Before the login flow was: Login -> get refresh (stored as HttpOnly cookie) + access token (stored in memory) -> protected endpoint request (attach access token as Authorization header) -> access token expires in 15 minutes, so use refresh token to obtain new one when that happens now it looks like this: Login -> get auth token (stored as HttpOnly cookie) -> make protected endpont request (token sent) the reasoning for using the refresh + access token was to reduce DB calls, but in the end I don't think its worth the hassle.
This commit is contained in:
parent
3392b3345d
commit
229a53fa0a
@ -39,7 +39,7 @@
|
|||||||
"dayjs": "^1.9.1",
|
"dayjs": "^1.9.1",
|
||||||
"dompurify": "^2.2.6",
|
"dompurify": "^2.2.6",
|
||||||
"emoji-mart": "^3.0.0",
|
"emoji-mart": "^3.0.0",
|
||||||
"emoticon": "^3.2.0",
|
"emoticon": "^4.0.0",
|
||||||
"graphql": "^15.0.0",
|
"graphql": "^15.0.0",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
|
@ -215,9 +215,12 @@ const AdminRoute = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
|
/*
|
||||||
|
TODO: add permision check
|
||||||
if (user.roles.org !== 'admin') {
|
if (user.roles.org !== 'admin') {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
|
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
|
||||||
@ -225,7 +228,8 @@ const AdminRoute = () => {
|
|||||||
initialTab={0}
|
initialTab={0}
|
||||||
users={data.users}
|
users={data.users}
|
||||||
invitedUsers={data.invitedUsers}
|
invitedUsers={data.invitedUsers}
|
||||||
canInviteUser={user.roles.org === 'admin'}
|
// canInviteUser={user.roles.org === 'admin'} TODO: add permision check
|
||||||
|
canInviteUser={true}
|
||||||
onInviteUser={NOOP}
|
onInviteUser={NOOP}
|
||||||
onUpdateUserPassword={() => {
|
onUpdateUserPassword={() => {
|
||||||
hidePopup();
|
hidePopup();
|
||||||
|
@ -13,8 +13,6 @@ import Login from 'Auth';
|
|||||||
import Register from 'Register';
|
import Register from 'Register';
|
||||||
import Profile from 'Profile';
|
import Profile from 'Profile';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import JwtDecode from 'jwt-decode';
|
|
||||||
import { setAccessToken } from 'shared/utils/accessToken';
|
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
|
|
||||||
const MainContent = styled.div`
|
const MainContent = styled.div`
|
||||||
@ -26,9 +24,9 @@ const MainContent = styled.div`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type RefreshTokenResponse = {
|
type ValidateTokenResponse = {
|
||||||
accessToken: string;
|
valid: boolean;
|
||||||
setup?: null | { confirmToken: string };
|
userID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthorizedRoutes = () => {
|
const AuthorizedRoutes = () => {
|
||||||
@ -36,27 +34,17 @@ const AuthorizedRoutes = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { setUser } = useCurrentUser();
|
const { setUser } = useCurrentUser();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/auth/refresh_token', {
|
fetch('/auth/validate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then(async x => {
|
}).then(async x => {
|
||||||
const { status } = x;
|
const { status } = x;
|
||||||
if (status === 400) {
|
const response: ValidateTokenResponse = await x.json();
|
||||||
history.replace('/login');
|
const { valid, userID } = response;
|
||||||
|
if (!valid) {
|
||||||
|
history.replace(`/login`);
|
||||||
} else {
|
} else {
|
||||||
const response: RefreshTokenResponse = await x.json();
|
setUser(userID);
|
||||||
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);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
@ -3,13 +3,8 @@ import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
|
|||||||
import { ProfileMenu } from 'shared/components/DropdownMenu';
|
import { ProfileMenu } from 'shared/components/DropdownMenu';
|
||||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import {
|
import { RoleCode, useTopNavbarQuery, useDeleteProjectMutation, GetProjectsDocument } from 'shared/generated/graphql';
|
||||||
RoleCode,
|
|
||||||
useTopNavbarQuery,
|
|
||||||
useDeleteProjectMutation,
|
|
||||||
GetProjectsDocument,
|
|
||||||
} from 'shared/generated/graphql';
|
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import MiniProfile from 'shared/components/MiniProfile';
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
@ -107,23 +102,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
onRemoveInvitedFromBoard,
|
onRemoveInvitedFromBoard,
|
||||||
onRemoveFromBoard,
|
onRemoveFromBoard,
|
||||||
}) => {
|
}) => {
|
||||||
const { user, setUserRoles, setUser } = useCurrentUser();
|
const { user, setUser } = useCurrentUser();
|
||||||
const { loading, data } = useTopNavbarQuery({
|
const { loading, data } = useTopNavbarQuery({
|
||||||
onCompleted: response => {
|
// TODO: maybe remove?
|
||||||
if (user && user.roles) {
|
onCompleted: response => {},
|
||||||
setUserRoles({
|
|
||||||
org: user.roles.org,
|
|
||||||
teams: response.me.teamRoles.reduce((map, obj) => {
|
|
||||||
map.set(obj.teamID, obj.roleCode);
|
|
||||||
return map;
|
|
||||||
}, new Map<string, string>()),
|
|
||||||
projects: response.me.projectRoles.reduce((map, obj) => {
|
|
||||||
map.set(obj.projectID, obj.roleCode);
|
|
||||||
return map;
|
|
||||||
}, new Map<string, string>()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -147,7 +129,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
<Popup title={null} tab={0}>
|
<Popup title={null} tab={0}>
|
||||||
<ProfileMenu
|
<ProfileMenu
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
showAdminConsole={user ? user.roles.org === 'admin' : false}
|
showAdminConsole={true} // TODO: add permision check
|
||||||
onAdminConsole={() => {
|
onAdminConsole={() => {
|
||||||
history.push('/admin');
|
history.push('/admin');
|
||||||
hidePopup();
|
hidePopup();
|
||||||
@ -189,7 +171,9 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
|
// TODO: readd permision check
|
||||||
|
// const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
|
||||||
|
const userIsTeamOrProjectAdmin = true;
|
||||||
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
|
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
|
||||||
const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
|
const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
|
||||||
if (member) {
|
if (member) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
import { InMemoryCache } from '@apollo/client';
|
||||||
|
|
||||||
const cache = new InMemoryCache();
|
const cache = new InMemoryCache();
|
||||||
|
|
||||||
|
@ -1,79 +1,20 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
export enum PermissionLevel {
|
|
||||||
ORG,
|
|
||||||
TEAM,
|
|
||||||
PROJECT,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PermissionObjectType {
|
|
||||||
ORG,
|
|
||||||
TEAM,
|
|
||||||
PROJECT,
|
|
||||||
TASK,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CurrentUserRoles = {
|
|
||||||
org: string;
|
|
||||||
teams: Map<string, string>;
|
|
||||||
projects: Map<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CurrentUserRaw {
|
|
||||||
id: string;
|
|
||||||
roles: CurrentUserRoles;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserContextState = {
|
type UserContextState = {
|
||||||
user: CurrentUserRaw | null;
|
user: string | null;
|
||||||
setUser: (user: CurrentUserRaw | null) => void;
|
setUser: (user: string | null) => void;
|
||||||
setUserRoles: (roles: CurrentUserRoles) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserContext = React.createContext<UserContextState>({
|
export const UserContext = React.createContext<UserContextState>({
|
||||||
user: null,
|
user: null,
|
||||||
setUser: _user => null,
|
setUser: _user => null,
|
||||||
setUserRoles: roles => null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface CurrentUser extends CurrentUserRaw {
|
|
||||||
isAdmin: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
|
||||||
isVisible: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCurrentUser = () => {
|
export const useCurrentUser = () => {
|
||||||
const { user, setUser, setUserRoles } = useContext(UserContext);
|
const { user, setUser } = useContext(UserContext);
|
||||||
let currentUser: CurrentUser | null = null;
|
|
||||||
if (user) {
|
|
||||||
currentUser = {
|
|
||||||
...user,
|
|
||||||
isAdmin(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
|
||||||
if (user.roles.org === 'admin') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
switch (level) {
|
|
||||||
case PermissionLevel.TEAM:
|
|
||||||
return subjectID ? this.roles.teams.get(subjectID) === 'admin' : false;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isVisible(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
|
||||||
if (user.roles.org === 'admin') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
switch (level) {
|
|
||||||
case PermissionLevel.TEAM:
|
|
||||||
return subjectID ? this.roles.teams.get(subjectID) !== null : false;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
user: currentUser,
|
user,
|
||||||
setUser,
|
setUser,
|
||||||
setUserRoles,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import jwtDecode from 'jwt-decode';
|
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import { Router } from 'react-router';
|
import { Router } from 'react-router';
|
||||||
import { PopupProvider } from 'shared/components/PopupMenu';
|
import { PopupProvider } from 'shared/components/PopupMenu';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import { setAccessToken } from 'shared/utils/accessToken';
|
|
||||||
import styled, { ThemeProvider } from 'styled-components';
|
import styled, { ThemeProvider } from 'styled-components';
|
||||||
import NormalizeStyles from './NormalizeStyles';
|
import NormalizeStyles from './NormalizeStyles';
|
||||||
import BaseStyles from './BaseStyles';
|
import BaseStyles from './BaseStyles';
|
||||||
import theme from './ThemeStyles';
|
import theme from './ThemeStyles';
|
||||||
import Routes from './Routes';
|
import Routes from './Routes';
|
||||||
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context';
|
import { UserContext } from './context';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
@ -48,19 +46,11 @@ const StyledContainer = styled(ToastContainer).attrs({
|
|||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
const [user, setUser] = useState<string | null>(null);
|
||||||
const setUserRoles = (roles: CurrentUserRoles) => {
|
|
||||||
if (user) {
|
|
||||||
setUser({
|
|
||||||
...user,
|
|
||||||
roles,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
|
<UserContext.Provider value={{ user, setUser }}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<NormalizeStyles />
|
<NormalizeStyles />
|
||||||
<BaseStyles />
|
<BaseStyles />
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import JwtDecode from 'jwt-decode';
|
|
||||||
import { setAccessToken } from 'shared/utils/accessToken';
|
|
||||||
import Login from 'shared/components/Login';
|
import Login from 'shared/components/Login';
|
||||||
import UserContext from 'App/context';
|
import UserContext from 'App/context';
|
||||||
import { Container, LoginWrapper } from './Styles';
|
import { Container, LoginWrapper } from './Styles';
|
||||||
@ -30,43 +28,24 @@ const Auth = () => {
|
|||||||
setComplete(true);
|
setComplete(true);
|
||||||
} else {
|
} else {
|
||||||
const response = await x.json();
|
const response = await x.json();
|
||||||
const { accessToken } = response;
|
const { userID } = response;
|
||||||
const claims: JWTToken = JwtDecode(accessToken);
|
setUser(userID);
|
||||||
const currentUser = {
|
|
||||||
id: claims.userId,
|
|
||||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
|
||||||
};
|
|
||||||
setUser(currentUser);
|
|
||||||
setComplete(true);
|
|
||||||
setAccessToken(accessToken);
|
|
||||||
|
|
||||||
history.push('/');
|
history.push('/');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/auth/refresh_token', {
|
fetch('/auth/validate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then(async x => {
|
}).then(async x => {
|
||||||
const { status } = x;
|
const response = await x.json();
|
||||||
if (status === 200) {
|
const { valid, userID } = response;
|
||||||
const response: RefreshTokenResponse = await x.json();
|
if (valid) {
|
||||||
const { accessToken, setup } = response;
|
setUser(userID);
|
||||||
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');
|
history.replace('/projects');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -5,8 +5,6 @@ import { useHistory, useLocation } from 'react-router';
|
|||||||
import * as QueryString from 'query-string';
|
import * as QueryString from 'query-string';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Container, LoginWrapper } from './Styles';
|
import { Container, LoginWrapper } from './Styles';
|
||||||
import JwtDecode from 'jwt-decode';
|
|
||||||
import { setAccessToken } from 'shared/utils/accessToken';
|
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
|
|
||||||
const UsersConfirm = () => {
|
const UsersConfirm = () => {
|
||||||
@ -31,18 +29,8 @@ const UsersConfirm = () => {
|
|||||||
const { status } = x;
|
const { status } = x;
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
const response = await x.json();
|
const response = await x.json();
|
||||||
const { accessToken } = response;
|
const { userID } = response;
|
||||||
const claims: JWTToken = JwtDecode(accessToken);
|
setUser(userID);
|
||||||
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('/');
|
history.push('/');
|
||||||
} else {
|
} else {
|
||||||
setFailed();
|
setFailed();
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
import { getAccessToken } from 'shared/utils/accessToken';
|
|
||||||
import Settings from 'shared/components/Settings';
|
import Settings from 'shared/components/Settings';
|
||||||
import {
|
import {
|
||||||
useMeQuery,
|
useMeQuery,
|
||||||
@ -49,12 +48,9 @@ const Projects = () => {
|
|||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
const fileData = new FormData();
|
const fileData = new FormData();
|
||||||
fileData.append('file', e.target.files[0]);
|
fileData.append('file', e.target.files[0]);
|
||||||
const accessToken = getAccessToken();
|
|
||||||
axios
|
axios
|
||||||
.post('/users/me/avatar', fileData, {
|
.post('/users/me/avatar', fileData, {
|
||||||
headers: {
|
withCredentials: true,
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if ($fileUpload && $fileUpload.current) {
|
if ($fileUpload && $fileUpload.current) {
|
||||||
@ -75,7 +71,7 @@ const Projects = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onResetPassword={(password, done) => {
|
onResetPassword={(password, done) => {
|
||||||
updateUserPassword({ variables: { userID: user.id, password } });
|
updateUserPassword({ variables: { userID: user, password } });
|
||||||
toast('Password was changed!');
|
toast('Password was changed!');
|
||||||
done();
|
done();
|
||||||
}}
|
}}
|
||||||
|
@ -543,7 +543,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
onChangeTaskMetaFilter={filter => {
|
onChangeTaskMetaFilter={filter => {
|
||||||
setTaskMetaFilters(filter);
|
setTaskMetaFilters(filter);
|
||||||
}}
|
}}
|
||||||
userID={user?.id}
|
userID={user ?? ''}
|
||||||
labels={labelsRef}
|
labels={labelsRef}
|
||||||
members={membersRef}
|
members={membersRef}
|
||||||
/>,
|
/>,
|
||||||
|
@ -541,7 +541,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
bio="None"
|
bio="None"
|
||||||
onRemoveFromTask={() => {
|
onRemoveFromTask={() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
unassignTask({ variables: { taskID: data.findTask.id, userID: user ?? '' } });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import NewProject from 'shared/components/NewProject';
|
import NewProject from 'shared/components/NewProject';
|
||||||
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -268,7 +268,7 @@ const Projects = () => {
|
|||||||
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
|
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ProjectsContainer>
|
<ProjectsContainer>
|
||||||
{user.roles.org === 'admin' && (
|
{true && ( // TODO: add permision check
|
||||||
<AddTeamButton
|
<AddTeamButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={$target => {
|
onClick={$target => {
|
||||||
@ -330,7 +330,7 @@ const Projects = () => {
|
|||||||
<div key={team.id}>
|
<div key={team.id}>
|
||||||
<ProjectSectionTitleWrapper>
|
<ProjectSectionTitleWrapper>
|
||||||
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
||||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
{true && ( // TODO: add permision check
|
||||||
<SectionActions>
|
<SectionActions>
|
||||||
<SectionActionLink to={`/teams/${team.id}`}>
|
<SectionActionLink to={`/teams/${team.id}`}>
|
||||||
<SectionAction variant="outline">Projects</SectionAction>
|
<SectionAction variant="outline">Projects</SectionAction>
|
||||||
@ -355,7 +355,7 @@ const Projects = () => {
|
|||||||
</ProjectTile>
|
</ProjectTile>
|
||||||
</ProjectListItem>
|
</ProjectListItem>
|
||||||
))}
|
))}
|
||||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
{true && ( // TODO: add permision check
|
||||||
<ProjectListItem>
|
<ProjectListItem>
|
||||||
<ProjectAddTile
|
<ProjectAddTile
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -3,7 +3,7 @@ import Input from 'shared/components/Input';
|
|||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { useCurrentUser, PermissionLevel, PermissionObjectType } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import Select from 'shared/components/Select';
|
import Select from 'shared/components/Select';
|
||||||
import {
|
import {
|
||||||
useGetTeamQuery,
|
useGetTeamQuery,
|
||||||
@ -424,7 +424,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
pollInterval: 3000,
|
pollInterval: 3000,
|
||||||
});
|
});
|
||||||
const { user, setUserRoles } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const warning =
|
const warning =
|
||||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||||
const [createTeamMember] = useCreateTeamMemberMutation({
|
const [createTeamMember] = useCreateTeamMemberMutation({
|
||||||
@ -446,17 +446,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation({
|
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation();
|
||||||
onCompleted: r => {
|
|
||||||
if (user) {
|
|
||||||
setUserRoles(
|
|
||||||
produce(user.roles, draftRoles => {
|
|
||||||
draftRoles.teams.set(r.updateTeamMemberRole.teamID, r.updateTeamMemberRole.member.role.code);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<GetTeamQuery>(
|
updateApolloCache<GetTeamQuery>(
|
||||||
@ -491,7 +481,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
</ListDesc>
|
</ListDesc>
|
||||||
<ListActions>
|
<ListActions>
|
||||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && (
|
{true && ( // TODO: add permission check
|
||||||
<InviteMemberButton
|
<InviteMemberButton
|
||||||
onClick={$target => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
@ -528,11 +518,12 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
currentUserID={user.id ?? ''}
|
currentUserID={user ?? ''}
|
||||||
subject={member}
|
subject={member}
|
||||||
members={data.findTeam.members}
|
members={data.findTeam.members}
|
||||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||||
canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)}
|
// canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)} TODO: add permission check
|
||||||
|
canChangeRole={true}
|
||||||
onChangeRole={roleCode => {
|
onChangeRole={roleCode => {
|
||||||
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
||||||
}}
|
}}
|
||||||
|
@ -13,7 +13,7 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
|
|||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||||
import { PermissionObjectType, PermissionLevel, useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import Members from './Members';
|
import Members from './Members';
|
||||||
import Projects from './Projects';
|
import Projects from './Projects';
|
||||||
@ -95,9 +95,12 @@ const Teams = () => {
|
|||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
|
/*
|
||||||
|
TODO: re-add permission check
|
||||||
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar
|
<GlobalTopNavbar
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import axios from 'axios';
|
import { ApolloClient } from '@apollo/client';
|
||||||
import createAuthRefreshInterceptor from 'axios-auth-refresh';
|
import { ApolloProvider } from '@apollo/client/react';
|
||||||
import { ApolloProvider } from '@apollo/react-hooks';
|
|
||||||
import { ApolloClient } from 'apollo-client';
|
|
||||||
import { HttpLink } from 'apollo-link-http';
|
|
||||||
import { onError } from 'apollo-link-error';
|
|
||||||
import { enableMapSet } from 'immer';
|
import { enableMapSet } from 'immer';
|
||||||
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import updateLocale from 'dayjs/plugin/updateLocale';
|
import updateLocale from 'dayjs/plugin/updateLocale';
|
||||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
import isBetween from 'dayjs/plugin/isBetween';
|
import isBetween from 'dayjs/plugin/isBetween';
|
||||||
import weekday from 'dayjs/plugin/weekday';
|
import weekday from 'dayjs/plugin/weekday';
|
||||||
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
|
||||||
import cache from './App/cache';
|
import cache from './App/cache';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
@ -34,131 +29,8 @@ dayjs.updateLocale('en', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let forward$;
|
const client = new ApolloClient({ uri: '/graphql', cache });
|
||||||
let isRefreshing = false;
|
console.log('cloient', client);
|
||||||
let pendingRequests: any = [];
|
|
||||||
|
|
||||||
const refreshAuthLogic = (failedRequest: any) =>
|
|
||||||
axios.post('/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => {
|
|
||||||
setAccessToken(tokenRefreshResponse.data.accessToken);
|
|
||||||
failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.data.accessToken}`;
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
createAuthRefreshInterceptor(axios, refreshAuthLogic);
|
|
||||||
|
|
||||||
const resolvePendingRequests = () => {
|
|
||||||
pendingRequests.map((callback: any) => callback());
|
|
||||||
pendingRequests = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolvePromise = (resolve: () => void) => {
|
|
||||||
pendingRequests.push(() => resolve());
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetPendingRequests = () => {
|
|
||||||
pendingRequests = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const setRefreshing = (newVal: boolean) => {
|
|
||||||
isRefreshing = newVal;
|
|
||||||
};
|
|
||||||
|
|
||||||
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
|
||||||
if (graphQLErrors) {
|
|
||||||
for (const err of graphQLErrors) {
|
|
||||||
if (err.extensions && err.extensions.code) {
|
|
||||||
switch (err.extensions.code) {
|
|
||||||
case 'UNAUTHENTICATED':
|
|
||||||
if (!isRefreshing) {
|
|
||||||
setRefreshing(true);
|
|
||||||
forward$ = fromPromise(
|
|
||||||
getNewToken()
|
|
||||||
.then((response: any) => {
|
|
||||||
setAccessToken(response.accessToken);
|
|
||||||
resolvePendingRequests();
|
|
||||||
return response.accessToken;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
resetPendingRequests();
|
|
||||||
// TODO
|
|
||||||
// Handle token refresh errors e.g clear stored tokens, redirect to login, ...
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setRefreshing(false);
|
|
||||||
}),
|
|
||||||
).filter(value => Boolean(value));
|
|
||||||
} else {
|
|
||||||
forward$ = fromPromise(new Promise(resolvePromise));
|
|
||||||
}
|
|
||||||
return forward$.flatMap(() => forward(operation));
|
|
||||||
default:
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (networkError) {
|
|
||||||
console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestLink = new ApolloLink(
|
|
||||||
(operation, forward) =>
|
|
||||||
new Observable((observer: any) => {
|
|
||||||
let handle: any;
|
|
||||||
Promise.resolve(operation)
|
|
||||||
.then((op: any) => {
|
|
||||||
const accessToken = getAccessToken();
|
|
||||||
if (accessToken) {
|
|
||||||
op.setContext({
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
handle = forward(operation).subscribe({
|
|
||||||
next: observer.next.bind(observer),
|
|
||||||
error: observer.error.bind(observer),
|
|
||||||
complete: observer.complete.bind(observer),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(observer.error.bind(observer));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (handle) {
|
|
||||||
handle.unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const client = new ApolloClient({
|
|
||||||
link: ApolloLink.from([
|
|
||||||
onError(({ graphQLErrors, networkError }) => {
|
|
||||||
if (graphQLErrors) {
|
|
||||||
graphQLErrors.forEach(
|
|
||||||
({ message, locations, path }) =>
|
|
||||||
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`), // eslint-disable-line no-console
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (networkError) {
|
|
||||||
console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
errorLink,
|
|
||||||
requestLink,
|
|
||||||
new HttpLink({
|
|
||||||
uri: '/graphql',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
cache,
|
|
||||||
});
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import TextareaAutosize from 'react-autosize-textarea/lib';
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ export const NameEditor: React.FC<NameEditorProps> = ({ onSave: handleSave, onCa
|
|||||||
<ListNameEditorWrapper>
|
<ListNameEditorWrapper>
|
||||||
<ListNameEditor
|
<ListNameEditor
|
||||||
ref={$editorRef}
|
ref={$editorRef}
|
||||||
|
height={40}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
value={listName}
|
value={listName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
|
||||||
|
@ -10,6 +10,7 @@ export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
|
|||||||
z-index: ${props => props.zIndex};
|
z-index: ${props => props.zIndex};
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
|
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
|
||||||
${props =>
|
${props =>
|
||||||
props.color === 'success' &&
|
props.color === 'success' &&
|
||||||
@ -18,6 +19,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
|
|||||||
stroke: ${props.theme.colors.success};
|
stroke: ${props.theme.colors.success};
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ClockIcon = styled(Clock)<{ color: string }>`
|
export const ClockIcon = styled(Clock)<{ color: string }>`
|
||||||
fill: ${props => props.color};
|
fill: ${props => props.color};
|
||||||
`;
|
`;
|
||||||
@ -26,7 +28,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
resize: none;
|
resize: none;
|
||||||
height: 90px;
|
height: 54px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import visit from 'unist-util-visit';
|
import visit from 'unist-util-visit';
|
||||||
import emoji from 'node-emoji';
|
import emoji from 'node-emoji';
|
||||||
import emoticon from 'emoticon';
|
import { emoticon } from 'emoticon';
|
||||||
import { Emoji } from 'emoji-mart';
|
import { Emoji } from 'emoji-mart';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,18 +0,0 @@
|
|||||||
let accessToken = '';
|
|
||||||
|
|
||||||
export function setAccessToken(newToken: string) {
|
|
||||||
console.log(newToken);
|
|
||||||
accessToken = newToken;
|
|
||||||
}
|
|
||||||
export function getAccessToken() {
|
|
||||||
return accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNewToken() {
|
|
||||||
return fetch('/auth/refresh_token', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
}).then(x => {
|
|
||||||
return x.json();
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import { PermissionObjectType, PermissionLevel } from 'App/context';
|
|
||||||
|
|
||||||
export default function userCan(level: PermissionLevel, objectType: PermissionObjectType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
5
frontend/src/taskcafe.d.ts
vendored
5
frontend/src/taskcafe.d.ts
vendored
@ -59,11 +59,6 @@ type User = TaskUser & {
|
|||||||
owned: RelatedList;
|
owned: RelatedList;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RefreshTokenResponse = {
|
|
||||||
accessToken: string;
|
|
||||||
setup?: null | { confirmToken: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoginFormData = {
|
type LoginFormData = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
@ -5397,10 +5397,10 @@ emojis-list@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||||
|
|
||||||
emoticon@^3.2.0:
|
emoticon@^4.0.0:
|
||||||
version "3.2.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f"
|
resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-4.0.0.tgz#827be62e6fee2a47517187992358adc5297ca320"
|
||||||
integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg==
|
integrity sha512-Of6PNiAQGHhprBEootT77SdsynrVD97v1nzpiJ+FB2XCyvZmwdVI+hlpUduKWb2+7uVOcE1Myh3QAVB8vEBXvw==
|
||||||
|
|
||||||
encodeurl@~1.0.2:
|
encodeurl@~1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
6
go.mod
6
go.mod
@ -3,11 +3,11 @@ module github.com/jordanknott/taskcafe
|
|||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.11.3
|
github.com/99designs/gqlgen v0.13.0
|
||||||
github.com/RichardKnop/machinery v1.9.1
|
github.com/RichardKnop/machinery v1.9.1
|
||||||
github.com/brianvoe/gofakeit/v5 v5.11.2
|
github.com/brianvoe/gofakeit/v5 v5.11.2
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
|
||||||
github.com/go-chi/chi v3.3.2+incompatible
|
github.com/go-chi/chi v3.3.2+incompatible
|
||||||
|
github.com/go-chi/cors v1.2.0
|
||||||
github.com/golang-migrate/migrate/v4 v4.11.0
|
github.com/golang-migrate/migrate/v4 v4.11.0
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/jinzhu/now v1.1.1
|
github.com/jinzhu/now v1.1.1
|
||||||
@ -24,7 +24,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.0.0
|
github.com/spf13/cobra v1.0.0
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/spf13/viper v1.4.0
|
github.com/spf13/viper v1.4.0
|
||||||
github.com/vektah/gqlparser/v2 v2.0.1
|
github.com/vektah/gqlparser/v2 v2.1.0
|
||||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
|
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
|
19
go.sum
19
go.sum
@ -40,8 +40,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
|||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
|
github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA=
|
||||||
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
|
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
@ -96,9 +96,6 @@ github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwj
|
|||||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||||
github.com/brianvoe/gofakeit v1.2.0 h1:GGbzCqQx9ync4ObAUhRa3F/M73eL9VZL3X09WoTwphM=
|
|
||||||
github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8=
|
|
||||||
github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc=
|
|
||||||
github.com/brianvoe/gofakeit/v5 v5.11.2 h1:Ny5Nsf4z2023ZvYP8ujW8p5B1t5sxhdFaQ/0IYXbeSA=
|
github.com/brianvoe/gofakeit/v5 v5.11.2 h1:Ny5Nsf4z2023ZvYP8ujW8p5B1t5sxhdFaQ/0IYXbeSA=
|
||||||
github.com/brianvoe/gofakeit/v5 v5.11.2/go.mod h1:/ZENnKqX+XrN8SORLe/fu5lZDIo1tuPncWuRD+eyhSI=
|
github.com/brianvoe/gofakeit/v5 v5.11.2/go.mod h1:/ZENnKqX+XrN8SORLe/fu5lZDIo1tuPncWuRD+eyhSI=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@ -125,6 +122,7 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7
|
|||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||||
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-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/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/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 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk=
|
||||||
@ -166,6 +164,8 @@ github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0
|
|||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
|
github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
|
||||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
|
github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
|
||||||
|
github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
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-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-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@ -293,10 +293,10 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
|||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
|
||||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
@ -550,6 +550,7 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
|||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
|
||||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
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 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||||
@ -559,8 +560,8 @@ github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy
|
|||||||
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4=
|
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 h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
|
||||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
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=
|
github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
|
||||||
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
||||||
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
||||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
|
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
|
||||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RestrictedMode is used restrict JWT access to just the install route
|
|
||||||
type RestrictedMode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Unrestricted is the code to allow access to all routes
|
|
||||||
Unrestricted RestrictedMode = "unrestricted"
|
|
||||||
// InstallOnly is the code to restrict access ONLY to install route
|
|
||||||
InstallOnly = "install_only"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Role is the role code for the user
|
|
||||||
type Role string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// RoleAdmin is the code for the admin role
|
|
||||||
RoleAdmin Role = "admin"
|
|
||||||
// RoleMember is the code for the member role
|
|
||||||
RoleMember Role = "member"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AccessTokenClaims is the claims the access JWT token contains
|
|
||||||
type AccessTokenClaims struct {
|
|
||||||
UserID string `json:"userId"`
|
|
||||||
Restricted RestrictedMode `json:"restricted"`
|
|
||||||
OrgRole Role `json:"orgRole"`
|
|
||||||
jwt.StandardClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrExpiredToken is the error returned if the token has expired
|
|
||||||
type ErrExpiredToken struct{}
|
|
||||||
|
|
||||||
// Error returns the error message for ErrExpiredToken
|
|
||||||
func (r *ErrExpiredToken) Error() string {
|
|
||||||
return "token is expired"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrMalformedToken is the error returned if the token has malformed
|
|
||||||
type ErrMalformedToken struct{}
|
|
||||||
|
|
||||||
// Error returns the error message for ErrMalformedToken
|
|
||||||
func (r *ErrMalformedToken) Error() string {
|
|
||||||
return "token is malformed"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAccessToken generates a new JWT access token with the correct claims
|
|
||||||
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string, jwtKey []byte, expirationTime time.Duration) (string, error) {
|
|
||||||
role := RoleMember
|
|
||||||
if orgRole == "admin" {
|
|
||||||
role = RoleAdmin
|
|
||||||
}
|
|
||||||
accessExpirationTime := time.Now().Add(expirationTime)
|
|
||||||
accessClaims := &AccessTokenClaims{
|
|
||||||
UserID: userID,
|
|
||||||
Restricted: restrictedMode,
|
|
||||||
OrgRole: role,
|
|
||||||
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
|
||||||
accessTokenString, err := accessToken.SignedString(jwtKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return accessTokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAccessTokenCustomExpiration creates an access token with a custom duration
|
|
||||||
func NewAccessTokenCustomExpiration(userID string, dur time.Duration, jwtKey []byte) (string, error) {
|
|
||||||
accessExpirationTime := time.Now().Add(dur)
|
|
||||||
accessClaims := &AccessTokenClaims{
|
|
||||||
UserID: userID,
|
|
||||||
Restricted: Unrestricted,
|
|
||||||
OrgRole: RoleMember,
|
|
||||||
StandardClaims: jwt.StandardClaims{ExpiresAt: accessExpirationTime.Unix()},
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
|
||||||
accessTokenString, err := accessToken.SignedString(jwtKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return accessTokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAccessToken validates a JWT access token and returns the contained claims or an error if it's invalid
|
|
||||||
func ValidateAccessToken(accessTokenString string, jwtKey []byte) (AccessTokenClaims, error) {
|
|
||||||
accessClaims := &AccessTokenClaims{}
|
|
||||||
accessToken, err := jwt.ParseWithClaims(accessTokenString, accessClaims, func(token *jwt.Token) (interface{}, error) {
|
|
||||||
return jwtKey, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if accessToken.Valid {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"token": accessTokenString,
|
|
||||||
"timeToExpire": time.Unix(accessClaims.ExpiresAt, 0),
|
|
||||||
}).Debug("token is valid")
|
|
||||||
return *accessClaims, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if ve, ok := err.(*jwt.ValidationError); ok {
|
|
||||||
if ve.Errors&(jwt.ValidationErrorMalformed|jwt.ValidationErrorSignatureInvalid) != 0 {
|
|
||||||
return AccessTokenClaims{}, &ErrMalformedToken{}
|
|
||||||
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
|
|
||||||
return AccessTokenClaims{}, &ErrExpiredToken{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return AccessTokenClaims{}, err
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Override time value for jwt tests. Restore default value after.
|
|
||||||
func at(t time.Time, f func()) {
|
|
||||||
jwt.TimeFunc = func() time.Time {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
f()
|
|
||||||
jwt.TimeFunc = time.Now
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuth_ValidateAccessToken(t *testing.T) {
|
|
||||||
expectedToken := AccessTokenClaims{
|
|
||||||
UserID: "1234",
|
|
||||||
Restricted: "unrestricted",
|
|
||||||
OrgRole: "member",
|
|
||||||
StandardClaims: jwt.StandardClaims{ExpiresAt: 1000},
|
|
||||||
}
|
|
||||||
// jwt with the claims of expectedToken signed by secretKey
|
|
||||||
jwtString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0IiwicmVzdHJpY3RlZCI6InVucmVzdHJpY3RlZCIsIm9yZ1JvbGUiOiJtZW1iZXIiLCJleHAiOjEwMDB9.Zc4mrnogDccYffA7dWogdWsZMELftQluh2X5xDyzOpA"
|
|
||||||
secretKey := []byte("secret")
|
|
||||||
|
|
||||||
// Check that decrypt failure is detected
|
|
||||||
token, err := ValidateAccessToken(jwtString, []byte("incorrectSecret"))
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("[IncorrectKey] Expected an error when validating a token with the incorrect key, instead got token %v", token)
|
|
||||||
} else if _, ok := err.(*ErrMalformedToken); !ok {
|
|
||||||
t.Errorf("[IncorrectKey] Expected an ErrMalformedToken error when validating a token with the incorrect key, instead got error %T:%v", err, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that token expiration check works
|
|
||||||
token, err = ValidateAccessToken(jwtString, secretKey)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("[TokenExpired] Expected an error when validating an expired token, instead got token %v", token)
|
|
||||||
} else if _, ok := err.(*ErrExpiredToken); !ok {
|
|
||||||
t.Errorf("[TokenExpired] Expected an ErrExpiredToken error when validating an expired token, instead got error %T:%v", err, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that token validation works with a valid token
|
|
||||||
// Set the time to be valid for the token expiration
|
|
||||||
at(time.Unix(500, 0), func() {
|
|
||||||
token, err = ValidateAccessToken(jwtString, secretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("[TokenValid] Expected no errors when validating token, instead got err %v", err)
|
|
||||||
} else if token != expectedToken {
|
|
||||||
t.Errorf("[TokenValid] Expected token with claims %v but instead had claims %v", expectedToken, token)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -86,6 +86,6 @@ func Execute() {
|
|||||||
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate(VersionTemplate())
|
rootCmd.SetVersionTemplate(VersionTemplate())
|
||||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd())
|
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd())
|
||||||
rootCmd.Execute()
|
rootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTokenCmd() *cobra.Command {
|
|
||||||
return &cobra.Command{
|
|
||||||
Use: "token",
|
|
||||||
Short: "Create a long lived JWT token for dev purposes",
|
|
||||||
Long: "Create a long lived JWT token for dev purposes",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
secret := viper.GetString("server.secret")
|
|
||||||
if strings.TrimSpace(secret) == "" {
|
|
||||||
return errors.New("server.secret must be set (TASKCAFE_SERVER_SECRET)")
|
|
||||||
}
|
|
||||||
token, err := auth.NewAccessTokenCustomExpiration(args[0], time.Hour*24, []byte(secret))
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("issue while creating access token")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(token)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,6 +10,13 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AuthToken struct {
|
||||||
|
TokenID uuid.UUID `json:"token_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type LabelColor struct {
|
type LabelColor struct {
|
||||||
LabelColorID uuid.UUID `json:"label_color_id"`
|
LabelColorID uuid.UUID `json:"label_color_id"`
|
||||||
ColorHex string `json:"color_hex"`
|
ColorHex string `json:"color_hex"`
|
||||||
@ -74,13 +81,6 @@ type ProjectMemberInvited struct {
|
|||||||
UserAccountInvitedID uuid.UUID `json:"user_account_invited_id"`
|
UserAccountInvitedID uuid.UUID `json:"user_account_invited_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefreshToken struct {
|
|
||||||
TokenID uuid.UUID `json:"token_id"`
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Role struct {
|
type Role struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Querier interface {
|
type Querier interface {
|
||||||
|
CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, error)
|
||||||
CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
||||||
CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error)
|
CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error)
|
||||||
CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
|
CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
|
||||||
@ -20,7 +21,6 @@ type Querier interface {
|
|||||||
CreatePersonalProjectLink(ctx context.Context, arg CreatePersonalProjectLinkParams) (PersonalProject, error)
|
CreatePersonalProjectLink(ctx context.Context, arg CreatePersonalProjectLinkParams) (PersonalProject, error)
|
||||||
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
|
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
|
||||||
CreateProjectMember(ctx context.Context, arg CreateProjectMemberParams) (ProjectMember, error)
|
CreateProjectMember(ctx context.Context, arg CreateProjectMemberParams) (ProjectMember, error)
|
||||||
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
|
|
||||||
CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error)
|
CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error)
|
||||||
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
|
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
|
||||||
CreateTaskActivity(ctx context.Context, arg CreateTaskActivityParams) (TaskActivity, error)
|
CreateTaskActivity(ctx context.Context, arg CreateTaskActivityParams) (TaskActivity, error)
|
||||||
@ -35,6 +35,8 @@ type Querier interface {
|
|||||||
CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
|
CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
|
||||||
CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
|
CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
|
||||||
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
|
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
|
||||||
|
DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error
|
||||||
|
DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error
|
||||||
DeleteConfirmTokenForEmail(ctx context.Context, email string) error
|
DeleteConfirmTokenForEmail(ctx context.Context, email string) error
|
||||||
DeleteExpiredTokens(ctx context.Context) error
|
DeleteExpiredTokens(ctx context.Context) error
|
||||||
DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error
|
DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error
|
||||||
@ -43,8 +45,6 @@ type Querier interface {
|
|||||||
DeleteProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) error
|
DeleteProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) error
|
||||||
DeleteProjectMember(ctx context.Context, arg DeleteProjectMemberParams) error
|
DeleteProjectMember(ctx context.Context, arg DeleteProjectMemberParams) error
|
||||||
DeleteProjectMemberInvitedForEmail(ctx context.Context, email string) 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)
|
DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error)
|
||||||
DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
|
DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
|
||||||
DeleteTaskChecklistByID(ctx context.Context, taskChecklistID uuid.UUID) error
|
DeleteTaskChecklistByID(ctx context.Context, taskChecklistID uuid.UUID) error
|
||||||
@ -71,6 +71,7 @@ type Querier interface {
|
|||||||
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
|
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
|
||||||
GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error)
|
GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error)
|
||||||
GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, error)
|
GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, error)
|
||||||
|
GetAuthTokenByID(ctx context.Context, tokenID uuid.UUID) (AuthToken, error)
|
||||||
GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error)
|
GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error)
|
||||||
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
||||||
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
|
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
|
||||||
@ -100,7 +101,6 @@ type Querier interface {
|
|||||||
GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error)
|
GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error)
|
||||||
GetProjectsForInvitedMember(ctx context.Context, email string) ([]uuid.UUID, error)
|
GetProjectsForInvitedMember(ctx context.Context, email string) ([]uuid.UUID, error)
|
||||||
GetRecentlyAssignedTaskForUserID(ctx context.Context, arg GetRecentlyAssignedTaskForUserIDParams) ([]Task, error)
|
GetRecentlyAssignedTaskForUserID(ctx context.Context, arg GetRecentlyAssignedTaskForUserIDParams) ([]Task, error)
|
||||||
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
|
|
||||||
GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
|
GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
|
||||||
GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (Role, error)
|
GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (Role, error)
|
||||||
GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRoleForUserIDRow, error)
|
GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRoleForUserIDRow, error)
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
-- name: GetRefreshTokenByID :one
|
-- name: GetAuthTokenByID :one
|
||||||
SELECT * FROM refresh_token WHERE token_id = $1;
|
SELECT * FROM auth_token WHERE token_id = $1;
|
||||||
|
|
||||||
-- name: CreateRefreshToken :one
|
-- name: CreateAuthToken :one
|
||||||
INSERT INTO refresh_token (user_id, created_at, expires_at) VALUES ($1, $2, $3) RETURNING *;
|
INSERT INTO auth_token (user_id, created_at, expires_at) VALUES ($1, $2, $3) RETURNING *;
|
||||||
|
|
||||||
-- name: DeleteRefreshTokenByID :exec
|
-- name: DeleteAuthTokenByID :exec
|
||||||
DELETE FROM refresh_token WHERE token_id = $1;
|
DELETE FROM auth_token WHERE token_id = $1;
|
||||||
|
|
||||||
-- name: DeleteRefreshTokenByUserID :exec
|
-- name: DeleteAuthTokenByUserID :exec
|
||||||
DELETE FROM refresh_token WHERE user_id = $1;
|
DELETE FROM auth_token WHERE user_id = $1;
|
||||||
|
|
||||||
-- name: DeleteExpiredTokens :exec
|
-- name: DeleteExpiredTokens :exec
|
||||||
DELETE FROM refresh_token WHERE expires_at <= NOW();
|
DELETE FROM auth_token WHERE expires_at <= NOW();
|
||||||
|
@ -10,19 +10,19 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createRefreshToken = `-- name: CreateRefreshToken :one
|
const createAuthToken = `-- name: CreateAuthToken :one
|
||||||
INSERT INTO refresh_token (user_id, created_at, expires_at) VALUES ($1, $2, $3) RETURNING token_id, user_id, created_at, expires_at
|
INSERT INTO auth_token (user_id, created_at, expires_at) VALUES ($1, $2, $3) RETURNING token_id, user_id, created_at, expires_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateRefreshTokenParams struct {
|
type CreateAuthTokenParams struct {
|
||||||
UserID uuid.UUID `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) {
|
func (q *Queries) CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, error) {
|
||||||
row := q.db.QueryRowContext(ctx, createRefreshToken, arg.UserID, arg.CreatedAt, arg.ExpiresAt)
|
row := q.db.QueryRowContext(ctx, createAuthToken, arg.UserID, arg.CreatedAt, arg.ExpiresAt)
|
||||||
var i RefreshToken
|
var i AuthToken
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.TokenID,
|
&i.TokenID,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
@ -32,8 +32,26 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteAuthTokenByID = `-- name: DeleteAuthTokenByID :exec
|
||||||
|
DELETE FROM auth_token WHERE token_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteAuthTokenByID, tokenID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAuthTokenByUserID = `-- name: DeleteAuthTokenByUserID :exec
|
||||||
|
DELETE FROM auth_token WHERE user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteAuthTokenByUserID, userID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteExpiredTokens = `-- name: DeleteExpiredTokens :exec
|
const deleteExpiredTokens = `-- name: DeleteExpiredTokens :exec
|
||||||
DELETE FROM refresh_token WHERE expires_at <= NOW()
|
DELETE FROM auth_token WHERE expires_at <= NOW()
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeleteExpiredTokens(ctx context.Context) error {
|
func (q *Queries) DeleteExpiredTokens(ctx context.Context) error {
|
||||||
@ -41,31 +59,13 @@ func (q *Queries) DeleteExpiredTokens(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRefreshTokenByID = `-- name: DeleteRefreshTokenByID :exec
|
const getAuthTokenByID = `-- name: GetAuthTokenByID :one
|
||||||
DELETE FROM refresh_token WHERE token_id = $1
|
SELECT token_id, user_id, created_at, expires_at FROM auth_token WHERE token_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error {
|
func (q *Queries) GetAuthTokenByID(ctx context.Context, tokenID uuid.UUID) (AuthToken, error) {
|
||||||
_, err := q.db.ExecContext(ctx, deleteRefreshTokenByID, tokenID)
|
row := q.db.QueryRowContext(ctx, getAuthTokenByID, tokenID)
|
||||||
return err
|
var i AuthToken
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRefreshTokenByUserID = `-- name: DeleteRefreshTokenByUserID :exec
|
|
||||||
DELETE FROM refresh_token WHERE user_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteRefreshTokenByUserID, userID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRefreshTokenByID = `-- name: GetRefreshTokenByID :one
|
|
||||||
SELECT token_id, user_id, created_at, expires_at FROM refresh_token WHERE token_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getRefreshTokenByID, tokenID)
|
|
||||||
var i RefreshToken
|
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.TokenID,
|
&i.TokenID,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,6 @@ import (
|
|||||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||||
"github.com/99designs/gqlgen/graphql/playground"
|
"github.com/99designs/gqlgen/graphql/playground"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/logger"
|
"github.com/jordanknott/taskcafe/internal/logger"
|
||||||
"github.com/jordanknott/taskcafe/internal/utils"
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
@ -34,6 +33,8 @@ func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
|
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
|
||||||
|
/*
|
||||||
|
TODO: add permission check
|
||||||
role, ok := GetUserRole(ctx)
|
role, ok := GetUserRole(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("user ID is missing")
|
return nil, errors.New("user ID is missing")
|
||||||
@ -43,6 +44,7 @@ func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler
|
|||||||
} else if level == ActionLevelOrg {
|
} else if level == ActionLevelOrg {
|
||||||
return nil, errors.New("must be an org admin")
|
return nil, errors.New("must be an org admin")
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
var subjectID uuid.UUID
|
var subjectID uuid.UUID
|
||||||
in := graphql.GetFieldContext(ctx).Args["input"]
|
in := graphql.GetFieldContext(ctx).Args["input"]
|
||||||
@ -76,7 +78,7 @@ func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler
|
|||||||
// TODO: add config setting to disable personal projects
|
// TODO: add config setting to disable personal projects
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
subjectID, ok = subjectField.Interface().(uuid.UUID)
|
subjectID, ok := subjectField.Interface().(uuid.UUID)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.New(ctx).Error("error while casting subject UUID")
|
logger.New(ctx).Error("error while casting subject UUID")
|
||||||
return nil, errors.New("error while casting subject uuid")
|
return nil, errors.New("error while casting subject uuid")
|
||||||
@ -190,23 +192,10 @@ func GetUserID(ctx context.Context) (uuid.UUID, bool) {
|
|||||||
return userID, ok
|
return userID, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserRole retrieves the user role out of a context
|
|
||||||
func GetUserRole(ctx context.Context) (auth.Role, bool) {
|
|
||||||
role, ok := ctx.Value(utils.OrgRoleKey).(auth.Role)
|
|
||||||
return role, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUser retrieves both the user id & user role out of a context
|
// GetUser retrieves both the user id & user role out of a context
|
||||||
func GetUser(ctx context.Context) (uuid.UUID, auth.Role, bool) {
|
func GetUser(ctx context.Context) (uuid.UUID, bool) {
|
||||||
userID, userOK := GetUserID(ctx)
|
userID, userOK := GetUserID(ctx)
|
||||||
role, roleOK := GetUserRole(ctx)
|
return userID, userOK
|
||||||
return userID, role, userOK && roleOK
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRestrictedMode retrieves the restricted mode code out of a context
|
|
||||||
func GetRestrictedMode(ctx context.Context) (auth.RestrictedMode, bool) {
|
|
||||||
restricted, ok := ctx.Value(utils.RestrictedModeKey).(auth.RestrictedMode)
|
|
||||||
return restricted, ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProjectRoles retrieves the team & project role for the given project ID
|
// GetProjectRoles retrieves the team & project role for the given project ID
|
||||||
|
@ -255,6 +255,7 @@ type LogoutUser struct {
|
|||||||
|
|
||||||
type MePayload struct {
|
type MePayload struct {
|
||||||
User *db.UserAccount `json:"user"`
|
User *db.UserAccount `json:"user"`
|
||||||
|
Organization *RoleCode `json:"organization"`
|
||||||
TeamRoles []TeamRole `json:"teamRoles"`
|
TeamRoles []TeamRole `json:"teamRoles"`
|
||||||
ProjectRoles []ProjectRole `json:"projectRoles"`
|
ProjectRoles []ProjectRole `json:"projectRoles"`
|
||||||
}
|
}
|
||||||
@ -312,10 +313,6 @@ type NewProjectLabel struct {
|
|||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NewRefreshToken struct {
|
|
||||||
UserID uuid.UUID `json:"userID"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NewTask struct {
|
type NewTask struct {
|
||||||
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -382,6 +379,12 @@ type ProfileIcon struct {
|
|||||||
BgColor *string `json:"bgColor"`
|
BgColor *string `json:"bgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectPermission struct {
|
||||||
|
Team RoleCode `json:"team"`
|
||||||
|
Project RoleCode `json:"project"`
|
||||||
|
Org RoleCode `json:"org"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProjectRole struct {
|
type ProjectRole struct {
|
||||||
ProjectID uuid.UUID `json:"projectID"`
|
ProjectID uuid.UUID `json:"projectID"`
|
||||||
RoleCode RoleCode `json:"roleCode"`
|
RoleCode RoleCode `json:"roleCode"`
|
||||||
@ -435,6 +438,11 @@ type TaskPositionUpdate struct {
|
|||||||
Position float64 `json:"position"`
|
Position float64 `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamPermission struct {
|
||||||
|
Team RoleCode `json:"team"`
|
||||||
|
Org RoleCode `json:"org"`
|
||||||
|
}
|
||||||
|
|
||||||
type TeamRole struct {
|
type TeamRole struct {
|
||||||
TeamID uuid.UUID `json:"teamID"`
|
TeamID uuid.UUID `json:"teamID"`
|
||||||
RoleCode RoleCode `json:"roleCode"`
|
RoleCode RoleCode `json:"roleCode"`
|
||||||
|
@ -50,13 +50,6 @@ type Member {
|
|||||||
member: MemberList!
|
member: MemberList!
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefreshToken {
|
|
||||||
id: ID!
|
|
||||||
userId: UUID!
|
|
||||||
expiresAt: Time!
|
|
||||||
createdAt: Time!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Role {
|
type Role {
|
||||||
code: String!
|
code: String!
|
||||||
name: String!
|
name: String!
|
||||||
@ -97,6 +90,7 @@ type Team {
|
|||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
name: String!
|
name: String!
|
||||||
|
permission: TeamPermission!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +100,17 @@ type InvitedMember {
|
|||||||
invitedOn: Time!
|
invitedOn: Time!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamPermission {
|
||||||
|
team: RoleCode!
|
||||||
|
org: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectPermission {
|
||||||
|
team: RoleCode!
|
||||||
|
project: RoleCode!
|
||||||
|
org: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
type Project {
|
type Project {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
@ -114,6 +119,7 @@ type Project {
|
|||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
invitedMembers: [InvitedMember!]!
|
invitedMembers: [InvitedMember!]!
|
||||||
|
permission: ProjectPermission!
|
||||||
labels: [ProjectLabel!]!
|
labels: [ProjectLabel!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,6 +320,7 @@ type ProjectRole {
|
|||||||
|
|
||||||
type MePayload {
|
type MePayload {
|
||||||
user: UserAccount!
|
user: UserAccount!
|
||||||
|
organization: RoleCode
|
||||||
teamRoles: [TeamRole!]!
|
teamRoles: [TeamRole!]!
|
||||||
projectRoles: [ProjectRole!]!
|
projectRoles: [ProjectRole!]!
|
||||||
}
|
}
|
||||||
@ -881,7 +888,6 @@ type UpdateTeamMemberRolePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
|
||||||
createUserAccount(input: NewUserAccount!):
|
createUserAccount(input: NewUserAccount!):
|
||||||
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
deleteUserAccount(input: DeleteUserAccount!):
|
deleteUserAccount(input: DeleteUserAccount!):
|
||||||
@ -954,10 +960,6 @@ type UpdateUserRolePayload {
|
|||||||
user: UserAccount!
|
user: UserAccount!
|
||||||
}
|
}
|
||||||
|
|
||||||
input NewRefreshToken {
|
|
||||||
userID: UUID!
|
|
||||||
}
|
|
||||||
|
|
||||||
input NewUserAccount {
|
input NewUserAccount {
|
||||||
username: String!
|
username: String!
|
||||||
email: String!
|
email: String!
|
||||||
|
@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jinzhu/now"
|
"github.com/jinzhu/now"
|
||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/logger"
|
"github.com/jordanknott/taskcafe/internal/logger"
|
||||||
"github.com/jordanknott/taskcafe/internal/utils"
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
@ -864,11 +863,12 @@ func (r *mutationResolver) DeleteTeam(ctx context.Context, input DeleteTeam) (*D
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
|
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
|
||||||
_, role, ok := GetUser(ctx)
|
_, ok := GetUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &db.Team{}, nil
|
return &db.Team{}, nil
|
||||||
}
|
}
|
||||||
if role == auth.RoleAdmin {
|
// if role == auth.RoleAdmin { // TODO: add permision check
|
||||||
|
if true {
|
||||||
createdAt := time.Now().UTC()
|
createdAt := time.Now().UTC()
|
||||||
team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
|
team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
|
||||||
return &team, err
|
return &team, err
|
||||||
@ -944,20 +944,13 @@ func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTea
|
|||||||
return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, err
|
return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*db.RefreshToken, error) {
|
|
||||||
userID := uuid.MustParse("0183d9ab-d0ed-4c9b-a3df-77a0cdd93dca")
|
|
||||||
refreshCreatedAt := time.Now().UTC()
|
|
||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
|
||||||
refreshToken, err := r.Repository.CreateRefreshToken(ctx, db.CreateRefreshTokenParams{userID, refreshCreatedAt, refreshExpiresAt})
|
|
||||||
return &refreshToken, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
|
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
|
||||||
_, role, ok := GetUser(ctx)
|
_, ok := GetUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &db.UserAccount{}, nil
|
return &db.UserAccount{}, nil
|
||||||
}
|
}
|
||||||
if role != auth.RoleAdmin {
|
// if role != auth.RoleAdmin { TODO: add permsion check
|
||||||
|
if true {
|
||||||
return &db.UserAccount{}, &gqlerror.Error{
|
return &db.UserAccount{}, &gqlerror.Error{
|
||||||
Message: "Must be an organization admin",
|
Message: "Must be an organization admin",
|
||||||
Extensions: map[string]interface{}{
|
Extensions: map[string]interface{}{
|
||||||
@ -984,11 +977,12 @@ func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserA
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
|
func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
|
||||||
_, role, ok := GetUser(ctx)
|
_, ok := GetUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &DeleteUserAccountPayload{Ok: false}, nil
|
return &DeleteUserAccountPayload{Ok: false}, nil
|
||||||
}
|
}
|
||||||
if role != auth.RoleAdmin {
|
// if role != auth.RoleAdmin { TODO: add permision check
|
||||||
|
if true {
|
||||||
return &DeleteUserAccountPayload{Ok: false}, &gqlerror.Error{
|
return &DeleteUserAccountPayload{Ok: false}, &gqlerror.Error{
|
||||||
Message: "User not found",
|
Message: "User not found",
|
||||||
Extensions: map[string]interface{}{
|
Extensions: map[string]interface{}{
|
||||||
@ -1030,7 +1024,7 @@ func (r *mutationResolver) DeleteInvitedUserAccount(ctx context.Context, input D
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
|
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
|
||||||
err := r.Repository.DeleteRefreshTokenByUserID(ctx, input.UserID)
|
err := r.Repository.DeleteAuthTokenByUserID(ctx, input.UserID)
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1059,11 +1053,12 @@ func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateU
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
|
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
|
||||||
_, role, ok := GetUser(ctx)
|
_, ok := GetUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &UpdateUserRolePayload{}, nil
|
return &UpdateUserRolePayload{}, nil
|
||||||
}
|
}
|
||||||
if role != auth.RoleAdmin {
|
// if role != auth.RoleAdmin { TODO: add permision check
|
||||||
|
if true {
|
||||||
return &UpdateUserRolePayload{}, &gqlerror.Error{
|
return &UpdateUserRolePayload{}, &gqlerror.Error{
|
||||||
Message: "User not found",
|
Message: "User not found",
|
||||||
Extensions: map[string]interface{}{
|
Extensions: map[string]interface{}{
|
||||||
@ -1211,6 +1206,10 @@ func (r *projectResolver) InvitedMembers(ctx context.Context, obj *db.Project) (
|
|||||||
return invited, err
|
return invited, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectResolver) Permission(ctx context.Context, obj *db.Project) (*ProjectPermission, error) {
|
||||||
|
panic(fmt.Errorf("not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
func (r *projectResolver) Labels(ctx context.Context, obj *db.Project) ([]db.ProjectLabel, error) {
|
func (r *projectResolver) Labels(ctx context.Context, obj *db.Project) ([]db.ProjectLabel, error) {
|
||||||
labels, err := r.Repository.GetProjectLabelsForProject(ctx, obj.ProjectID)
|
labels, err := r.Repository.GetProjectLabelsForProject(ctx, obj.ProjectID)
|
||||||
return labels, err
|
return labels, err
|
||||||
@ -1296,7 +1295,7 @@ func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]db.Project, error) {
|
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]db.Project, error) {
|
||||||
userID, orgRole, ok := GetUser(ctx)
|
userID, ok := GetUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.New(ctx).Info("user id was not found from middleware")
|
logger.New(ctx).Info("user id was not found from middleware")
|
||||||
return []db.Project{}, nil
|
return []db.Project{}, nil
|
||||||
@ -1309,11 +1308,14 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]
|
|||||||
|
|
||||||
var teams []db.Team
|
var teams []db.Team
|
||||||
var err error
|
var err error
|
||||||
|
/* TODO: add permsion check
|
||||||
if orgRole == "admin" {
|
if orgRole == "admin" {
|
||||||
teams, err = r.Repository.GetAllTeams(ctx)
|
teams, err = r.Repository.GetAllTeams(ctx)
|
||||||
} else {
|
} else {
|
||||||
teams, err = r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
teams, err = r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
teams, err = r.Repository.GetAllTeams(ctx)
|
||||||
|
|
||||||
projects := make(map[string]db.Project)
|
projects := make(map[string]db.Project)
|
||||||
for _, team := range teams {
|
for _, team := range teams {
|
||||||
@ -1359,15 +1361,18 @@ func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||||
userID, orgRole, ok := GetUser(ctx)
|
userID, ok := GetUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.New(ctx).Error("userID or org role does not exist")
|
logger.New(ctx).Error("userID or org role does not exist")
|
||||||
return []db.Team{}, errors.New("internal error")
|
return []db.Team{}, errors.New("internal error")
|
||||||
}
|
}
|
||||||
if orgRole == "admin" {
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: add permision check
|
||||||
|
if orgRole == "admin" {
|
||||||
return r.Repository.GetAllTeams(ctx)
|
return r.Repository.GetAllTeams(ctx)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
teams := make(map[string]db.Team)
|
teams := make(map[string]db.Team)
|
||||||
adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
||||||
@ -1596,10 +1601,6 @@ func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFil
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
|
|
||||||
return obj.TokenID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *taskResolver) ID(ctx context.Context, obj *db.Task) (uuid.UUID, error) {
|
func (r *taskResolver) ID(ctx context.Context, obj *db.Task) (uuid.UUID, error) {
|
||||||
return obj.TaskID, nil
|
return obj.TaskID, nil
|
||||||
}
|
}
|
||||||
@ -1848,6 +1849,10 @@ func (r *teamResolver) ID(ctx context.Context, obj *db.Team) (uuid.UUID, error)
|
|||||||
return obj.TeamID, nil
|
return obj.TeamID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *teamResolver) Permission(ctx context.Context, obj *db.Team) (*TeamPermission, error) {
|
||||||
|
panic(fmt.Errorf("not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) {
|
func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) {
|
||||||
members := []Member{}
|
members := []Member{}
|
||||||
|
|
||||||
@ -1966,9 +1971,6 @@ func (r *Resolver) ProjectLabel() ProjectLabelResolver { return &projectLabelRes
|
|||||||
// Query returns QueryResolver implementation.
|
// Query returns QueryResolver implementation.
|
||||||
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
// RefreshToken returns RefreshTokenResolver implementation.
|
|
||||||
func (r *Resolver) RefreshToken() RefreshTokenResolver { return &refreshTokenResolver{r} }
|
|
||||||
|
|
||||||
// Task returns TaskResolver implementation.
|
// Task returns TaskResolver implementation.
|
||||||
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
|
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
|
||||||
|
|
||||||
@ -2005,7 +2007,6 @@ type organizationResolver struct{ *Resolver }
|
|||||||
type projectResolver struct{ *Resolver }
|
type projectResolver struct{ *Resolver }
|
||||||
type projectLabelResolver struct{ *Resolver }
|
type projectLabelResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
type refreshTokenResolver struct{ *Resolver }
|
|
||||||
type taskResolver struct{ *Resolver }
|
type taskResolver struct{ *Resolver }
|
||||||
type taskActivityResolver struct{ *Resolver }
|
type taskActivityResolver struct{ *Resolver }
|
||||||
type taskChecklistResolver struct{ *Resolver }
|
type taskChecklistResolver struct{ *Resolver }
|
||||||
|
@ -50,13 +50,6 @@ type Member {
|
|||||||
member: MemberList!
|
member: MemberList!
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefreshToken {
|
|
||||||
id: ID!
|
|
||||||
userId: UUID!
|
|
||||||
expiresAt: Time!
|
|
||||||
createdAt: Time!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Role {
|
type Role {
|
||||||
code: String!
|
code: String!
|
||||||
name: String!
|
name: String!
|
||||||
@ -97,6 +90,7 @@ type Team {
|
|||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
name: String!
|
name: String!
|
||||||
|
permission: TeamPermission!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +100,17 @@ type InvitedMember {
|
|||||||
invitedOn: Time!
|
invitedOn: Time!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamPermission {
|
||||||
|
team: RoleCode!
|
||||||
|
org: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectPermission {
|
||||||
|
team: RoleCode!
|
||||||
|
project: RoleCode!
|
||||||
|
org: RoleCode!
|
||||||
|
}
|
||||||
|
|
||||||
type Project {
|
type Project {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
@ -114,6 +119,7 @@ type Project {
|
|||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
invitedMembers: [InvitedMember!]!
|
invitedMembers: [InvitedMember!]!
|
||||||
|
permission: ProjectPermission!
|
||||||
labels: [ProjectLabel!]!
|
labels: [ProjectLabel!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ type ProjectRole {
|
|||||||
|
|
||||||
type MePayload {
|
type MePayload {
|
||||||
user: UserAccount!
|
user: UserAccount!
|
||||||
|
organization: RoleCode
|
||||||
teamRoles: [TeamRole!]!
|
teamRoles: [TeamRole!]!
|
||||||
projectRoles: [ProjectRole!]!
|
projectRoles: [ProjectRole!]!
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
|
||||||
createUserAccount(input: NewUserAccount!):
|
createUserAccount(input: NewUserAccount!):
|
||||||
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
deleteUserAccount(input: DeleteUserAccount!):
|
deleteUserAccount(input: DeleteUserAccount!):
|
||||||
@ -72,10 +71,6 @@ type UpdateUserRolePayload {
|
|||||||
user: UserAccount!
|
user: UserAccount!
|
||||||
}
|
}
|
||||||
|
|
||||||
input NewRefreshToken {
|
|
||||||
userID: UUID!
|
|
||||||
}
|
|
||||||
|
|
||||||
input NewUserAccount {
|
input NewUserAccount {
|
||||||
username: String!
|
username: String!
|
||||||
email: String!
|
email: String!
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@ -54,10 +53,15 @@ type Setup struct {
|
|||||||
ConfirmToken string `json:"confirmToken"`
|
ConfirmToken string `json:"confirmToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ValidateAuthTokenResponse struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
UserID string `json:"userID"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoginResponseData is the response data for when a user logs in
|
// LoginResponseData is the response data for when a user logs in
|
||||||
type LoginResponseData struct {
|
type LoginResponseData struct {
|
||||||
AccessToken string `json:"accessToken"`
|
UserID string `json:"userID"`
|
||||||
Setup bool `json:"setup"`
|
Complete bool `json:"complete"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutResponseData is the response data for when a user logs out
|
// LogoutResponseData is the response data for when a user logs out
|
||||||
@ -65,8 +69,8 @@ type LogoutResponseData struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshTokenResponseData is the response data for when an access token is refreshed
|
// AuthTokenResponseData is the response data for when an access token is refreshed
|
||||||
type RefreshTokenResponseData struct {
|
type AuthTokenResponseData struct {
|
||||||
AccessToken string `json:"accessToken"`
|
AccessToken string `json:"accessToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,93 +80,9 @@ type AvatarUploadResponseData struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshTokenHandler handles when a user attempts to refresh token
|
|
||||||
func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
log.WithError(err).Error("unknown error")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refreshTokenID := uuid.MustParse(c.Value)
|
|
||||||
token, err := h.repo.GetRefreshTokenByID(r.Context(), refreshTokenID)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
|
|
||||||
log.WithError(err).WithFields(log.Fields{"refreshTokenID": refreshTokenID.String()}).Error("no tokens found")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.WithError(err).Error("token retrieve failure")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := h.repo.GetUserAccountByID(r.Context(), token.UserID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("user retrieve failure")
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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})
|
|
||||||
|
|
||||||
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.SecurityConfig.Secret, h.SecurityConfig.AccessTokenExpiration)
|
|
||||||
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",
|
|
||||||
Value: refreshTokenString.TokenID.String(),
|
|
||||||
Expires: refreshExpiresAt,
|
|
||||||
HttpOnly: true,
|
|
||||||
})
|
|
||||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, Setup: false})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogoutHandler removes all refresh tokens to log out user
|
// LogoutHandler removes all refresh tokens to log out user
|
||||||
func (h *TaskcafeHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *TaskcafeHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
c, err := r.Cookie("refreshToken")
|
c, err := r.Cookie("authToken")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == http.ErrNoCookie {
|
if err == http.ErrNoCookie {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
@ -172,7 +92,7 @@ func (h *TaskcafeHandler) LogoutHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshTokenID := uuid.MustParse(c.Value)
|
refreshTokenID := uuid.MustParse(c.Value)
|
||||||
err = h.repo.DeleteRefreshTokenByID(r.Context(), refreshTokenID)
|
err = h.repo.DeleteAuthTokenByID(r.Context(), refreshTokenID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -216,87 +136,23 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCreatedAt := time.Now().UTC()
|
authCreatedAt := time.Now().UTC()
|
||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
|
||||||
|
|
||||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.SecurityConfig.Secret, h.SecurityConfig.AccessTokenExpiration)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-type", "application/json")
|
w.Header().Set("Content-type", "application/json")
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "refreshToken",
|
Name: "authToken",
|
||||||
Value: refreshTokenString.TokenID.String(),
|
Value: authToken.TokenID.String(),
|
||||||
Expires: refreshExpiresAt,
|
Expires: authExpiresAt,
|
||||||
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
})
|
})
|
||||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
json.NewEncoder(w).Encode(LoginResponseData{Complete: true, UserID: authToken.UserID.String()})
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
if restricted != auth.InstallOnly {
|
|
||||||
log.Warning("attempted to install without install only restriction")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := h.repo.GetSystemOptionByKey(r.Context(), "is_installed")
|
|
||||||
if err != sql.ErrNoRows {
|
|
||||||
log.WithError(err).Error("install handler called even though system is installed")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestData InstallRequestData
|
|
||||||
err = json.NewDecoder(r.Body).Decode(&requestData)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAt := time.Now().UTC()
|
|
||||||
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(requestData.User.Password), 14)
|
|
||||||
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",
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err = h.repo.CreateSystemOption(r.Context(), db.CreateSystemOptionParams{Key: "is_installed", Value: sql.NullString{Valid: true, String: "true"}})
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshCreatedAt := time.Now().UTC()
|
|
||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
|
||||||
|
|
||||||
log.WithField("userID", user.UserID.String()).Info("creating install access token")
|
|
||||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.SecurityConfig.Secret, h.SecurityConfig.AccessTokenExpiration)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
log.Info(accessTokenString)
|
|
||||||
|
|
||||||
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) ConfirmUser(w http.ResponseWriter, r *http.Request) {
|
func (h *TaskcafeHandler) ConfirmUser(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -382,23 +238,43 @@ func (h *TaskcafeHandler) ConfirmUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCreatedAt := time.Now().UTC()
|
authCreatedAt := time.Now().UTC()
|
||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
|
||||||
|
|
||||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.SecurityConfig.Secret, h.SecurityConfig.AccessTokenExpiration)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-type", "application/json")
|
w.Header().Set("Content-type", "application/json")
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "refreshToken",
|
Name: "authToken",
|
||||||
Value: refreshTokenString.TokenID.String(),
|
Value: authToken.TokenID.String(),
|
||||||
Expires: refreshExpiresAt,
|
Path: "/",
|
||||||
|
Expires: authExpiresAt,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
})
|
})
|
||||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
json.NewEncoder(w).Encode(LoginResponseData{Complete: true, UserID: authToken.UserID.String()})
|
||||||
|
}
|
||||||
|
func (h *TaskcafeHandler) ValidateAuthTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := r.Cookie("authToken")
|
||||||
|
if err != nil {
|
||||||
|
if err == http.ErrNoCookie {
|
||||||
|
json.NewEncoder(w).Encode(ValidateAuthTokenResponse{Valid: false, UserID: ""})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.WithError(err).Error("unknown error")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authTokenID := uuid.MustParse(c.Value)
|
||||||
|
token, err := h.repo.GetAuthTokenByID(r.Context(), authTokenID)
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(ValidateAuthTokenResponse{Valid: false, UserID: ""})
|
||||||
|
} else {
|
||||||
|
json.NewEncoder(w).Encode(ValidateAuthTokenResponse{Valid: true, UserID: token.UserID.String()})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -425,6 +301,7 @@ func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("error checking for active user")
|
log.WithError(err).Error("error checking for active user")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if !hasActiveUser {
|
if !hasActiveUser {
|
||||||
json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: true})
|
json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: true})
|
||||||
@ -469,7 +346,7 @@ func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (rs authResource) Routes(taskcafeHandler TaskcafeHandler) chi.Router {
|
func (rs authResource) Routes(taskcafeHandler TaskcafeHandler) chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Post("/login", taskcafeHandler.LoginHandler)
|
r.Post("/login", taskcafeHandler.LoginHandler)
|
||||||
r.Post("/refresh_token", taskcafeHandler.RefreshTokenHandler)
|
|
||||||
r.Post("/logout", taskcafeHandler.LogoutHandler)
|
r.Post("/logout", taskcafeHandler.LogoutHandler)
|
||||||
|
r.Post("/validate", taskcafeHandler.ValidateAuthTokenHandler)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -3,33 +3,49 @@ package route
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/utils"
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthenticationMiddleware is a middleware that requires a valid JWT token to be passed via the Authorization header
|
// AuthenticationMiddleware is a middleware that requires a valid JWT token to be passed via the Authorization header
|
||||||
type AuthenticationMiddleware struct {
|
type AuthenticationMiddleware struct {
|
||||||
jwtKey []byte
|
repo db.Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middleware returns the middleware handler
|
// Middleware returns the middleware handler
|
||||||
func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
requestID := uuid.New()
|
requestID := uuid.New()
|
||||||
bearerTokenRaw := r.Header.Get("Authorization")
|
foundToken := true
|
||||||
splitToken := strings.Split(bearerTokenRaw, "Bearer")
|
tokenRaw := ""
|
||||||
if len(splitToken) != 2 {
|
c, err := r.Cookie("authToken")
|
||||||
|
if err != nil {
|
||||||
|
if err == http.ErrNoCookie {
|
||||||
|
foundToken = false
|
||||||
|
} else {
|
||||||
|
log.WithError(err).Error("unknown error")
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accessTokenString := strings.TrimSpace(splitToken[1])
|
}
|
||||||
accessClaims, err := auth.ValidateAccessToken(accessTokenString, m.jwtKey)
|
if !foundToken {
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
log.WithError(err).Error("no auth token found in cookie or authorization header")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokenRaw = token
|
||||||
|
} else {
|
||||||
|
tokenRaw = c.Value
|
||||||
|
}
|
||||||
|
authTokenID := uuid.MustParse(tokenRaw)
|
||||||
|
token, err := m.repo.GetAuthTokenByID(r.Context(), authTokenID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*auth.ErrExpiredToken); ok {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
w.Write([]byte(`{
|
w.Write([]byte(`{
|
||||||
"data": {},
|
"data": {},
|
||||||
@ -43,25 +59,10 @@ func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
|||||||
}`))
|
}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Error(err)
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var userID uuid.UUID
|
ctx := context.WithValue(r.Context(), utils.UserIDKey, token.UserID)
|
||||||
if accessClaims.Restricted == auth.InstallOnly {
|
// ctx = context.WithValue(ctx, utils.RestrictedModeKey, accessClaims.Restricted)
|
||||||
userID = uuid.New()
|
// ctx = context.WithValue(ctx, utils.OrgRoleKey, accessClaims.OrgRole)
|
||||||
} else {
|
|
||||||
userID, err = uuid.Parse(accessClaims.UserID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("middleware access token userID parse")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
ctx = context.WithValue(ctx, utils.ReqIDKey, requestID)
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@ -80,6 +81,17 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
|
|||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(middleware.Timeout(60 * time.Second))
|
r.Use(middleware.Timeout(60 * time.Second))
|
||||||
|
|
||||||
|
r.Use(cors.Handler(cors.Options{
|
||||||
|
// AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts
|
||||||
|
AllowedOrigins: []string{"https://*", "http://*"},
|
||||||
|
// AllowOriginFunc: func(r *http.Request, origin string) bool { return true },
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Accept", "Authorization", "Cookie", "Content-Type", "X-CSRF-Token"},
|
||||||
|
ExposedHeaders: []string{"Link"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||||
|
}))
|
||||||
|
|
||||||
repository := db.NewRepository(dbConnection)
|
repository := db.NewRepository(dbConnection)
|
||||||
taskcafeHandler := TaskcafeHandler{*repository, securityConfig}
|
taskcafeHandler := TaskcafeHandler{*repository, securityConfig}
|
||||||
|
|
||||||
@ -91,7 +103,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
|
|||||||
mux.Post("/auth/confirm", taskcafeHandler.ConfirmUser)
|
mux.Post("/auth/confirm", taskcafeHandler.ConfirmUser)
|
||||||
mux.Post("/auth/register", taskcafeHandler.RegisterUser)
|
mux.Post("/auth/register", taskcafeHandler.RegisterUser)
|
||||||
})
|
})
|
||||||
auth := AuthenticationMiddleware{securityConfig.Secret}
|
auth := AuthenticationMiddleware{*repository}
|
||||||
r.Group(func(mux chi.Router) {
|
r.Group(func(mux chi.Router) {
|
||||||
mux.Use(auth.Middleware)
|
mux.Use(auth.Middleware)
|
||||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE refresh_token RENAME TO auth_token;
|
Loading…
Reference in New Issue
Block a user