Compare commits
	
		
			2 Commits
		
	
	
		
			0.2.2
			...
			feat/list-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0eb9c0db94 | ||
| 
						 | 
					cdcccbfb4a | 
							
								
								
									
										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!
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 | 
			
		||||
### Added
 | 
			
		||||
- Task sorting & filtering
 | 
			
		||||
- Redesigned the Task Details UI
 | 
			
		||||
- Implement task group actions (duplicate/delete all tasks/sort)
 | 
			
		||||
 | 
			
		||||
### Fixed
 | 
			
		||||
- removed CORS middleware to fix security issue
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,6 @@ Was this project useful? Please consider <a href="https://www.buymeacoffee.com/j
 | 
			
		||||
 | 
			
		||||
**Please note that this project is still in active development. Some options may not work yet! For updates on development, join the Discord server**
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
Currently Taskcafe only offers basic task tracking through a Kanban board.
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -171,7 +171,7 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
 | 
			
		||||
 | 
			
		||||
const AdminRoute = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.title = 'Admin | Taskcafé';
 | 
			
		||||
    document.title = 'Taskcafé | Admin';
 | 
			
		||||
  }, []);
 | 
			
		||||
  const { loading, data } = useUsersQuery();
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,20 +3,11 @@ 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,
 | 
			
		||||
  useClearProfileAvatarMutation,
 | 
			
		||||
  useUpdateUserPasswordMutation,
 | 
			
		||||
  useUpdateUserInfoMutation,
 | 
			
		||||
  MeQuery,
 | 
			
		||||
  MeDocument,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
import { useMeQuery, useClearProfileAvatarMutation, useUpdateUserPasswordMutation } from 'shared/generated/graphql';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { useCurrentUser } from 'App/context';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import { toast } from 'react-toastify';
 | 
			
		||||
import updateApolloCache from 'shared/utils/cache';
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
 | 
			
		||||
const MainContent = styled.div`
 | 
			
		||||
  padding: 0 0 50px 80px;
 | 
			
		||||
@@ -28,7 +19,6 @@ const Projects = () => {
 | 
			
		||||
  const $fileUpload = useRef<HTMLInputElement>(null);
 | 
			
		||||
  const [clearProfileAvatar] = useClearProfileAvatarMutation();
 | 
			
		||||
  const { user } = useCurrentUser();
 | 
			
		||||
  const [updateUserInfo] = useUpdateUserInfoMutation();
 | 
			
		||||
  const [updateUserPassword] = useUpdateUserPasswordMutation();
 | 
			
		||||
  const { loading, data, refetch } = useMeQuery();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@@ -79,13 +69,6 @@ const Projects = () => {
 | 
			
		||||
            toast('Password was changed!');
 | 
			
		||||
            done();
 | 
			
		||||
          }}
 | 
			
		||||
          onChangeUserInfo={(d, done) => {
 | 
			
		||||
            updateUserInfo({
 | 
			
		||||
              variables: { name: d.full_name, bio: d.bio, email: d.email, initials: d.initials },
 | 
			
		||||
            });
 | 
			
		||||
            toast('User info was saved!');
 | 
			
		||||
            done();
 | 
			
		||||
          }}
 | 
			
		||||
          onProfileAvatarRemove={() => {
 | 
			
		||||
            clearProfileAvatar();
 | 
			
		||||
          }}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@ import {
 | 
			
		||||
  useCreateProjectMutation,
 | 
			
		||||
  GetProjectsDocument,
 | 
			
		||||
  GetProjectsQuery,
 | 
			
		||||
  MeQuery,
 | 
			
		||||
  MeDocument,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
@@ -262,7 +260,11 @@ const Projects = () => {
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <span>loading</span>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
 | 
			
		||||
 
 | 
			
		||||
@@ -85,30 +85,18 @@ type TeamsRouteProps = {
 | 
			
		||||
const Teams = () => {
 | 
			
		||||
  const { teamID } = useParams<TeamsRouteProps>();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({
 | 
			
		||||
    variables: { teamID },
 | 
			
		||||
    onCompleted: resp => {
 | 
			
		||||
      document.title = `${resp.findTeam.name} | Taskcafé`;
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({ variables: { teamID } });
 | 
			
		||||
  const { user } = useCurrentUser();
 | 
			
		||||
  const [currentTab, setCurrentTab] = useState(0);
 | 
			
		||||
  const match = useRouteMatch();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.title = 'Teams | Taskcafé';
 | 
			
		||||
  }, []);
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return (
 | 
			
		||||
      <GlobalTopNavbar
 | 
			
		||||
        menuType={[
 | 
			
		||||
          { name: 'Projects', link: `${match.url}` },
 | 
			
		||||
          { name: 'Members', link: `${match.url}/members` },
 | 
			
		||||
        ]}
 | 
			
		||||
        currentTab={currentTab}
 | 
			
		||||
        onSetTab={tab => {
 | 
			
		||||
          setCurrentTab(tab);
 | 
			
		||||
        }}
 | 
			
		||||
        onSaveProjectName={NOOP}
 | 
			
		||||
        projectID={null}
 | 
			
		||||
        name={null}
 | 
			
		||||
      />
 | 
			
		||||
      <>
 | 
			
		||||
        <span>loading</span>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
 
 | 
			
		||||
@@ -557,7 +557,6 @@ const Admin: React.FC<AdminProps> = ({
 | 
			
		||||
        <TabNavContent>
 | 
			
		||||
          {items.map((item, idx) => (
 | 
			
		||||
            <NavItem
 | 
			
		||||
              key={item.name}
 | 
			
		||||
              onClick={(tab, top) => {
 | 
			
		||||
                if ($tabNav && $tabNav.current) {
 | 
			
		||||
                  const pos = $tabNav.current.getBoundingClientRect();
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -147,11 +147,6 @@ export const ListCardLabelText = styled.span`
 | 
			
		||||
  line-height: 16px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ListCardLabelsWrapper = styled.div`
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  position: relative;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.variant === 'small'
 | 
			
		||||
@@ -183,6 +178,8 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
@@ -18,7 +20,6 @@ import {
 | 
			
		||||
  ListCardLabels,
 | 
			
		||||
  ListCardLabel,
 | 
			
		||||
  ListCardLabelText,
 | 
			
		||||
  ListCardLabelsWrapper,
 | 
			
		||||
  ListCardOperation,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
  CardMembers,
 | 
			
		||||
@@ -153,42 +154,39 @@ const Card = React.forwardRef(
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Pencil width={8} height={8} />
 | 
			
		||||
              <FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
 | 
			
		||||
            </ListCardOperation>
 | 
			
		||||
          )}
 | 
			
		||||
          <ListCardDetails complete={complete ?? false}>
 | 
			
		||||
            {labels && labels.length !== 0 && (
 | 
			
		||||
              <ListCardLabelsWrapper>
 | 
			
		||||
                <ListCardLabels
 | 
			
		||||
                  toggleLabels={toggleLabels}
 | 
			
		||||
                  toggleDirection={toggleDirection}
 | 
			
		||||
                  onClick={e => {
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
                    if (onCardLabelClick) {
 | 
			
		||||
                      onCardLabelClick();
 | 
			
		||||
                    }
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {labels
 | 
			
		||||
                    .slice()
 | 
			
		||||
                    .sort((a, b) => a.labelColor.position - b.labelColor.position)
 | 
			
		||||
                    .map(label => (
 | 
			
		||||
                      <ListCardLabel
 | 
			
		||||
                        onAnimationEnd={() => {
 | 
			
		||||
                          if (setToggleLabels) {
 | 
			
		||||
                            setToggleLabels(false);
 | 
			
		||||
                          }
 | 
			
		||||
                        }}
 | 
			
		||||
                        variant={labelVariant ?? 'large'}
 | 
			
		||||
                        color={label.labelColor.colorHex}
 | 
			
		||||
                        key={label.id}
 | 
			
		||||
                      >
 | 
			
		||||
                        <ListCardLabelText>{label.name}</ListCardLabelText>
 | 
			
		||||
                      </ListCardLabel>
 | 
			
		||||
                    ))}
 | 
			
		||||
                </ListCardLabels>
 | 
			
		||||
              </ListCardLabelsWrapper>
 | 
			
		||||
            )}
 | 
			
		||||
            <ListCardLabels
 | 
			
		||||
              toggleLabels={toggleLabels}
 | 
			
		||||
              toggleDirection={toggleDirection}
 | 
			
		||||
              onClick={e => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                if (onCardLabelClick) {
 | 
			
		||||
                  onCardLabelClick();
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {labels &&
 | 
			
		||||
                labels
 | 
			
		||||
                  .slice()
 | 
			
		||||
                  .sort((a, b) => a.labelColor.position - b.labelColor.position)
 | 
			
		||||
                  .map(label => (
 | 
			
		||||
                    <ListCardLabel
 | 
			
		||||
                      onAnimationEnd={() => {
 | 
			
		||||
                        if (setToggleLabels) {
 | 
			
		||||
                          setToggleLabels(false);
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                      variant={labelVariant ?? 'large'}
 | 
			
		||||
                      color={label.labelColor.colorHex}
 | 
			
		||||
                      key={label.id}
 | 
			
		||||
                    >
 | 
			
		||||
                      <ListCardLabelText>{label.name}</ListCardLabelText>
 | 
			
		||||
                    </ListCardLabel>
 | 
			
		||||
                  ))}
 | 
			
		||||
            </ListCardLabels>
 | 
			
		||||
            {editable ? (
 | 
			
		||||
              <EditorContent>
 | 
			
		||||
                {complete && <CompleteIcon width={16} height={16} />}
 | 
			
		||||
@@ -216,18 +214,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>
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,6 @@ const Icon = styled.div`
 | 
			
		||||
 | 
			
		||||
