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:
@ -215,9 +215,12 @@ const AdminRoute = () => {
|
||||
},
|
||||
});
|
||||
if (data && user) {
|
||||
/*
|
||||
TODO: add permision check
|
||||
if (user.roles.org !== 'admin') {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
|
||||
@ -225,7 +228,8 @@ const AdminRoute = () => {
|
||||
initialTab={0}
|
||||
users={data.users}
|
||||
invitedUsers={data.invitedUsers}
|
||||
canInviteUser={user.roles.org === 'admin'}
|
||||
// canInviteUser={user.roles.org === 'admin'} TODO: add permision check
|
||||
canInviteUser={true}
|
||||
onInviteUser={NOOP}
|
||||
onUpdateUserPassword={() => {
|
||||
hidePopup();
|
||||
|
@ -13,8 +13,6 @@ import Login from 'Auth';
|
||||
import Register from 'Register';
|
||||
import Profile from 'Profile';
|
||||
import styled from 'styled-components';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
|
||||
const MainContent = styled.div`
|
||||
@ -26,9 +24,9 @@ const MainContent = styled.div`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
type RefreshTokenResponse = {
|
||||
accessToken: string;
|
||||
setup?: null | { confirmToken: string };
|
||||
type ValidateTokenResponse = {
|
||||
valid: boolean;
|
||||
userID: string;
|
||||
};
|
||||
|
||||
const AuthorizedRoutes = () => {
|
||||
@ -36,27 +34,17 @@ const AuthorizedRoutes = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { setUser } = useCurrentUser();
|
||||
useEffect(() => {
|
||||
fetch('/auth/refresh_token', {
|
||||
fetch('/auth/validate', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 400) {
|
||||
history.replace('/login');
|
||||
const response: ValidateTokenResponse = await x.json();
|
||||
const { valid, userID } = response;
|
||||
if (!valid) {
|
||||
history.replace(`/login`);
|
||||
} else {
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken, setup } = response;
|
||||
if (setup) {
|
||||
history.replace(`/register?confirmToken=${setup.confirmToken}`);
|
||||
} else {
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(accessToken);
|
||||
}
|
||||
setUser(userID);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
|
@ -3,13 +3,8 @@ import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
|
||||
import { ProfileMenu } from 'shared/components/DropdownMenu';
|
||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||
import { useHistory } from 'react-router';
|
||||
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||
import {
|
||||
RoleCode,
|
||||
useTopNavbarQuery,
|
||||
useDeleteProjectMutation,
|
||||
GetProjectsDocument,
|
||||
} from 'shared/generated/graphql';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
import { RoleCode, useTopNavbarQuery, useDeleteProjectMutation, GetProjectsDocument } from 'shared/generated/graphql';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import produce from 'immer';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
@ -107,23 +102,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
onRemoveInvitedFromBoard,
|
||||
onRemoveFromBoard,
|
||||
}) => {
|
||||
const { user, setUserRoles, setUser } = useCurrentUser();
|
||||
const { user, setUser } = useCurrentUser();
|
||||
const { loading, data } = useTopNavbarQuery({
|
||||
onCompleted: response => {
|
||||
if (user && user.roles) {
|
||||
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>()),
|
||||
});
|
||||
}
|
||||
},
|
||||
// TODO: maybe remove?
|
||||
onCompleted: response => {},
|
||||
});
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const history = useHistory();
|
||||
@ -147,7 +129,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
<Popup title={null} tab={0}>
|
||||
<ProfileMenu
|
||||
onLogout={onLogout}
|
||||
showAdminConsole={user ? user.roles.org === 'admin' : false}
|
||||
showAdminConsole={true} // TODO: add permision check
|
||||
onAdminConsole={() => {
|
||||
history.push('/admin');
|
||||
hidePopup();
|
||||
@ -189,7 +171,9 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
if (!user) {
|
||||
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 member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
|
||||
if (member) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
|
||||
const cache = new InMemoryCache();
|
||||
|
||||
|
@ -1,79 +1,20 @@
|
||||
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 = {
|
||||
user: CurrentUserRaw | null;
|
||||
setUser: (user: CurrentUserRaw | null) => void;
|
||||
setUserRoles: (roles: CurrentUserRoles) => void;
|
||||
user: string | null;
|
||||
setUser: (user: string | null) => void;
|
||||
};
|
||||
|
||||
export const UserContext = React.createContext<UserContextState>({
|
||||
user: null,
|
||||
setUser: _user => null,
|
||||
setUserRoles: roles => null,
|
||||
});
|
||||
|
||||
export interface CurrentUser extends CurrentUserRaw {
|
||||
isAdmin: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||
isVisible: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||
}
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
const { user, setUser, setUserRoles } = useContext(UserContext);
|
||||
let currentUser: CurrentUser | null = null;
|
||||
if (user) {
|
||||
currentUser = {
|
||||
...user,
|
||||
isAdmin(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||
if (user.roles.org === 'admin') {
|
||||
return true;
|
||||
}
|
||||
switch (level) {
|
||||
case PermissionLevel.TEAM:
|
||||
return subjectID ? this.roles.teams.get(subjectID) === 'admin' : false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isVisible(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||
if (user.roles.org === 'admin') {
|
||||
return true;
|
||||
}
|
||||
switch (level) {
|
||||
case PermissionLevel.TEAM:
|
||||
return subjectID ? this.roles.teams.get(subjectID) !== null : false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
return {
|
||||
user: currentUser,
|
||||
user,
|
||||
setUser,
|
||||
setUserRoles,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,16 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { Router } from 'react-router';
|
||||
import { PopupProvider } from 'shared/components/PopupMenu';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import NormalizeStyles from './NormalizeStyles';
|
||||
import BaseStyles from './BaseStyles';
|
||||
import theme from './ThemeStyles';
|
||||
import Routes from './Routes';
|
||||
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context';
|
||||
import { UserContext } from './context';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
@ -48,19 +46,11 @@ const StyledContainer = styled(ToastContainer).attrs({
|
||||
const history = createBrowserHistory();
|
||||
|
||||
const App = () => {
|
||||
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
||||
const setUserRoles = (roles: CurrentUserRoles) => {
|
||||
if (user) {
|
||||
setUser({
|
||||
...user,
|
||||
roles,
|
||||
});
|
||||
}
|
||||
};
|
||||
const [user, setUser] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
|
||||
<UserContext.Provider value={{ user, setUser }}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import Login from 'shared/components/Login';
|
||||
import UserContext from 'App/context';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
@ -30,42 +28,23 @@ const Auth = () => {
|
||||
setComplete(true);
|
||||
} else {
|
||||
const response = await x.json();
|
||||
const { accessToken } = response;
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||
};
|
||||
setUser(currentUser);
|
||||
setComplete(true);
|
||||
setAccessToken(accessToken);
|
||||
|
||||
const { userID } = response;
|
||||
setUser(userID);
|
||||
history.push('/');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/auth/refresh_token', {
|
||||
fetch('/auth/validate', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 200) {
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken, setup } = response;
|
||||
if (setup) {
|
||||
history.replace(`/register?confirmToken=${setup.confirmToken}`);
|
||||
} else {
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(accessToken);
|
||||
history.replace('/projects');
|
||||
}
|
||||
const response = await x.json();
|
||||
const { valid, userID } = response;
|
||||
if (valid) {
|
||||
setUser(userID);
|
||||
history.replace('/projects');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
@ -5,8 +5,6 @@ import { useHistory, useLocation } from 'react-router';
|
||||
import * as QueryString from 'query-string';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
|
||||
const UsersConfirm = () => {
|
||||
@ -31,18 +29,8 @@ const UsersConfirm = () => {
|
||||
const { status } = x;
|
||||
if (status === 200) {
|
||||
const response = await x.json();
|
||||
const { accessToken } = response;
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: {
|
||||
org: claims.orgRole,
|
||||
teams: new Map<string, string>(),
|
||||
projects: new Map<string, string>(),
|
||||
},
|
||||
};
|
||||
setUser(currentUser);
|
||||
setAccessToken(accessToken);
|
||||
const { userID } = response;
|
||||
setUser(userID);
|
||||
history.push('/');
|
||||
} else {
|
||||
setFailed();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import GlobalTopNavbar from 'App/TopNavbar';
|
||||
import { getAccessToken } from 'shared/utils/accessToken';
|
||||
import Settings from 'shared/components/Settings';
|
||||
import {
|
||||
useMeQuery,
|
||||
@ -49,12 +48,9 @@ const Projects = () => {
|
||||
if (e.target.files) {
|
||||
const fileData = new FormData();
|
||||
fileData.append('file', e.target.files[0]);
|
||||
const accessToken = getAccessToken();
|
||||
axios
|
||||
.post('/users/me/avatar', fileData, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(res => {
|
||||
if ($fileUpload && $fileUpload.current) {
|
||||
@ -75,7 +71,7 @@ const Projects = () => {
|
||||
}
|
||||
}}
|
||||
onResetPassword={(password, done) => {
|
||||
updateUserPassword({ variables: { userID: user.id, password } });
|
||||
updateUserPassword({ variables: { userID: user, password } });
|
||||
toast('Password was changed!');
|
||||
done();
|
||||
}}
|
||||
|
@ -543,7 +543,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
||||
onChangeTaskMetaFilter={filter => {
|
||||
setTaskMetaFilters(filter);
|
||||
}}
|
||||
userID={user?.id}
|
||||
userID={user ?? ''}
|
||||
labels={labelsRef}
|
||||
members={membersRef}
|
||||
/>,
|
||||
|
@ -541,7 +541,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
bio="None"
|
||||
onRemoveFromTask={() => {
|
||||
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 NewProject from 'shared/components/NewProject';
|
||||
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
import Button from 'shared/components/Button';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -268,7 +268,7 @@ const Projects = () => {
|
||||
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
|
||||
<Wrapper>
|
||||
<ProjectsContainer>
|
||||
{user.roles.org === 'admin' && (
|
||||
{true && ( // TODO: add permision check
|
||||
<AddTeamButton
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
@ -330,7 +330,7 @@ const Projects = () => {
|
||||
<div key={team.id}>
|
||||
<ProjectSectionTitleWrapper>
|
||||
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||
{true && ( // TODO: add permision check
|
||||
<SectionActions>
|
||||
<SectionActionLink to={`/teams/${team.id}`}>
|
||||
<SectionAction variant="outline">Projects</SectionAction>
|
||||
@ -355,7 +355,7 @@ const Projects = () => {
|
||||
</ProjectTile>
|
||||
</ProjectListItem>
|
||||
))}
|
||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||
{true && ( // TODO: add permision check
|
||||
<ProjectListItem>
|
||||
<ProjectAddTile
|
||||
onClick={() => {
|
||||
|
@ -3,7 +3,7 @@ import Input from 'shared/components/Input';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import produce from 'immer';
|
||||
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 {
|
||||
useGetTeamQuery,
|
||||
@ -424,7 +424,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 3000,
|
||||
});
|
||||
const { user, setUserRoles } = useCurrentUser();
|
||||
const { user } = useCurrentUser();
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
const [createTeamMember] = useCreateTeamMemberMutation({
|
||||
@ -446,17 +446,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
);
|
||||
},
|
||||
});
|
||||
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation({
|
||||
onCompleted: r => {
|
||||
if (user) {
|
||||
setUserRoles(
|
||||
produce(user.roles, draftRoles => {
|
||||
draftRoles.teams.set(r.updateTeamMemberRole.teamID, r.updateTeamMemberRole.member.role.code);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation();
|
||||
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<GetTeamQuery>(
|
||||
@ -491,7 +481,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && (
|
||||
{true && ( // TODO: add permission check
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
@ -528,11 +518,12 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
currentUserID={user.id ?? ''}
|
||||
currentUserID={user ?? ''}
|
||||
subject={member}
|
||||
members={data.findTeam.members}
|
||||
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 => {
|
||||
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
||||
}}
|
||||
|
@ -13,7 +13,7 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { History } from 'history';
|
||||
import produce from 'immer';
|
||||
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||
import { PermissionObjectType, PermissionLevel, useCurrentUser } from 'App/context';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import Members from './Members';
|
||||
import Projects from './Projects';
|
||||
@ -95,9 +95,12 @@ const Teams = () => {
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
const match = useRouteMatch();
|
||||
if (data && user) {
|
||||
/*
|
||||
TODO: re-add permission check
|
||||
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar
|
||||
|
@ -1,20 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import axios from 'axios';
|
||||
import createAuthRefreshInterceptor from 'axios-auth-refresh';
|
||||
import { ApolloProvider } from '@apollo/react-hooks';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { onError } from 'apollo-link-error';
|
||||
import { ApolloClient } from '@apollo/client';
|
||||
import { ApolloProvider } from '@apollo/client/react';
|
||||
|
||||
import { enableMapSet } from 'immer';
|
||||
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
||||
import dayjs from 'dayjs';
|
||||
import updateLocale from 'dayjs/plugin/updateLocale';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
||||
import cache from './App/cache';
|
||||
import App from './App';
|
||||
|
||||
@ -34,131 +29,8 @@ dayjs.updateLocale('en', {
|
||||
},
|
||||
});
|
||||
|
||||
let forward$;
|
||||
let isRefreshing = false;
|
||||
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,
|
||||
});
|
||||
const client = new ApolloClient({ uri: '/graphql', cache });
|
||||
console.log('cloient', client);
|
||||
|
||||
ReactDOM.render(
|
||||
<ApolloProvider client={client}>
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 Button from 'shared/components/Button';
|
||||
|
||||
|
@ -49,6 +49,7 @@ export const NameEditor: React.FC<NameEditorProps> = ({ onSave: handleSave, onCa
|
||||
<ListNameEditorWrapper>
|
||||
<ListNameEditor
|
||||
ref={$editorRef}
|
||||
height={40}
|
||||
onKeyDown={onKeyDown}
|
||||
value={listName}
|
||||
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};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
|
||||
${props =>
|
||||
props.color === 'success' &&
|
||||
@ -18,6 +19,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
|
||||
stroke: ${props.theme.colors.success};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const ClockIcon = styled(Clock)<{ color: string }>`
|
||||
fill: ${props => props.color};
|
||||
`;
|
||||
@ -26,7 +28,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
resize: none;
|
||||
height: 90px;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
|
||||
background: none;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import visit from 'unist-util-visit';
|
||||
import emoji from 'node-emoji';
|
||||
import emoticon from 'emoticon';
|
||||
import { emoticon } from 'emoticon';
|
||||
import { Emoji } from 'emoji-mart';
|
||||
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;
|
||||
};
|
||||
|
||||
type RefreshTokenResponse = {
|
||||
accessToken: string;
|
||||
setup?: null | { confirmToken: string };
|
||||
};
|
||||
|
||||
type LoginFormData = {
|
||||
username: string;
|
||||
password: string;
|
||||
|
Reference in New Issue
Block a user