Compare commits
	
		
			1 Commits
		
	
	
		
			0.2.2
			...
			feat/updat
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					4a8d4a6ec3 | 
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -18,8 +18,6 @@ If applicable, add screenshots to help explain your problem.
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context about the problem here.
 | 
			
		||||
 | 
			
		||||
Please send the Taskcafe web service logs if applicable.
 | 
			
		||||
 | 
			
		||||
<!--
 | 
			
		||||
 | 
			
		||||
Please read the contributing guide before working on any new pull requests!
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
[server]
 | 
			
		||||
hostname = '0.0.0.0:3333'
 | 
			
		||||
[general]
 | 
			
		||||
host = '0.0.0.0:3333'
 | 
			
		||||
 | 
			
		||||
[email_notifications]
 | 
			
		||||
enabled = true
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,14 @@
 | 
			
		||||
    "@apollo/client": "^3.0.0-rc.8",
 | 
			
		||||
    "@apollo/react-common": "^3.1.4",
 | 
			
		||||
    "@apollo/react-hooks": "^3.1.3",
 | 
			
		||||
    "@fortawesome/fontawesome-svg-core": "^1.2.27",
 | 
			
		||||
    "@fortawesome/free-brands-svg-icons": "^5.12.1",
 | 
			
		||||
    "@fortawesome/free-regular-svg-icons": "^5.12.1",
 | 
			
		||||
    "@fortawesome/free-solid-svg-icons": "^5.12.1",
 | 
			
		||||
    "@fortawesome/react-fontawesome": "^0.1.8",
 | 
			
		||||
    "@testing-library/jest-dom": "^4.2.4",
 | 
			
		||||
    "@testing-library/react": "^9.3.2",
 | 
			
		||||
    "@testing-library/user-event": "^7.1.2",
 | 
			
		||||
    "@types/axios": "^0.14.0",
 | 
			
		||||
    "@types/color": "^3.0.1",
 | 
			
		||||
    "@types/date-fns": "^2.6.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ const Install = () => {
 | 
			
		||||
                  } else {
 | 
			
		||||
                    const response: RefreshTokenResponse = await x.data;
 | 
			
		||||
                    const { accessToken: newToken, isInstalled } = response;
 | 
			
		||||
                    const claims: JWTToken = jwtDecode(newToken);
 | 
			
		||||
                    const claims: JWTToken = jwtDecode(accessToken);
 | 
			
		||||
                    const currentUser = {
 | 
			
		||||
                      id: claims.userId,
 | 
			
		||||
                      roles: {
 | 
			
		||||
@@ -69,7 +69,7 @@ const Install = () => {
 | 
			
		||||
                      },
 | 
			
		||||
                    };
 | 
			
		||||
                    setUser(currentUser);
 | 
			
		||||
                    setAccessToken(newToken);
 | 
			
		||||
                    setAccessToken(accessToken);
 | 
			
		||||
                    if (!isInstalled) {
 | 
			
		||||
                      history.replace('/install');
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -323,6 +323,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
  const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
 | 
			
		||||
  const { loading, data } = useFindProjectQuery({
 | 
			
		||||
    variables: { projectID },
 | 
			
		||||
    pollInterval: 5000,
 | 
			
		||||
  });
 | 
			
		||||
  const [deleteTaskGroupTasks] = useDeleteTaskGroupTasksMutation({
 | 
			
		||||
    update: (client, resp) =>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import Modal from 'shared/components/Modal';
 | 
			
		||||
import TaskDetails from 'shared/components/TaskDetails';
 | 
			
		||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
 | 
			
		||||
import MemberManager from 'shared/components/MemberManager';
 | 
			
		||||
import { useRouteMatch, useHistory } from 'react-router';
 | 
			
		||||
import { useRouteMatch, useHistory, Redirect } from 'react-router';
 | 
			
		||||
import {
 | 
			
		||||
  useDeleteTaskChecklistMutation,
 | 
			
		||||
  useUpdateTaskChecklistNameMutation,
 | 
			
		||||
@@ -32,6 +32,7 @@ import Input from 'shared/components/Input';
 | 
			
		||||
import { useForm } from 'react-hook-form';
 | 
			
		||||
import updateApolloCache from 'shared/utils/cache';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import hasNotFoundError from 'shared/utils/error';
 | 
			
		||||
 | 
			
		||||
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
 | 
			
		||||
  const total = checklists.reduce((prev: any, next: any) => {
 | 
			
		||||
@@ -269,8 +270,8 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
 | 
			
		||||
  const [setTaskComplete] = useSetTaskCompleteMutation();
 | 
			
		||||
  const { loading, data, refetch, error } = useFindTaskQuery({ variables: { taskID }, pollInterval: 5000 });
 | 
			
		||||
  const [setTaskComplete, { error: setTaskCompleteError }] = useSetTaskCompleteMutation();
 | 
			
		||||
  const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
 | 
			
		||||
    onCompleted: () => {
 | 
			
		||||
      refetch();
 | 
			
		||||
@@ -289,9 +290,13 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
      refreshCache();
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return null;
 | 
			
		||||
  if (hasNotFoundError(error, setTaskCompleteError)) {
 | 
			
		||||
    return <Redirect to={projectURL} />;
 | 
			
		||||
  }
 | 
			
		||||
  if (setTaskCompleteError && setTaskCompleteError)
 | 
			
		||||
    if (loading) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  if (!data) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
@@ -346,7 +351,11 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
              onTaskNameChange={onTaskNameChange}
 | 
			
		||||
              onTaskDescriptionChange={onTaskDescriptionChange}
 | 
			
		||||
              onToggleTaskComplete={task => {
 | 
			
		||||
                setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
 | 
			
		||||
                setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }).catch(r => {
 | 
			
		||||
                  if (hasNotFoundError(r)) {
 | 
			
		||||
                    history.push(projectURL);
 | 
			
		||||
                  }
 | 
			
		||||
                });
 | 
			
		||||
              }}
 | 
			
		||||
              onDeleteTask={onDeleteTask}
 | 
			
		||||
              onChangeItemName={(itemID, itemName) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@ import {
 | 
			
		||||
  useCreateProjectMutation,
 | 
			
		||||
  GetProjectsDocument,
 | 
			
		||||
  GetProjectsQuery,
 | 
			
		||||
  MeQuery,
 | 
			
		||||
  MeDocument,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
@@ -236,7 +234,7 @@ type ShowNewProject = {
 | 
			
		||||
 | 
			
		||||
const Projects = () => {
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only', pollInterval: 5000 });
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.title = 'Taskcafé';
 | 
			
		||||
  }, []);
 | 
			
		||||
 
 | 
			
		||||
@@ -154,7 +154,7 @@ type TeamProjectsProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({ variables: { teamID } });
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({ variables: { teamID }, pollInterval: 5000 });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <span>loading</span>;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import styled, { css, keyframes } from 'styled-components';
 | 
			
		||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 | 
			
		||||
import { mixin } from 'shared/utils/styles';
 | 
			
		||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
			
		||||
import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
 | 
			
		||||
import { CheckCircle, CheckSquareOutline } from 'shared/icons';
 | 
			
		||||
import { RefObject } from 'react';
 | 
			
		||||
import TaskAssignee from 'shared/components/TaskAssignee';
 | 
			
		||||
 | 
			
		||||
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
 | 
			
		||||
@@ -18,9 +20,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
 | 
			
		||||
      stroke: rgba(${props.theme.colors.success});
 | 
			
		||||
    `}
 | 
			
		||||
`;
 | 
			
		||||
export const ClockIcon = styled(Clock)<{ color: string }>`
 | 
			
		||||
  fill: ${props => props.color};
 | 
			
		||||
`;
 | 
			
		||||
export const ClockIcon = styled(FontAwesomeIcon)``;
 | 
			
		||||
 | 
			
		||||
export const EditorTextarea = styled(TextareaAutosize)`
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import React, { useState, useRef, useEffect } from 'react';
 | 
			
		||||
import { Pencil, Eye, List } from 'shared/icons';
 | 
			
		||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 | 
			
		||||
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faClock, faEye } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import {
 | 
			
		||||
  EditorTextarea,
 | 
			
		||||
  CardMember,
 | 
			
		||||
@@ -153,7 +155,7 @@ const Card = React.forwardRef(
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Pencil width={8} height={8} />
 | 
			
		||||
              <FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
 | 
			
		||||
            </ListCardOperation>
 | 
			
		||||
          )}
 | 
			
		||||
          <ListCardDetails complete={complete ?? false}>
 | 
			
		||||
@@ -216,18 +218,18 @@ const Card = React.forwardRef(
 | 
			
		||||
            <ListCardBadges>
 | 
			
		||||
              {watched && (
 | 
			
		||||
                <ListCardBadge>
 | 
			
		||||
                  <Eye width={8} height={8} />
 | 
			
		||||
                  <FontAwesomeIcon color="#6b778c" icon={faEye} size="xs" />
 | 
			
		||||
                </ListCardBadge>
 | 
			
		||||
              )}
 | 
			
		||||
              {dueDate && (
 | 
			
		||||
                <DueDateCardBadge isPastDue={dueDate.isPastDue}>
 | 
			
		||||
                  <ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} width={8} height={8} />
 | 
			
		||||
                  <ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} icon={faClock} size="xs" />
 | 
			
		||||
                  <ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
 | 
			
		||||
                </DueDateCardBadge>
 | 
			
		||||
              )}
 | 
			
		||||
              {description && (
 | 
			
		||||
                <DescriptionBadge>
 | 
			
		||||
                  <List width={8} height={8} />
 | 
			
		||||
                  <FontAwesomeIcon color="#6b778c" icon={faList} size="xs" />
 | 
			
		||||
                </DescriptionBadge>
 | 
			
		||||
              )}
 | 
			
		||||
              {checklists && (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import Button from 'shared/components/Button';
 | 
			
		||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
			
		||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 | 
			
		||||
import { mixin } from 'shared/utils/styles';
 | 
			
		||||
 | 
			
		||||
export const CancelIconWrapper = styled.div`
 | 
			
		||||
export const CancelIcon = styled(FontAwesomeIcon)`
 | 
			
		||||
  opacity: 0.8;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  font-size: 1.25em;
 | 
			
		||||
  padding-left: 5px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
 | 
			
		||||
  padding-bottom: 8px;
 | 
			
		||||
  display: ${props => (props.isOpen ? 'flex' : 'none')};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
import React, { useState, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
 | 
			
		||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
			
		||||
import { Cross } from 'shared/icons';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CardComposerWrapper,
 | 
			
		||||
  CancelIconWrapper,
 | 
			
		||||
  CancelIcon,
 | 
			
		||||
  AddCardButton,
 | 
			
		||||
  ComposerControls,
 | 
			
		||||
  ComposerControlsSaveSection,
 | 
			
		||||
@@ -52,9 +52,7 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
 | 
			
		||||
          >
 | 
			
		||||
            Add Card
 | 
			
		||||
          </AddCardButton>
 | 
			
		||||
          <CancelIconWrapper onClick={() => onClose()}>
 | 
			
		||||
            <Cross width={12} height={12} />
 | 
			
		||||
          </CancelIconWrapper>
 | 
			
		||||
          <CancelIcon onClick={onClose} icon={faTimes} color="#42526e" />
 | 
			
		||||
        </ComposerControlsSaveSection>
 | 
			
		||||
        <ComposerControlsActionsSection />
 | 
			
		||||
      </ComposerControls>
 | 
			
		||||
 
 | 
			
		||||
@@ -585,30 +585,3 @@ export const ActivityItemLog = styled.span`
 | 
			
		||||
  margin-left: 2px;
 | 
			
		||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ViewRawButton = styled.button`
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  padding: 8px 12px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: 4px;
 | 
			
		||||
  bottom: -24px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.25);
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: rgba(${props => props.theme.colors.text.primary});
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const TaskDetailsEditor = styled(TextareaAutosize)`
 | 
			
		||||
  min-height: 108px;
 | 
			
		||||
  color: #c2c6dc;
 | 
			
		||||
  background: #262c49;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  line-height: 20px;
 | 
			
		||||
  margin-left: 32px;
 | 
			
		||||
  margin-right: 32px;
 | 
			
		||||
  padding: 9px 8px 7px 8px;
 | 
			
		||||
  outline: none;
 | 
			
		||||
  border: none;
 | 
			
		||||
`;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,6 @@ import {
 | 
			
		||||
  AssignUserLabel,
 | 
			
		||||
  AssignUsersButton,
 | 
			
		||||
  AssignedUsersSection,
 | 
			
		||||
  ViewRawButton,
 | 
			
		||||
  DueDateTitle,
 | 
			
		||||
  Container,
 | 
			
		||||
  LeftSidebar,
 | 
			
		||||
@@ -66,7 +65,6 @@ import {
 | 
			
		||||
  CommentProfile,
 | 
			
		||||
  CommentInnerWrapper,
 | 
			
		||||
  ActivitySection,
 | 
			
		||||
  TaskDetailsEditor,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
 | 
			
		||||
import onDragEnd from './onDragEnd';
 | 
			
		||||
@@ -155,7 +153,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
    return true;
 | 
			
		||||
  });
 | 
			
		||||
  const [saveTimeout, setSaveTimeout] = useState<any>(null);
 | 
			
		||||
  const [showRaw, setShowRaw] = useState(false);
 | 
			
		||||
  const [showCommentActions, setShowCommentActions] = useState(false);
 | 
			
		||||
  const taskDescriptionRef = useRef(task.description ?? '');
 | 
			
		||||
  const $noMemberBtn = useRef<HTMLDivElement>(null);
 | 
			
		||||
@@ -312,34 +309,28 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
        </HeaderContainer>
 | 
			
		||||
        <InnerContentContainer>
 | 
			
		||||
          <DescriptionContainer>
 | 
			
		||||
            {showRaw ? (
 | 
			
		||||
              <TaskDetailsEditor value={taskDescriptionRef.current} />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <EditorContainer
 | 
			
		||||
                onClick={e => {
 | 
			
		||||
                  if (!editTaskDescription) {
 | 
			
		||||
                    setEditTaskDescription(true);
 | 
			
		||||
                  }
 | 
			
		||||
            <EditorContainer
 | 
			
		||||
              onClick={e => {
 | 
			
		||||
                if (!editTaskDescription) {
 | 
			
		||||
                  setEditTaskDescription(true);
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Editor
 | 
			
		||||
                defaultValue={task.description ?? ''}
 | 
			
		||||
                theme={dark}
 | 
			
		||||
                readOnly={!editTaskDescription}
 | 
			
		||||
                autoFocus
 | 
			
		||||
                onChange={value => {
 | 
			
		||||
                  setSaveTimeout(() => {
 | 
			
		||||
                    clearTimeout(saveTimeout);
 | 
			
		||||
                    return setTimeout(saveDescription, 2000);
 | 
			
		||||
                  });
 | 
			
		||||
                  const text = value();
 | 
			
		||||
                  taskDescriptionRef.current = text;
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Editor
 | 
			
		||||
                  defaultValue={task.description ?? ''}
 | 
			
		||||
                  theme={dark}
 | 
			
		||||
                  readOnly={!editTaskDescription}
 | 
			
		||||
                  autoFocus
 | 
			
		||||
                  onChange={value => {
 | 
			
		||||
                    setSaveTimeout(() => {
 | 
			
		||||
                      clearTimeout(saveTimeout);
 | 
			
		||||
                      return setTimeout(saveDescription, 2000);
 | 
			
		||||
                    });
 | 
			
		||||
                    const text = value();
 | 
			
		||||
                    taskDescriptionRef.current = text;
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              </EditorContainer>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <ViewRawButton onClick={() => setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'}</ViewRawButton>
 | 
			
		||||
              />
 | 
			
		||||
            </EditorContainer>
 | 
			
		||||
          </DescriptionContainer>
 | 
			
		||||
          <ChecklistSection>
 | 
			
		||||
            <DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>
 | 
			
		||||
 
 | 
			
		||||
@@ -209,7 +209,8 @@ export enum ObjectType {
 | 
			
		||||
  Org = 'ORG',
 | 
			
		||||
  Team = 'TEAM',
 | 
			
		||||
  Project = 'PROJECT',
 | 
			
		||||
  Task = 'TASK'
 | 
			
		||||
  Task = 'TASK',
 | 
			
		||||
  TaskGroup = 'TASK_GROUP'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Query = {
 | 
			
		||||
@@ -722,7 +723,7 @@ export type UpdateProjectMemberRolePayload = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type NewTask = {
 | 
			
		||||
  taskGroupID: Scalars['String'];
 | 
			
		||||
  taskGroupID: Scalars['UUID'];
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  position: Scalars['Float'];
 | 
			
		||||
};
 | 
			
		||||
@@ -1472,7 +1473,7 @@ export type UpdateProjectMemberRoleMutation = (
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export type CreateTaskMutationVariables = {
 | 
			
		||||
  taskGroupID: Scalars['String'];
 | 
			
		||||
  taskGroupID: Scalars['UUID'];
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  position: Scalars['Float'];
 | 
			
		||||
};
 | 
			
		||||
@@ -3044,7 +3045,7 @@ export type UpdateProjectMemberRoleMutationHookResult = ReturnType<typeof useUpd
 | 
			
		||||
export type UpdateProjectMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateProjectMemberRoleMutation>;
 | 
			
		||||
export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateProjectMemberRoleMutation, UpdateProjectMemberRoleMutationVariables>;
 | 
			
		||||
export const CreateTaskDocument = gql`
 | 
			
		||||
    mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
 | 
			
		||||
    mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
 | 
			
		||||
  createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
 | 
			
		||||
    ...TaskFields
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import gql from 'graphql-tag';
 | 
			
		||||
import TASK_FRAGMENT from '../fragments/task';
 | 
			
		||||
 | 
			
		||||
const CREATE_TASK_MUTATION = gql`
 | 
			
		||||
  mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
 | 
			
		||||
  mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
 | 
			
		||||
    createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
 | 
			
		||||
      ...TaskFields
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
const Eye: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 576 512">
 | 
			
		||||
      <path d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z" />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Eye;
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
const List: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 512 512">
 | 
			
		||||
      <path d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z" />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default List;
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
import Cross from './Cross';
 | 
			
		||||
import Cog from './Cog';
 | 
			
		||||
import Eye from './Eye';
 | 
			
		||||
import List from './List';
 | 
			
		||||
import At from './At';
 | 
			
		||||
import Task from './Task';
 | 
			
		||||
import Smile from './Smile';
 | 
			
		||||
@@ -87,6 +85,4 @@ export {
 | 
			
		||||
  Clone,
 | 
			
		||||
  Paperclip,
 | 
			
		||||
  Share,
 | 
			
		||||
  Eye,
 | 
			
		||||
  List,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
let accessToken = '';
 | 
			
		||||
 | 
			
		||||
export function setAccessToken(newToken: string) {
 | 
			
		||||
  console.log(newToken);
 | 
			
		||||
  accessToken = newToken;
 | 
			
		||||
}
 | 
			
		||||
export function getAccessToken() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								frontend/src/shared/utils/error.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/shared/utils/error.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import { ApolloError } from '@apollo/client';
 | 
			
		||||
 | 
			
		||||
export default function hasNotFoundError(...errors: Array<ApolloError | undefined>) {
 | 
			
		||||
  for (const error of errors) {
 | 
			
		||||
    if (error && error.graphQLErrors.length !== 0) {
 | 
			
		||||
      const notFound = error.graphQLErrors.find(e => e.extensions && e.extensions.code === '404');
 | 
			
		||||
      if (notFound) {
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
@@ -7,6 +7,8 @@ import (
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var jwtKey = []byte("taskcafe_test_key")
 | 
			
		||||
 | 
			
		||||
// RestrictedMode is used restrict JWT access to just the install route
 | 
			
		||||
type RestrictedMode string
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +54,7 @@ func (r *ErrMalformedToken) Error() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAccessToken generates a new JWT access token with the correct claims
 | 
			
		||||
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string, jwtKey []byte) (string, error) {
 | 
			
		||||
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string) (string, error) {
 | 
			
		||||
	role := RoleMember
 | 
			
		||||
	if orgRole == "admin" {
 | 
			
		||||
		role = RoleAdmin
 | 
			
		||||
@@ -74,7 +76,7 @@ func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAccessTokenCustomExpiration creates an access token with a custom duration
 | 
			
		||||
func NewAccessTokenCustomExpiration(userID string, dur time.Duration, jwtKey []byte) (string, error) {
 | 
			
		||||
func NewAccessTokenCustomExpiration(userID string, dur time.Duration) (string, error) {
 | 
			
		||||
	accessExpirationTime := time.Now().Add(dur)
 | 
			
		||||
	accessClaims := &AccessTokenClaims{
 | 
			
		||||
		UserID:         userID,
 | 
			
		||||
@@ -92,7 +94,7 @@ func NewAccessTokenCustomExpiration(userID string, dur time.Duration, jwtKey []b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
func ValidateAccessToken(accessTokenString string) (AccessTokenClaims, error) {
 | 
			
		||||
	accessClaims := &AccessTokenClaims{}
 | 
			
		||||
	accessToken, err := jwt.ParseWithClaims(accessTokenString, accessClaims, func(token *jwt.Token) (interface{}, error) {
 | 
			
		||||
		return jwtKey, nil
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,12 @@
 | 
			
		||||
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 {
 | 
			
		||||
@@ -18,18 +15,13 @@ func newTokenCmd() *cobra.Command {
 | 
			
		||||
		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))
 | 
			
		||||
		Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
			token, err := auth.NewAccessTokenCustomExpiration(args[0], time.Hour*24)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.WithError(err).Error("issue while creating access token")
 | 
			
		||||
				return err
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Println(token)
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,11 @@ package commands
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang-migrate/migrate/v4"
 | 
			
		||||
	"github.com/golang-migrate/migrate/v4/database/postgres"
 | 
			
		||||
	"github.com/golang-migrate/migrate/v4/source/httpfs"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
 | 
			
		||||
@@ -40,21 +38,16 @@ func newWebCmd() *cobra.Command {
 | 
			
		||||
			)
 | 
			
		||||
			var db *sqlx.DB
 | 
			
		||||
			var err error
 | 
			
		||||
			var retryDuration time.Duration
 | 
			
		||||
			maxRetryNumber := 4
 | 
			
		||||
			for i := 0; i < maxRetryNumber; i++ {
 | 
			
		||||
			retryNumber := 0
 | 
			
		||||
			for i := 0; retryNumber <= 3; i++ {
 | 
			
		||||
				retryNumber++
 | 
			
		||||
				db, err = sqlx.Connect("postgres", connection)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				retryDuration = time.Duration(i*2) * time.Second
 | 
			
		||||
				log.WithFields(log.Fields{"retryNumber": i, "retryDuration": retryDuration}).WithError(err).Error("issue connecting to database, retrying")
 | 
			
		||||
				if i != maxRetryNumber-1 {
 | 
			
		||||
					time.Sleep(retryDuration)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
				retryDuration := time.Duration(i*2) * time.Second
 | 
			
		||||
				log.WithFields(log.Fields{"retryNumber": retryNumber, "retryDuration": retryDuration}).WithError(err).Error("issue connecting to database, retrying")
 | 
			
		||||
				time.Sleep(retryDuration)
 | 
			
		||||
			}
 | 
			
		||||
			db.SetMaxOpenConns(25)
 | 
			
		||||
			db.SetMaxIdleConns(25)
 | 
			
		||||
@@ -69,12 +62,7 @@ func newWebCmd() *cobra.Command {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
 | 
			
		||||
			secret := viper.GetString("server.secret")
 | 
			
		||||
			if strings.TrimSpace(secret) == "" {
 | 
			
		||||
				log.Warn("server.secret is not set, generating a random secret")
 | 
			
		||||
				secret = uuid.New().String()
 | 
			
		||||
			}
 | 
			
		||||
			r, _ := route.NewRouter(db, []byte(secret))
 | 
			
		||||
			r, _ := route.NewRouter(db)
 | 
			
		||||
			http.ListenAndServe(viper.GetString("server.hostname"), r)
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
 
 | 
			
		||||
@@ -2648,6 +2648,7 @@ enum ObjectType {
 | 
			
		||||
  TEAM
 | 
			
		||||
  PROJECT
 | 
			
		||||
  TASK
 | 
			
		||||
  TASK_GROUP
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
 | 
			
		||||
@@ -2851,20 +2852,20 @@ type UpdateProjectMemberRolePayload {
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createTask(input: NewTask!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
 | 
			
		||||
  deleteTask(input: DeleteTaskInput!):
 | 
			
		||||
    DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
 | 
			
		||||
  updateTaskDescription(input: UpdateTaskDescriptionInput!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskLocation(input: NewTaskLocation!):
 | 
			
		||||
    UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskName(input: UpdateTaskName!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  setTaskComplete(input: SetTaskComplete!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskDueDate(input: UpdateTaskDueDate!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
 | 
			
		||||
  assignTask(input: AssignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
@@ -2873,7 +2874,7 @@ extend type Mutation {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTask {
 | 
			
		||||
  taskGroupID: String!
 | 
			
		||||
  taskGroupID: UUID!
 | 
			
		||||
  name: String!
 | 
			
		||||
  position: Float!
 | 
			
		||||
}
 | 
			
		||||
@@ -6533,7 +6534,7 @@ func (ec *executionContext) _Mutation_createTask(ctx context.Context, field grap
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK_GROUP")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -6606,7 +6607,7 @@ func (ec *executionContext) _Mutation_deleteTask(ctx context.Context, field grap
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -6679,7 +6680,7 @@ func (ec *executionContext) _Mutation_updateTaskDescription(ctx context.Context,
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -6752,7 +6753,7 @@ func (ec *executionContext) _Mutation_updateTaskLocation(ctx context.Context, fi
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -6825,7 +6826,7 @@ func (ec *executionContext) _Mutation_updateTaskName(ctx context.Context, field
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -6898,7 +6899,7 @@ func (ec *executionContext) _Mutation_setTaskComplete(ctx context.Context, field
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -6971,7 +6972,7 @@ func (ec *executionContext) _Mutation_updateTaskDueDate(ctx context.Context, fie
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -15194,7 +15195,7 @@ func (ec *executionContext) unmarshalInputNewTask(ctx context.Context, obj inter
 | 
			
		||||
		switch k {
 | 
			
		||||
		case "taskGroupID":
 | 
			
		||||
			var err error
 | 
			
		||||
			it.TaskGroupID, err = ec.unmarshalNString2string(ctx, v)
 | 
			
		||||
			it.TaskGroupID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return it, err
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ import (
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/db"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/utils"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/vektah/gqlparser/v2/gqlerror"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// NewHandler returns a new graphql endpoint handler.
 | 
			
		||||
@@ -51,6 +52,8 @@ func NewHandler(repo db.Repository) http.Handler {
 | 
			
		||||
			fieldName = "TeamID"
 | 
			
		||||
		case ObjectTypeTask:
 | 
			
		||||
			fieldName = "TaskID"
 | 
			
		||||
		case ObjectTypeTaskGroup:
 | 
			
		||||
			fieldName = "TaskGroupID"
 | 
			
		||||
		default:
 | 
			
		||||
			fieldName = "ProjectID"
 | 
			
		||||
		}
 | 
			
		||||
@@ -68,6 +71,13 @@ func NewHandler(repo db.Repository) http.Handler {
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				}
 | 
			
		||||
			} else if typeArg == ObjectTypeTaskGroup {
 | 
			
		||||
				log.WithFields(log.Fields{"subjectID": subjectID}).Info("fetching project ID using task group ID")
 | 
			
		||||
				taskGroup, err := repo.GetTaskGroupByID(ctx, subjectID)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				}
 | 
			
		||||
				subjectID = taskGroup.ProjectID
 | 
			
		||||
			}
 | 
			
		||||
			roles, err := GetProjectRoles(ctx, repo, subjectID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
@@ -186,3 +196,13 @@ func GetActionType(actionType int32) ActionType {
 | 
			
		||||
		panic("Not a valid entity type!")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NotFoundError creates a 404 gqlerror
 | 
			
		||||
func NotFoundError(message string) error {
 | 
			
		||||
	return &gqlerror.Error{
 | 
			
		||||
		Message: message,
 | 
			
		||||
		Extensions: map[string]interface{}{
 | 
			
		||||
			"code": "404",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -229,9 +229,9 @@ type NewRefreshToken struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NewTask struct {
 | 
			
		||||
	TaskGroupID string  `json:"taskGroupID"`
 | 
			
		||||
	Name        string  `json:"name"`
 | 
			
		||||
	Position    float64 `json:"position"`
 | 
			
		||||
	TaskGroupID uuid.UUID `json:"taskGroupID"`
 | 
			
		||||
	Name        string    `json:"name"`
 | 
			
		||||
	Position    float64   `json:"position"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NewTaskGroup struct {
 | 
			
		||||
@@ -648,10 +648,11 @@ func (e EntityType) MarshalGQL(w io.Writer) {
 | 
			
		||||
type ObjectType string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ObjectTypeOrg     ObjectType = "ORG"
 | 
			
		||||
	ObjectTypeTeam    ObjectType = "TEAM"
 | 
			
		||||
	ObjectTypeProject ObjectType = "PROJECT"
 | 
			
		||||
	ObjectTypeTask    ObjectType = "TASK"
 | 
			
		||||
	ObjectTypeOrg       ObjectType = "ORG"
 | 
			
		||||
	ObjectTypeTeam      ObjectType = "TEAM"
 | 
			
		||||
	ObjectTypeProject   ObjectType = "PROJECT"
 | 
			
		||||
	ObjectTypeTask      ObjectType = "TASK"
 | 
			
		||||
	ObjectTypeTaskGroup ObjectType = "TASK_GROUP"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AllObjectType = []ObjectType{
 | 
			
		||||
@@ -659,11 +660,12 @@ var AllObjectType = []ObjectType{
 | 
			
		||||
	ObjectTypeTeam,
 | 
			
		||||
	ObjectTypeProject,
 | 
			
		||||
	ObjectTypeTask,
 | 
			
		||||
	ObjectTypeTaskGroup,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ObjectType) IsValid() bool {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask:
 | 
			
		||||
	case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask, ObjectTypeTaskGroup:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
 
 | 
			
		||||
@@ -174,6 +174,7 @@ enum ObjectType {
 | 
			
		||||
  TEAM
 | 
			
		||||
  PROJECT
 | 
			
		||||
  TASK
 | 
			
		||||
  TASK_GROUP
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
 | 
			
		||||
@@ -377,20 +378,20 @@ type UpdateProjectMemberRolePayload {
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createTask(input: NewTask!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
 | 
			
		||||
  deleteTask(input: DeleteTaskInput!):
 | 
			
		||||
    DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
 | 
			
		||||
  updateTaskDescription(input: UpdateTaskDescriptionInput!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskLocation(input: NewTaskLocation!):
 | 
			
		||||
    UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskName(input: UpdateTaskName!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  setTaskComplete(input: SetTaskComplete!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskDueDate(input: UpdateTaskDueDate!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
 | 
			
		||||
  assignTask(input: AssignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
@@ -399,7 +400,7 @@ extend type Mutation {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTask {
 | 
			
		||||
  taskGroupID: String!
 | 
			
		||||
  taskGroupID: UUID!
 | 
			
		||||
  name: String!
 | 
			
		||||
  position: Float!
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -179,14 +179,9 @@ func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input Up
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
 | 
			
		||||
	taskGroupID, err := uuid.Parse(input.TaskGroupID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("issue while parsing task group ID")
 | 
			
		||||
		return &db.Task{}, err
 | 
			
		||||
	}
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
	log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": taskGroupID}).Info("creating task")
 | 
			
		||||
	task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{taskGroupID, createdAt, input.Name, input.Position})
 | 
			
		||||
	log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task")
 | 
			
		||||
	task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{input.TaskGroupID, createdAt, input.Name, input.Position})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("issue while creating task")
 | 
			
		||||
		return &db.Task{}, err
 | 
			
		||||
@@ -238,6 +233,9 @@ func (r *mutationResolver) SetTaskComplete(ctx context.Context, input SetTaskCom
 | 
			
		||||
	completedAt := time.Now().UTC()
 | 
			
		||||
	task, err := r.Repository.SetTaskComplete(ctx, db.SetTaskCompleteParams{TaskID: input.TaskID, Complete: input.Complete, CompletedAt: sql.NullTime{Time: completedAt, Valid: true}})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == sql.ErrNoRows {
 | 
			
		||||
			return &db.Task{}, NotFoundError("task does not exist")
 | 
			
		||||
		}
 | 
			
		||||
		return &db.Task{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return &task, nil
 | 
			
		||||
@@ -1033,6 +1031,14 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) {
 | 
			
		||||
	task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
 | 
			
		||||
	if err == sql.ErrNoRows {
 | 
			
		||||
		return &db.Task{}, &gqlerror.Error{
 | 
			
		||||
			Message: "Task does not exist",
 | 
			
		||||
			Extensions: map[string]interface{}{
 | 
			
		||||
				"code": "404",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &task, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1240,6 +1246,9 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er
 | 
			
		||||
	taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
 | 
			
		||||
	taskMembers := []Member{}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == sql.ErrNoRows {
 | 
			
		||||
			return taskMembers, nil
 | 
			
		||||
		}
 | 
			
		||||
		return taskMembers, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, taskMemberLink := range taskMemberLinks {
 | 
			
		||||
@@ -1274,11 +1283,19 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *taskResolver) Labels(ctx context.Context, obj *db.Task) ([]db.TaskLabel, error) {
 | 
			
		||||
	return r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
 | 
			
		||||
	labels, err := r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
 | 
			
		||||
	if err != nil && err != sql.ErrNoRows {
 | 
			
		||||
		return []db.TaskLabel{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return labels, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *taskResolver) Checklists(ctx context.Context, obj *db.Task) ([]db.TaskChecklist, error) {
 | 
			
		||||
	return r.Repository.GetTaskChecklistsForTask(ctx, obj.TaskID)
 | 
			
		||||
	checklists, err := r.Repository.GetTaskChecklistsForTask(ctx, obj.TaskID)
 | 
			
		||||
	if err != nil && err != sql.ErrNoRows {
 | 
			
		||||
		return []db.TaskChecklist{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return checklists, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ enum ObjectType {
 | 
			
		||||
  TEAM
 | 
			
		||||
  PROJECT
 | 
			
		||||
  TASK
 | 
			
		||||
  TASK_GROUP
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createTask(input: NewTask!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
 | 
			
		||||
  deleteTask(input: DeleteTaskInput!):
 | 
			
		||||
    DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
 | 
			
		||||
  updateTaskDescription(input: UpdateTaskDescriptionInput!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskLocation(input: NewTaskLocation!):
 | 
			
		||||
    UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskName(input: UpdateTaskName!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  setTaskComplete(input: SetTaskComplete!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  updateTaskDueDate(input: UpdateTaskDueDate!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
 | 
			
		||||
  assignTask(input: AssignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
@@ -22,7 +22,7 @@ extend type Mutation {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTask {
 | 
			
		||||
  taskGroupID: String!
 | 
			
		||||
  taskGroupID: UUID!
 | 
			
		||||
  name: String!
 | 
			
		||||
  position: Float!
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,8 @@ import (
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var jwtKey = []byte("taskcafe_test_key")
 | 
			
		||||
 | 
			
		||||
type authResource struct{}
 | 
			
		||||
 | 
			
		||||
// LoginRequestData is the request data when a user logs in
 | 
			
		||||
@@ -67,7 +69,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
 | 
			
		||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode, h.jwtKey)
 | 
			
		||||
		accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
@@ -121,7 +123,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
@@ -188,7 +190,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
 | 
			
		||||
	refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
			
		||||
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
@@ -249,12 +251,10 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
 | 
			
		||||
	refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
 | 
			
		||||
	refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
			
		||||
 | 
			
		||||
	log.WithField("userID", user.UserID.String()).Info("creating install access token")
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	log.Info(accessTokenString)
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-type", "application/json")
 | 
			
		||||
	http.SetCookie(w, &http.Cookie{
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,7 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
@@ -50,24 +48,22 @@ func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Requ
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
	filename := strings.ReplaceAll(handler.Filename, " ", "-")
 | 
			
		||||
	encodedFilename := url.QueryEscape(filename)
 | 
			
		||||
	log.WithFields(log.Fields{"filename": encodedFilename, "size": handler.Size, "header": handler.Header}).Info("file metadata")
 | 
			
		||||
	log.WithFields(log.Fields{"filename": handler.Filename, "size": handler.Size, "header": handler.Header}).Info("file metadata")
 | 
			
		||||
 | 
			
		||||
	fileBytes, err := ioutil.ReadAll(file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("while reading file")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = ioutil.WriteFile("uploads/"+filename, fileBytes, 0644)
 | 
			
		||||
	err = ioutil.WriteFile("uploads/"+handler.Filename, fileBytes, 0644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("while reading file")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.repo.UpdateUserAccountProfileAvatarURL(r.Context(), db.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{String: "/uploads/" + encodedFilename, Valid: true}})
 | 
			
		||||
	h.repo.UpdateUserAccountProfileAvatarURL(r.Context(), db.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{String: "http://localhost:3333/uploads/" + handler.Filename, Valid: true}})
 | 
			
		||||
	// return that we have successfully uploaded our file!
 | 
			
		||||
	log.Info("file uploaded")
 | 
			
		||||
	json.NewEncoder(w).Encode(AvatarUploadResponseData{URL: "/uploads/" + encodedFilename, UserID: userID.String()})
 | 
			
		||||
	json.NewEncoder(w).Encode(AvatarUploadResponseData{URL: "http://localhost:3333/uploads/" + handler.Filename, UserID: userID.String()})
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AuthenticationMiddleware is a middleware that requires a valid JWT token to be passed via the Authorization header
 | 
			
		||||
type AuthenticationMiddleware struct {
 | 
			
		||||
	jwtKey []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Middleware returns the middleware handler
 | 
			
		||||
func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
 | 
			
		||||
func AuthenticationMiddleware(next http.Handler) http.Handler {
 | 
			
		||||
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		bearerTokenRaw := r.Header.Get("Authorization")
 | 
			
		||||
		splitToken := strings.Split(bearerTokenRaw, "Bearer")
 | 
			
		||||
@@ -26,7 +21,7 @@ func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		accessTokenString := strings.TrimSpace(splitToken[1])
 | 
			
		||||
		accessClaims, err := auth.ValidateAccessToken(accessTokenString, m.jwtKey)
 | 
			
		||||
		accessClaims, err := auth.ValidateAccessToken(accessTokenString)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if _, ok := err.(*auth.ErrExpiredToken); ok {
 | 
			
		||||
				w.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
 
 | 
			
		||||
@@ -59,12 +59,11 @@ func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
// TaskcafeHandler contains all the route handlers
 | 
			
		||||
type TaskcafeHandler struct {
 | 
			
		||||
	repo   db.Repository
 | 
			
		||||
	jwtKey []byte
 | 
			
		||||
	repo db.Repository
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewRouter creates a new router for chi
 | 
			
		||||
func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
 | 
			
		||||
func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
 | 
			
		||||
	formatter := new(log.TextFormatter)
 | 
			
		||||
	formatter.TimestampFormat = "02-01-2006 15:04:05"
 | 
			
		||||
	formatter.FullTimestamp = true
 | 
			
		||||
@@ -80,7 +79,7 @@ func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
 | 
			
		||||
	r.Use(middleware.Timeout(60 * time.Second))
 | 
			
		||||
 | 
			
		||||
	repository := db.NewRepository(dbConnection)
 | 
			
		||||
	taskcafeHandler := TaskcafeHandler{*repository, jwtKey}
 | 
			
		||||
	taskcafeHandler := TaskcafeHandler{*repository}
 | 
			
		||||
 | 
			
		||||
	var imgServer = http.FileServer(http.Dir("./uploads/"))
 | 
			
		||||
	r.Group(func(mux chi.Router) {
 | 
			
		||||
@@ -89,9 +88,8 @@ func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
 | 
			
		||||
		mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
 | 
			
		||||
 | 
			
		||||
	})
 | 
			
		||||
	auth := AuthenticationMiddleware{jwtKey}
 | 
			
		||||
	r.Group(func(mux chi.Router) {
 | 
			
		||||
		mux.Use(auth.Middleware)
 | 
			
		||||
		mux.Use(AuthenticationMiddleware)
 | 
			
		||||
		mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
 | 
			
		||||
		mux.Post("/auth/install", taskcafeHandler.InstallHandler)
 | 
			
		||||
		mux.Handle("/graphql", graph.NewHandler(*repository))
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user