type InputProps = {
 | 
			
		||||
  variant?: 'normal' | 'alternate';
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
  label?: string;
 | 
			
		||||
  width?: string;
 | 
			
		||||
  floatingLabel?: boolean;
 | 
			
		||||
@@ -117,7 +116,6 @@ function useCombinedRefs(...refs: any) {
 | 
			
		||||
const Input = React.forwardRef(
 | 
			
		||||
  (
 | 
			
		||||
    {
 | 
			
		||||
      disabled = false,
 | 
			
		||||
      width = 'auto',
 | 
			
		||||
      variant = 'normal',
 | 
			
		||||
      type = 'text',
 | 
			
		||||
@@ -162,7 +160,6 @@ const Input = React.forwardRef(
 | 
			
		||||
          onChange={e => {
 | 
			
		||||
            setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
 | 
			
		||||
          }}
 | 
			
		||||
          disabled={disabled}
 | 
			
		||||
          hasValue={hasValue}
 | 
			
		||||
          ref={combinedRef}
 | 
			
		||||
          id={id}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,6 @@ export const Default = () => {
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Settings
 | 
			
		||||
        profile={profile}
 | 
			
		||||
        onChangeUserInfo={action('change user info')}
 | 
			
		||||
        onResetPassword={action('reset password')}
 | 
			
		||||
        onProfileAvatarRemove={action('remove')}
 | 
			
		||||
        onProfileAvatarChange={action('profile avatar change')}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,11 +10,6 @@ const PasswordInput = styled(Input)`
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const UserInfoInput = styled(Input)`
 | 
			
		||||
  margin-top: 30px;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const FormError = styled.span`
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  color: rgba(${props => props.theme.colors.warning});
 | 
			
		||||
@@ -245,7 +240,6 @@ const SaveButton = styled(Button)`
 | 
			
		||||
type SettingsProps = {
 | 
			
		||||
  onProfileAvatarChange: () => void;
 | 
			
		||||
  onProfileAvatarRemove: () => void;
 | 
			
		||||
  onChangeUserInfo: (data: UserInfoData, done: () => void) => void;
 | 
			
		||||
  onResetPassword: (password: string, done: () => void) => void;
 | 
			
		||||
  profile: TaskUser;
 | 
			
		||||
};
 | 
			
		||||
@@ -306,93 +300,9 @@ const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword })
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type UserInfoData = {
 | 
			
		||||
  full_name: string;
 | 
			
		||||
  bio: string;
 | 
			
		||||
  initials: string;
 | 
			
		||||
  email: string;
 | 
			
		||||
};
 | 
			
		||||
type UserInfoTabProps = {
 | 
			
		||||
  profile: TaskUser;
 | 
			
		||||
  onProfileAvatarChange: () => void;
 | 
			
		||||
  onProfileAvatarRemove: () => void;
 | 
			
		||||
  onChangeUserInfo: (data: UserInfoData, done: () => void) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
 | 
			
		||||
const INITIALS_PATTERN = /^[a-zA-Z]{2,3}$/i;
 | 
			
		||||
 | 
			
		||||
const UserInfoTab: React.FC<UserInfoTabProps> = ({
 | 
			
		||||
  profile,
 | 
			
		||||
  onProfileAvatarRemove,
 | 
			
		||||
  onProfileAvatarChange,
 | 
			
		||||
  onChangeUserInfo,
 | 
			
		||||
}) => {
 | 
			
		||||
  const [active, setActive] = useState(true);
 | 
			
		||||
  const { register, handleSubmit, errors } = useForm<UserInfoData>();
 | 
			
		||||
  const done = () => {
 | 
			
		||||
    setActive(true);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AvatarSettings
 | 
			
		||||
        onProfileAvatarRemove={onProfileAvatarRemove}
 | 
			
		||||
        onProfileAvatarChange={onProfileAvatarChange}
 | 
			
		||||
        profile={profile.profileIcon}
 | 
			
		||||
      />
 | 
			
		||||
      <form
 | 
			
		||||
        onSubmit={handleSubmit(data => {
 | 
			
		||||
          setActive(false);
 | 
			
		||||
          onChangeUserInfo(data, done);
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        <UserInfoInput
 | 
			
		||||
          ref={register({ required: 'Full name is required' })}
 | 
			
		||||
          name="full_name"
 | 
			
		||||
          defaultValue={profile.fullName}
 | 
			
		||||
          width="100%"
 | 
			
		||||
          label="Name"
 | 
			
		||||
        />
 | 
			
		||||
        {errors.full_name && <FormError>{errors.full_name.message}</FormError>}
 | 
			
		||||
        <UserInfoInput
 | 
			
		||||
          defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
 | 
			
		||||
          ref={register({
 | 
			
		||||
            required: 'Initials is required',
 | 
			
		||||
            pattern: { value: INITIALS_PATTERN, message: 'Intials must be between two to four characters' },
 | 
			
		||||
          })}
 | 
			
		||||
          name="initials"
 | 
			
		||||
          width="100%"
 | 
			
		||||
          label="Initials "
 | 
			
		||||
        />
 | 
			
		||||
        {errors.initials && <FormError>{errors.initials.message}</FormError>}
 | 
			
		||||
        <UserInfoInput disabled defaultValue={profile.username ?? ''} width="100%" label="Username " />
 | 
			
		||||
        <UserInfoInput
 | 
			
		||||
          width="100%"
 | 
			
		||||
          name="email"
 | 
			
		||||
          ref={register({
 | 
			
		||||
            required: 'Email is required',
 | 
			
		||||
            pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
 | 
			
		||||
          })}
 | 
			
		||||
          defaultValue={profile.email ?? ''}
 | 
			
		||||
          label="Email"
 | 
			
		||||
        />
 | 
			
		||||
        {errors.email && <FormError>{errors.email.message}</FormError>}
 | 
			
		||||
        <UserInfoInput width="100%" name="bio" ref={register()} defaultValue={profile.bio ?? ''} label="Bio" />
 | 
			
		||||
        {errors.bio && <FormError>{errors.bio.message}</FormError>}
 | 
			
		||||
        <SettingActions>
 | 
			
		||||
          <SaveButton disabled={!active} type="submit">
 | 
			
		||||
            Save Change
 | 
			
		||||
          </SaveButton>
 | 
			
		||||
        </SettingActions>
 | 
			
		||||
      </form>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Settings: React.FC<SettingsProps> = ({
 | 
			
		||||
  onProfileAvatarRemove,
 | 
			
		||||
  onProfileAvatarChange,
 | 
			
		||||
  onChangeUserInfo,
 | 
			
		||||
  onResetPassword,
 | 
			
		||||
  profile,
 | 
			
		||||
}) => {
 | 
			
		||||
@@ -405,7 +315,6 @@ const Settings: React.FC<SettingsProps> = ({
 | 
			
		||||
        <TabNavContent>
 | 
			
		||||
          {items.map((item, idx) => (
 | 
			
		||||
            <NavItem
 | 
			
		||||
              key={item.name}
 | 
			
		||||
              onClick={(tab, top) => {
 | 
			
		||||
                if ($tabNav && $tabNav.current) {
 | 
			
		||||
                  const pos = $tabNav.current.getBoundingClientRect();
 | 
			
		||||
@@ -423,12 +332,23 @@ const Settings: React.FC<SettingsProps> = ({
 | 
			
		||||
      </TabNav>
 | 
			
		||||
      <TabContentWrapper>
 | 
			
		||||
        <Tab tab={0} currentTab={currentTab}>
 | 
			
		||||
          <UserInfoTab
 | 
			
		||||
            onProfileAvatarChange={onProfileAvatarChange}
 | 
			
		||||
          <AvatarSettings
 | 
			
		||||
            onProfileAvatarRemove={onProfileAvatarRemove}
 | 
			
		||||
            profile={profile}
 | 
			
		||||
            onChangeUserInfo={onChangeUserInfo}
 | 
			
		||||
            onProfileAvatarChange={onProfileAvatarChange}
 | 
			
		||||
            profile={profile.profileIcon}
 | 
			
		||||
          />
 | 
			
		||||
          <Input defaultValue={profile.fullName} width="100%" label="Name" />
 | 
			
		||||
          <Input
 | 
			
		||||
            defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
 | 
			
		||||
            width="100%"
 | 
			
		||||
            label="Initials "
 | 
			
		||||
          />
 | 
			
		||||
          <Input defaultValue={profile.username ?? ''} width="100%" label="Username " />
 | 
			
		||||
          <Input width="100%" label="Email" />
 | 
			
		||||
          <Input width="100%" label="Bio" />
 | 
			
		||||
          <SettingActions>
 | 
			
		||||
            <SaveButton>Save Change</SaveButton>
 | 
			
		||||
          </SettingActions>
 | 
			
		||||
        </Tab>
 | 
			
		||||
        <Tab tab={1} currentTab={currentTab}>
 | 
			
		||||
          <ResetPasswordTab onResetPassword={onResetPassword} />
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
@@ -172,7 +169,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
          <LeftSidebarSection>
 | 
			
		||||
            <SidebarTitle>TASK GROUP</SidebarTitle>
 | 
			
		||||
            <SidebarButton>
 | 
			
		||||
              <SidebarButtonText>{task.taskGroup.name}</SidebarButtonText>
 | 
			
		||||
              <SidebarButtonText>Release 0.1.0</SidebarButtonText>
 | 
			
		||||
            </SidebarButton>
 | 
			
		||||
            <DueDateTitle>DUE DATE</DueDateTitle>
 | 
			
		||||
            <SidebarButton
 | 
			
		||||
@@ -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)}>
 | 
			
		||||
 
 | 
			
		||||
@@ -251,8 +251,8 @@ export const NavSeparator = styled.div`
 | 
			
		||||
export const LogoContainer = styled(Link)`
 | 
			
		||||
  display: block;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  right: 50%;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  transform: translateX(-50%);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 
 | 
			
		||||
@@ -105,7 +105,6 @@ export type UserAccount = {
 | 
			
		||||
  createdAt: Scalars['Time'];
 | 
			
		||||
  fullName: Scalars['String'];
 | 
			
		||||
  initials: Scalars['String'];
 | 
			
		||||
  bio: Scalars['String'];
 | 
			
		||||
  role: Role;
 | 
			
		||||
  username: Scalars['String'];
 | 
			
		||||
  profileIcon: ProfileIcon;
 | 
			
		||||
@@ -304,7 +303,6 @@ export type Mutation = {
 | 
			
		||||
  updateTaskLocation: UpdateTaskLocationPayload;
 | 
			
		||||
  updateTaskName: Task;
 | 
			
		||||
  updateTeamMemberRole: UpdateTeamMemberRolePayload;
 | 
			
		||||
  updateUserInfo: UpdateUserInfoPayload;
 | 
			
		||||
  updateUserPassword: UpdateUserPasswordPayload;
 | 
			
		||||
  updateUserRole: UpdateUserRolePayload;
 | 
			
		||||
};
 | 
			
		||||
@@ -550,11 +548,6 @@ export type MutationUpdateTeamMemberRoleArgs = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationUpdateUserInfoArgs = {
 | 
			
		||||
  input: UpdateUserInfo;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationUpdateUserPasswordArgs = {
 | 
			
		||||
  input: UpdateUserPassword;
 | 
			
		||||
};
 | 
			
		||||
@@ -986,18 +979,6 @@ export type UpdateTeamMemberRolePayload = {
 | 
			
		||||
  member: Member;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type UpdateUserInfoPayload = {
 | 
			
		||||
   __typename?: 'UpdateUserInfoPayload';
 | 
			
		||||
  user: UserAccount;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type UpdateUserInfo = {
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  initials: Scalars['String'];
 | 
			
		||||
  email: Scalars['String'];
 | 
			
		||||
  bio: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type UpdateUserPassword = {
 | 
			
		||||
  userID: Scalars['UUID'];
 | 
			
		||||
  password: Scalars['String'];
 | 
			
		||||
@@ -1264,7 +1245,7 @@ export type FindTaskQuery = (
 | 
			
		||||
    & Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'>
 | 
			
		||||
    & { taskGroup: (
 | 
			
		||||
      { __typename?: 'TaskGroup' }
 | 
			
		||||
      & Pick<TaskGroup, 'id' | 'name'>
 | 
			
		||||
      & Pick<TaskGroup, 'id'>
 | 
			
		||||
    ), badges: (
 | 
			
		||||
      { __typename?: 'TaskBadges' }
 | 
			
		||||
      & { checklist?: Maybe<(
 | 
			
		||||
@@ -1373,7 +1354,7 @@ export type MeQuery = (
 | 
			
		||||
    { __typename?: 'MePayload' }
 | 
			
		||||
    & { user: (
 | 
			
		||||
      { __typename?: 'UserAccount' }
 | 
			
		||||
      & Pick<UserAccount, 'id' | 'fullName' | 'username' | 'email' | 'bio'>
 | 
			
		||||
      & Pick<UserAccount, 'id' | 'fullName'>
 | 
			
		||||
      & { profileIcon: (
 | 
			
		||||
        { __typename?: 'ProfileIcon' }
 | 
			
		||||
        & Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
 | 
			
		||||
@@ -2099,7 +2080,7 @@ export type CreateUserAccountMutation = (
 | 
			
		||||
  { __typename?: 'Mutation' }
 | 
			
		||||
  & { createUserAccount: (
 | 
			
		||||
    { __typename?: 'UserAccount' }
 | 
			
		||||
    & Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username' | 'bio'>
 | 
			
		||||
    & Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username'>
 | 
			
		||||
    & { profileIcon: (
 | 
			
		||||
      { __typename?: 'ProfileIcon' }
 | 
			
		||||
      & Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
 | 
			
		||||
@@ -2146,29 +2127,6 @@ export type DeleteUserAccountMutation = (
 | 
			
		||||
  ) }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export type UpdateUserInfoMutationVariables = {
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  initials: Scalars['String'];
 | 
			
		||||
  email: Scalars['String'];
 | 
			
		||||
  bio: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type UpdateUserInfoMutation = (
 | 
			
		||||
  { __typename?: 'Mutation' }
 | 
			
		||||
  & { updateUserInfo: (
 | 
			
		||||
    { __typename?: 'UpdateUserInfoPayload' }
 | 
			
		||||
    & { user: (
 | 
			
		||||
      { __typename?: 'UserAccount' }
 | 
			
		||||
      & Pick<UserAccount, 'id' | 'email' | 'fullName' | 'bio'>
 | 
			
		||||
      & { profileIcon: (
 | 
			
		||||
        { __typename?: 'ProfileIcon' }
 | 
			
		||||
        & Pick<ProfileIcon, 'initials'>
 | 
			
		||||
      ) }
 | 
			
		||||
    ) }
 | 
			
		||||
  ) }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export type UpdateUserPasswordMutationVariables = {
 | 
			
		||||
  userID: Scalars['UUID'];
 | 
			
		||||
  password: Scalars['String'];
 | 
			
		||||
@@ -2702,7 +2660,6 @@ export const FindTaskDocument = gql`
 | 
			
		||||
    complete
 | 
			
		||||
    taskGroup {
 | 
			
		||||
      id
 | 
			
		||||
      name
 | 
			
		||||
    }
 | 
			
		||||
    badges {
 | 
			
		||||
      checklist {
 | 
			
		||||
@@ -2838,9 +2795,6 @@ export const MeDocument = gql`
 | 
			
		||||
    user {
 | 
			
		||||
      id
 | 
			
		||||
      fullName
 | 
			
		||||
      username
 | 
			
		||||
      email
 | 
			
		||||
      bio
 | 
			
		||||
      profileIcon {
 | 
			
		||||
        initials
 | 
			
		||||
        bgColor
 | 
			
		||||
@@ -4317,7 +4271,6 @@ export const CreateUserAccountDocument = gql`
 | 
			
		||||
    fullName
 | 
			
		||||
    initials
 | 
			
		||||
    username
 | 
			
		||||
    bio
 | 
			
		||||
    profileIcon {
 | 
			
		||||
      url
 | 
			
		||||
      initials
 | 
			
		||||
@@ -4416,49 +4369,6 @@ export function useDeleteUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
 | 
			
		||||
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
 | 
			
		||||
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
 | 
			
		||||
export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
 | 
			
		||||
export const UpdateUserInfoDocument = gql`
 | 
			
		||||
    mutation updateUserInfo($name: String!, $initials: String!, $email: String!, $bio: String!) {
 | 
			
		||||
  updateUserInfo(input: {name: $name, initials: $initials, email: $email, bio: $bio}) {
 | 
			
		||||
    user {
 | 
			
		||||
      id
 | 
			
		||||
      email
 | 
			
		||||
      fullName
 | 
			
		||||
      bio
 | 
			
		||||
      profileIcon {
 | 
			
		||||
        initials
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
    `;
 | 
			
		||||
export type UpdateUserInfoMutationFn = ApolloReactCommon.MutationFunction<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * __useUpdateUserInfoMutation__
 | 
			
		||||
 *
 | 
			
		||||
 * To run a mutation, you first call `useUpdateUserInfoMutation` within a React component and pass it any options that fit your needs.
 | 
			
		||||
 * When your component renders, `useUpdateUserInfoMutation` returns a tuple that includes:
 | 
			
		||||
 * - A mutate function that you can call at any time to execute the mutation
 | 
			
		||||
 * - An object with fields that represent the current status of the mutation's execution
 | 
			
		||||
 *
 | 
			
		||||
 * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * const [updateUserInfoMutation, { data, loading, error }] = useUpdateUserInfoMutation({
 | 
			
		||||
 *   variables: {
 | 
			
		||||
 *      name: // value for 'name'
 | 
			
		||||
 *      initials: // value for 'initials'
 | 
			
		||||
 *      email: // value for 'email'
 | 
			
		||||
 *      bio: // value for 'bio'
 | 
			
		||||
 *   },
 | 
			
		||||
 * });
 | 
			
		||||
 */
 | 
			
		||||
export function useUpdateUserInfoMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>) {
 | 
			
		||||
        return ApolloReactHooks.useMutation<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>(UpdateUserInfoDocument, baseOptions);
 | 
			
		||||
      }
 | 
			
		||||
export type UpdateUserInfoMutationHookResult = ReturnType<typeof useUpdateUserInfoMutation>;
 | 
			
		||||
export type UpdateUserInfoMutationResult = ApolloReactCommon.MutationResult<UpdateUserInfoMutation>;
 | 
			
		||||
export type UpdateUserInfoMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>;
 | 
			
		||||
export const UpdateUserPasswordDocument = gql`
 | 
			
		||||
    mutation updateUserPassword($userID: UUID!, $password: String!) {
 | 
			
		||||
  updateUserPassword(input: {userID: $userID, password: $password}) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ query findTask($taskID: UUID!) {
 | 
			
		||||
    complete
 | 
			
		||||
    taskGroup {
 | 
			
		||||
      id
 | 
			
		||||
      name
 | 
			
		||||
    }
 | 
			
		||||
    badges {
 | 
			
		||||
      checklist {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,6 @@ query me {
 | 
			
		||||
    user {
 | 
			
		||||
      id
 | 
			
		||||
      fullName
 | 
			
		||||
      username
 | 
			
		||||
      email
 | 
			
		||||
      bio
 | 
			
		||||
      profileIcon {
 | 
			
		||||
        initials
 | 
			
		||||
        bgColor
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,6 @@ export const CREATE_USER_MUTATION = gql`
 | 
			
		||||
      fullName
 | 
			
		||||
      initials
 | 
			
		||||
      username
 | 
			
		||||
      bio
 | 
			
		||||
      profileIcon {
 | 
			
		||||
        url
 | 
			
		||||
        initials
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import gql from 'graphql-tag';
 | 
			
		||||
 | 
			
		||||
export const UPDATE_USER_INFO_MUTATION = gql`
 | 
			
		||||
  mutation updateUserInfo($name: String!, $initials: String!, $email: String!, $bio: String!) {
 | 
			
		||||
    updateUserInfo(input: { name: $name, initials: $initials, email: $email, bio: $bio }) {
 | 
			
		||||
      user {
 | 
			
		||||
        id
 | 
			
		||||
        email
 | 
			
		||||
        fullName
 | 
			
		||||
        bio
 | 
			
		||||
        profileIcon {
 | 
			
		||||
          initials
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default UPDATE_USER_INFO_MUTATION;
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								frontend/src/taskcafe.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								frontend/src/taskcafe.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -46,8 +46,6 @@ type OwnedList = {
 | 
			
		||||
type TaskUser = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  fullName: string;
 | 
			
		||||
  email?: string;
 | 
			
		||||
  bio?: string;
 | 
			
		||||
  profileIcon: ProfileIcon;
 | 
			
		||||
  username?: string;
 | 
			
		||||
  role?: Role;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
		},
 | 
			
		||||
 
 | 
			
		||||
@@ -157,5 +157,4 @@ type UserAccount struct {
 | 
			
		||||
	Initials         string         `json:"initials"`
 | 
			
		||||
	ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
 | 
			
		||||
	RoleCode         string         `json:"role_code"`
 | 
			
		||||
	Bio              string         `json:"bio"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -114,7 +114,6 @@ type Querier interface {
 | 
			
		||||
	UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
 | 
			
		||||
	UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error)
 | 
			
		||||
	UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
 | 
			
		||||
	UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccountInfoParams) (UserAccount, error)
 | 
			
		||||
	UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
 | 
			
		||||
	UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,10 +15,6 @@ INSERT INTO user_account(full_name, initials, email, username, created_at, passw
 | 
			
		||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
 | 
			
		||||
  RETURNING *;
 | 
			
		||||
 | 
			
		||||
-- name: UpdateUserAccountInfo :one
 | 
			
		||||
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
 | 
			
		||||
  WHERE user_id = $1 RETURNING *;
 | 
			
		||||
 | 
			
		||||
-- name: DeleteUserAccountByID :exec
 | 
			
		||||
DELETE FROM user_account WHERE user_id = $1;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ import (
 | 
			
		||||
 | 
			
		||||
const createUserAccount = `-- name: CreateUserAccount :one
 | 
			
		||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
 | 
			
		||||
  VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
			
		||||
  VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
type CreateUserAccountParams struct {
 | 
			
		||||
@@ -48,7 +48,6 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
 | 
			
		||||
		&i.Initials,
 | 
			
		||||
		&i.ProfileAvatarUrl,
 | 
			
		||||
		&i.RoleCode,
 | 
			
		||||
		&i.Bio,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
@@ -63,7 +62,7 @@ func (q *Queries) DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) e
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
 | 
			
		||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username != 'system'
 | 
			
		||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE username != 'system'
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
 | 
			
		||||
@@ -86,7 +85,6 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
 | 
			
		||||
			&i.Initials,
 | 
			
		||||
			&i.ProfileAvatarUrl,
 | 
			
		||||
			&i.RoleCode,
 | 
			
		||||
			&i.Bio,
 | 
			
		||||
		); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -121,7 +119,7 @@ func (q *Queries) GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getUserAccountByID = `-- name: GetUserAccountByID :one
 | 
			
		||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE user_id = $1
 | 
			
		||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE user_id = $1
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
 | 
			
		||||
@@ -138,13 +136,12 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
 | 
			
		||||
		&i.Initials,
 | 
			
		||||
		&i.ProfileAvatarUrl,
 | 
			
		||||
		&i.RoleCode,
 | 
			
		||||
		&i.Bio,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
 | 
			
		||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username = $1
 | 
			
		||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE username = $1
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
 | 
			
		||||
@@ -161,13 +158,12 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
 | 
			
		||||
		&i.Initials,
 | 
			
		||||
		&i.ProfileAvatarUrl,
 | 
			
		||||
		&i.RoleCode,
 | 
			
		||||
		&i.Bio,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const setUserPassword = `-- name: SetUserPassword :one
 | 
			
		||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
			
		||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
type SetUserPasswordParams struct {
 | 
			
		||||
@@ -189,52 +185,13 @@ func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams
 | 
			
		||||
		&i.Initials,
 | 
			
		||||
		&i.ProfileAvatarUrl,
 | 
			
		||||
		&i.RoleCode,
 | 
			
		||||
		&i.Bio,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateUserAccountInfo = `-- name: UpdateUserAccountInfo :one
 | 
			
		||||
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
 | 
			
		||||
  WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
type UpdateUserAccountInfoParams struct {
 | 
			
		||||
	UserID   uuid.UUID `json:"user_id"`
 | 
			
		||||
	Bio      string    `json:"bio"`
 | 
			
		||||
	FullName string    `json:"full_name"`
 | 
			
		||||
	Initials string    `json:"initials"`
 | 
			
		||||
	Email    string    `json:"email"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (q *Queries) UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccountInfoParams) (UserAccount, error) {
 | 
			
		||||
	row := q.db.QueryRowContext(ctx, updateUserAccountInfo,
 | 
			
		||||
		arg.UserID,
 | 
			
		||||
		arg.Bio,
 | 
			
		||||
		arg.FullName,
 | 
			
		||||
		arg.Initials,
 | 
			
		||||
		arg.Email,
 | 
			
		||||
	)
 | 
			
		||||
	var i UserAccount
 | 
			
		||||
	err := row.Scan(
 | 
			
		||||
		&i.UserID,
 | 
			
		||||
		&i.CreatedAt,
 | 
			
		||||
		&i.Email,
 | 
			
		||||
		&i.Username,
 | 
			
		||||
		&i.PasswordHash,
 | 
			
		||||
		&i.ProfileBgColor,
 | 
			
		||||
		&i.FullName,
 | 
			
		||||
		&i.Initials,
 | 
			
		||||
		&i.ProfileAvatarUrl,
 | 
			
		||||
		&i.RoleCode,
 | 
			
		||||
		&i.Bio,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
 | 
			
		||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
 | 
			
		||||
  RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
			
		||||
  RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
type UpdateUserAccountProfileAvatarURLParams struct {
 | 
			
		||||
@@ -256,13 +213,12 @@ func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg Upd
 | 
			
		||||
		&i.Initials,
 | 
			
		||||
		&i.ProfileAvatarUrl,
 | 
			
		||||
		&i.RoleCode,
 | 
			
		||||
		&i.Bio,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateUserRole = `-- name: UpdateUserRole :one
 | 
			
		||||
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
			
		||||
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
type UpdateUserRoleParams struct {
 | 
			
		||||
@@ -284,7 +240,6 @@ func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams)
 | 
			
		||||
		&i.Initials,
 | 
			
		||||
		&i.ProfileAvatarUrl,
 | 
			
		||||
		&i.RoleCode,
 | 
			
		||||
		&i.Bio,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -210,7 +210,6 @@ type ComplexityRoot struct {
 | 
			
		||||
		UpdateTaskLocation              func(childComplexity int, input NewTaskLocation) int
 | 
			
		||||
		UpdateTaskName                  func(childComplexity int, input UpdateTaskName) int
 | 
			
		||||
		UpdateTeamMemberRole            func(childComplexity int, input UpdateTeamMemberRole) int
 | 
			
		||||
		UpdateUserInfo                  func(childComplexity int, input UpdateUserInfo) int
 | 
			
		||||
		UpdateUserPassword              func(childComplexity int, input UpdateUserPassword) int
 | 
			
		||||
		UpdateUserRole                  func(childComplexity int, input UpdateUserRole) int
 | 
			
		||||
	}
 | 
			
		||||
@@ -405,10 +404,6 @@ type ComplexityRoot struct {
 | 
			
		||||
		TeamID func(childComplexity int) int
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	UpdateUserInfoPayload struct {
 | 
			
		||||
		User func(childComplexity int) int
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	UpdateUserPasswordPayload struct {
 | 
			
		||||
		Ok   func(childComplexity int) int
 | 
			
		||||
		User func(childComplexity int) int
 | 
			
		||||
@@ -419,7 +414,6 @@ type ComplexityRoot struct {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	UserAccount struct {
 | 
			
		||||
		Bio         func(childComplexity int) int
 | 
			
		||||
		CreatedAt   func(childComplexity int) int
 | 
			
		||||
		Email       func(childComplexity int) int
 | 
			
		||||
		FullName    func(childComplexity int) int
 | 
			
		||||
@@ -488,7 +482,6 @@ type MutationResolver interface {
 | 
			
		||||
	ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error)
 | 
			
		||||
	UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error)
 | 
			
		||||
	UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error)
 | 
			
		||||
	UpdateUserInfo(ctx context.Context, input UpdateUserInfo) (*UpdateUserInfoPayload, error)
 | 
			
		||||
}
 | 
			
		||||
type NotificationResolver interface {
 | 
			
		||||
	ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error)
 | 
			
		||||
@@ -1500,18 +1493,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 | 
			
		||||
 | 
			
		||||
		return e.complexity.Mutation.UpdateTeamMemberRole(childComplexity, args["input"].(UpdateTeamMemberRole)), true
 | 
			
		||||
 | 
			
		||||
	case "Mutation.updateUserInfo":
 | 
			
		||||
		if e.complexity.Mutation.UpdateUserInfo == nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		args, err := ec.field_Mutation_updateUserInfo_args(context.TODO(), rawArgs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return e.complexity.Mutation.UpdateUserInfo(childComplexity, args["input"].(UpdateUserInfo)), true
 | 
			
		||||
 | 
			
		||||
	case "Mutation.updateUserPassword":
 | 
			
		||||
		if e.complexity.Mutation.UpdateUserPassword == nil {
 | 
			
		||||
			break
 | 
			
		||||
@@ -2303,13 +2284,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 | 
			
		||||
 | 
			
		||||
		return e.complexity.UpdateTeamMemberRolePayload.TeamID(childComplexity), true
 | 
			
		||||
 | 
			
		||||
	case "UpdateUserInfoPayload.user":
 | 
			
		||||
		if e.complexity.UpdateUserInfoPayload.User == nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return e.complexity.UpdateUserInfoPayload.User(childComplexity), true
 | 
			
		||||
 | 
			
		||||
	case "UpdateUserPasswordPayload.ok":
 | 
			
		||||
		if e.complexity.UpdateUserPasswordPayload.Ok == nil {
 | 
			
		||||
			break
 | 
			
		||||
@@ -2331,13 +2305,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 | 
			
		||||
 | 
			
		||||
		return e.complexity.UpdateUserRolePayload.User(childComplexity), true
 | 
			
		||||
 | 
			
		||||
	case "UserAccount.bio":
 | 
			
		||||
		if e.complexity.UserAccount.Bio == nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return e.complexity.UserAccount.Bio(childComplexity), true
 | 
			
		||||
 | 
			
		||||
	case "UserAccount.createdAt":
 | 
			
		||||
		if e.complexity.UserAccount.CreatedAt == nil {
 | 
			
		||||
			break
 | 
			
		||||
@@ -2552,7 +2519,6 @@ type UserAccount {
 | 
			
		||||
  createdAt: Time!
 | 
			
		||||
  fullName: String!
 | 
			
		||||
  initials: String!
 | 
			
		||||
  bio: String!
 | 
			
		||||
  role: Role!
 | 
			
		||||
  username: String!
 | 
			
		||||
  profileIcon: ProfileIcon!
 | 
			
		||||
@@ -3197,19 +3163,6 @@ extend type Mutation {
 | 
			
		||||
  updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
 | 
			
		||||
  updateUserRole(input: UpdateUserRole!):
 | 
			
		||||
   UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
  updateUserInfo(input: UpdateUserInfo!):
 | 
			
		||||
    UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateUserInfoPayload {
 | 
			
		||||
  user: UserAccount!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserInfo {
 | 
			
		||||
  name: String!
 | 
			
		||||
  initials: String!
 | 
			
		||||
  email: String!
 | 
			
		||||
  bio: String!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserPassword {
 | 
			
		||||
@@ -3968,20 +3921,6 @@ func (ec *executionContext) field_Mutation_updateTeamMemberRole_args(ctx context
 | 
			
		||||
	return args, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) field_Mutation_updateUserInfo_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	args := map[string]interface{}{}
 | 
			
		||||
	var arg0 UpdateUserInfo
 | 
			
		||||
	if tmp, ok := rawArgs["input"]; ok {
 | 
			
		||||
		arg0, err = ec.unmarshalNUpdateUserInfo2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfo(ctx, tmp)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	args["input"] = arg0
 | 
			
		||||
	return args, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) field_Mutation_updateUserPassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	args := map[string]interface{}{}
 | 
			
		||||
@@ -9282,79 +9221,6 @@ func (ec *executionContext) _Mutation_updateUserRole(ctx context.Context, field
 | 
			
		||||
	return ec.marshalNUpdateUserRolePayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserRolePayload(ctx, field.Selections, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _Mutation_updateUserInfo(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			ec.Error(ctx, ec.Recover(ctx, r))
 | 
			
		||||
			ret = graphql.Null
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	fc := &graphql.FieldContext{
 | 
			
		||||
		Object:   "Mutation",
 | 
			
		||||
		Field:    field,
 | 
			
		||||
		Args:     nil,
 | 
			
		||||
		IsMethod: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx = graphql.WithFieldContext(ctx, fc)
 | 
			
		||||
	rawArgs := field.ArgumentMap(ec.Variables)
 | 
			
		||||
	args, err := ec.field_Mutation_updateUserInfo_args(ctx, rawArgs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ec.Error(ctx, err)
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	fc.Args = args
 | 
			
		||||
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 | 
			
		||||
		directive0 := func(rctx context.Context) (interface{}, error) {
 | 
			
		||||
			ctx = rctx // use context from middleware stack in children
 | 
			
		||||
			return ec.resolvers.Mutation().UpdateUserInfo(rctx, args["input"].(UpdateUserInfo))
 | 
			
		||||
		}
 | 
			
		||||
		directive1 := func(ctx context.Context) (interface{}, error) {
 | 
			
		||||
			roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "ORG")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "ORG")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			if ec.directives.HasRole == nil {
 | 
			
		||||
				return nil, errors.New("directive hasRole is not implemented")
 | 
			
		||||
			}
 | 
			
		||||
			return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmp, err := directive1(rctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if tmp == nil {
 | 
			
		||||
			return nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if data, ok := tmp.(*UpdateUserInfoPayload); ok {
 | 
			
		||||
			return data, nil
 | 
			
		||||
		}
 | 
			
		||||
		return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateUserInfoPayload`, tmp)
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ec.Error(ctx, err)
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	if resTmp == nil {
 | 
			
		||||
		if !graphql.HasFieldError(ctx, fc) {
 | 
			
		||||
			ec.Errorf(ctx, "must not be null")
 | 
			
		||||
		}
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	res := resTmp.(*UpdateUserInfoPayload)
 | 
			
		||||
	fc.Result = res
 | 
			
		||||
	return ec.marshalNUpdateUserInfoPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfoPayload(ctx, field.Selections, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _Notification_id(ctx context.Context, field graphql.CollectedField, obj *db.Notification) (ret graphql.Marshaler) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
@@ -13039,40 +12905,6 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload_member(ctx context.Cont
 | 
			
		||||
	return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _UpdateUserInfoPayload_user(ctx context.Context, field graphql.CollectedField, obj *UpdateUserInfoPayload) (ret graphql.Marshaler) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			ec.Error(ctx, ec.Recover(ctx, r))
 | 
			
		||||
			ret = graphql.Null
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	fc := &graphql.FieldContext{
 | 
			
		||||
		Object:   "UpdateUserInfoPayload",
 | 
			
		||||
		Field:    field,
 | 
			
		||||
		Args:     nil,
 | 
			
		||||
		IsMethod: false,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx = graphql.WithFieldContext(ctx, fc)
 | 
			
		||||
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 | 
			
		||||
		ctx = rctx // use context from middleware stack in children
 | 
			
		||||
		return obj.User, nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ec.Error(ctx, err)
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	if resTmp == nil {
 | 
			
		||||
		if !graphql.HasFieldError(ctx, fc) {
 | 
			
		||||
			ec.Errorf(ctx, "must not be null")
 | 
			
		||||
		}
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	res := resTmp.(*db.UserAccount)
 | 
			
		||||
	fc.Result = res
 | 
			
		||||
	return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _UpdateUserPasswordPayload_ok(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
@@ -13345,40 +13177,6 @@ func (ec *executionContext) _UserAccount_initials(ctx context.Context, field gra
 | 
			
		||||
	return ec.marshalNString2string(ctx, field.Selections, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _UserAccount_bio(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			ec.Error(ctx, ec.Recover(ctx, r))
 | 
			
		||||
			ret = graphql.Null
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	fc := &graphql.FieldContext{
 | 
			
		||||
		Object:   "UserAccount",
 | 
			
		||||
		Field:    field,
 | 
			
		||||
		Args:     nil,
 | 
			
		||||
		IsMethod: false,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx = graphql.WithFieldContext(ctx, fc)
 | 
			
		||||
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 | 
			
		||||
		ctx = rctx // use context from middleware stack in children
 | 
			
		||||
		return obj.Bio, nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ec.Error(ctx, err)
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	if resTmp == nil {
 | 
			
		||||
		if !graphql.HasFieldError(ctx, fc) {
 | 
			
		||||
			ec.Errorf(ctx, "must not be null")
 | 
			
		||||
		}
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	res := resTmp.(string)
 | 
			
		||||
	fc.Result = res
 | 
			
		||||
	return ec.marshalNString2string(ctx, field.Selections, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _UserAccount_role(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
@@ -15912,42 +15710,6 @@ func (ec *executionContext) unmarshalInputUpdateTeamMemberRole(ctx context.Conte
 | 
			
		||||
	return it, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) unmarshalInputUpdateUserInfo(ctx context.Context, obj interface{}) (UpdateUserInfo, error) {
 | 
			
		||||
	var it UpdateUserInfo
 | 
			
		||||
	var asMap = obj.(map[string]interface{})
 | 
			
		||||
 | 
			
		||||
	for k, v := range asMap {
 | 
			
		||||
		switch k {
 | 
			
		||||
		case "name":
 | 
			
		||||
			var err error
 | 
			
		||||
			it.Name, err = ec.unmarshalNString2string(ctx, v)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return it, err
 | 
			
		||||
			}
 | 
			
		||||
		case "initials":
 | 
			
		||||
			var err error
 | 
			
		||||
			it.Initials, err = ec.unmarshalNString2string(ctx, v)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return it, err
 | 
			
		||||
			}
 | 
			
		||||
		case "email":
 | 
			
		||||
			var err error
 | 
			
		||||
			it.Email, err = ec.unmarshalNString2string(ctx, v)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return it, err
 | 
			
		||||
			}
 | 
			
		||||
		case "bio":
 | 
			
		||||
			var err error
 | 
			
		||||
			it.Bio, err = ec.unmarshalNString2string(ctx, v)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return it, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return it, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) unmarshalInputUpdateUserPassword(ctx context.Context, obj interface{}) (UpdateUserPassword, error) {
 | 
			
		||||
	var it UpdateUserPassword
 | 
			
		||||
	var asMap = obj.(map[string]interface{})
 | 
			
		||||
@@ -16909,11 +16671,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 | 
			
		||||
			if out.Values[i] == graphql.Null {
 | 
			
		||||
				invalids++
 | 
			
		||||
			}
 | 
			
		||||
		case "updateUserInfo":
 | 
			
		||||
			out.Values[i] = ec._Mutation_updateUserInfo(ctx, field)
 | 
			
		||||
			if out.Values[i] == graphql.Null {
 | 
			
		||||
				invalids++
 | 
			
		||||
			}
 | 
			
		||||
		default:
 | 
			
		||||
			panic("unknown field " + strconv.Quote(field.Name))
 | 
			
		||||
		}
 | 
			
		||||
@@ -18478,33 +18235,6 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload(ctx context.Context, se
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var updateUserInfoPayloadImplementors = []string{"UpdateUserInfoPayload"}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _UpdateUserInfoPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserInfoPayload) graphql.Marshaler {
 | 
			
		||||
	fields := graphql.CollectFields(ec.OperationContext, sel, updateUserInfoPayloadImplementors)
 | 
			
		||||
 | 
			
		||||
	out := graphql.NewFieldSet(fields)
 | 
			
		||||
	var invalids uint32
 | 
			
		||||
	for i, field := range fields {
 | 
			
		||||
		switch field.Name {
 | 
			
		||||
		case "__typename":
 | 
			
		||||
			out.Values[i] = graphql.MarshalString("UpdateUserInfoPayload")
 | 
			
		||||
		case "user":
 | 
			
		||||
			out.Values[i] = ec._UpdateUserInfoPayload_user(ctx, field, obj)
 | 
			
		||||
			if out.Values[i] == graphql.Null {
 | 
			
		||||
				invalids++
 | 
			
		||||
			}
 | 
			
		||||
		default:
 | 
			
		||||
			panic("unknown field " + strconv.Quote(field.Name))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	out.Dispatch()
 | 
			
		||||
	if invalids > 0 {
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var updateUserPasswordPayloadImplementors = []string{"UpdateUserPasswordPayload"}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) _UpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserPasswordPayload) graphql.Marshaler {
 | 
			
		||||
@@ -18609,11 +18339,6 @@ func (ec *executionContext) _UserAccount(ctx context.Context, sel ast.SelectionS
 | 
			
		||||
			if out.Values[i] == graphql.Null {
 | 
			
		||||
				atomic.AddUint32(&invalids, 1)
 | 
			
		||||
			}
 | 
			
		||||
		case "bio":
 | 
			
		||||
			out.Values[i] = ec._UserAccount_bio(ctx, field, obj)
 | 
			
		||||
			if out.Values[i] == graphql.Null {
 | 
			
		||||
				atomic.AddUint32(&invalids, 1)
 | 
			
		||||
			}
 | 
			
		||||
		case "role":
 | 
			
		||||
			field := field
 | 
			
		||||
			out.Concurrently(i, func() (res graphql.Marshaler) {
 | 
			
		||||
@@ -20478,24 +20203,6 @@ func (ec *executionContext) marshalNUpdateTeamMemberRolePayload2ᚖgithubᚗcom
 | 
			
		||||
	return ec._UpdateTeamMemberRolePayload(ctx, sel, v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) unmarshalNUpdateUserInfo2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfo(ctx context.Context, v interface{}) (UpdateUserInfo, error) {
 | 
			
		||||
	return ec.unmarshalInputUpdateUserInfo(ctx, v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) marshalNUpdateUserInfoPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfoPayload(ctx context.Context, sel ast.SelectionSet, v UpdateUserInfoPayload) graphql.Marshaler {
 | 
			
		||||
	return ec._UpdateUserInfoPayload(ctx, sel, &v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) marshalNUpdateUserInfoPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfoPayload(ctx context.Context, sel ast.SelectionSet, v *UpdateUserInfoPayload) graphql.Marshaler {
 | 
			
		||||
	if v == nil {
 | 
			
		||||
		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
 | 
			
		||||
			ec.Errorf(ctx, "must not be null")
 | 
			
		||||
		}
 | 
			
		||||
		return graphql.Null
 | 
			
		||||
	}
 | 
			
		||||
	return ec._UpdateUserInfoPayload(ctx, sel, v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ec *executionContext) unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserPassword(ctx context.Context, v interface{}) (UpdateUserPassword, error) {
 | 
			
		||||
	return ec.unmarshalInputUpdateUserPassword(ctx, v)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -455,17 +455,6 @@ type UpdateTeamMemberRolePayload struct {
 | 
			
		||||
	Member *Member   `json:"member"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateUserInfo struct {
 | 
			
		||||
	Name     string `json:"name"`
 | 
			
		||||
	Initials string `json:"initials"`
 | 
			
		||||
	Email    string `json:"email"`
 | 
			
		||||
	Bio      string `json:"bio"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateUserInfoPayload struct {
 | 
			
		||||
	User *db.UserAccount `json:"user"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateUserPassword struct {
 | 
			
		||||
	UserID   uuid.UUID `json:"userID"`
 | 
			
		||||
	Password string    `json:"password"`
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,6 @@ type UserAccount {
 | 
			
		||||
  createdAt: Time!
 | 
			
		||||
  fullName: String!
 | 
			
		||||
  initials: String!
 | 
			
		||||
  bio: String!
 | 
			
		||||
  role: Role!
 | 
			
		||||
  username: String!
 | 
			
		||||
  profileIcon: ProfileIcon!
 | 
			
		||||
@@ -723,19 +722,6 @@ extend type Mutation {
 | 
			
		||||
  updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
 | 
			
		||||
  updateUserRole(input: UpdateUserRole!):
 | 
			
		||||
   UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
  updateUserInfo(input: UpdateUserInfo!):
 | 
			
		||||
    UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateUserInfoPayload {
 | 
			
		||||
  user: UserAccount!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserInfo {
 | 
			
		||||
  name: String!
 | 
			
		||||
  initials: String!
 | 
			
		||||
  email: String!
 | 
			
		||||
  bio: String!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserPassword {
 | 
			
		||||
 
 | 
			
		||||
@@ -826,17 +826,6 @@ func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserR
 | 
			
		||||
	return &UpdateUserRolePayload{User: &user}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) UpdateUserInfo(ctx context.Context, input UpdateUserInfo) (*UpdateUserInfoPayload, error) {
 | 
			
		||||
	userID, ok := GetUserID(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return &UpdateUserInfoPayload{}, errors.New("invalid user ID")
 | 
			
		||||
	}
 | 
			
		||||
	user, err := r.Repository.UpdateUserAccountInfo(ctx, db.UpdateUserAccountInfoParams{
 | 
			
		||||
		Bio: input.Bio, FullName: input.Name, Initials: input.Initials, Email: input.Email, UserID: userID,
 | 
			
		||||
	})
 | 
			
		||||
	return &UpdateUserInfoPayload{User: &user}, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
 | 
			
		||||
	return obj.NotificationID, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,6 @@ type UserAccount {
 | 
			
		||||
  createdAt: Time!
 | 
			
		||||
  fullName: String!
 | 
			
		||||
  initials: String!
 | 
			
		||||
  bio: String!
 | 
			
		||||
  role: Role!
 | 
			
		||||
  username: String!
 | 
			
		||||
  profileIcon: ProfileIcon!
 | 
			
		||||
 
 | 
			
		||||
@@ -11,19 +11,6 @@ extend type Mutation {
 | 
			
		||||
  updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
 | 
			
		||||
  updateUserRole(input: UpdateUserRole!):
 | 
			
		||||
   UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
  updateUserInfo(input: UpdateUserInfo!):
 | 
			
		||||
    UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateUserInfoPayload {
 | 
			
		||||
  user: UserAccount!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserInfo {
 | 
			
		||||
  name: String!
 | 
			
		||||
  initials: String!
 | 
			
		||||
  email: String!
 | 
			
		||||
  bio: String!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserPassword {
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -16,7 +14,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/db"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/frontend"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Frontend serves the index.html file
 | 
			
		||||
@@ -33,7 +30,7 @@ func (h *TaskcafeHandler) Frontend(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// ProfileImageUpload handles a user uploading a new avatar profile image
 | 
			
		||||
func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	log.Info("preparing to upload file")
 | 
			
		||||
	userID, ok := r.Context().Value(utils.UserIDKey).(uuid.UUID)
 | 
			
		||||
	userID, ok := r.Context().Value("userID").(uuid.UUID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Error("not a valid uuid")
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
@@ -50,24 +47,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))
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
ALTER TABLE user_account ADD COLUMN bio text NOT NULL DEFAULT '';
 | 
			
		||||
		Reference in New Issue
	
	Block a user