Compare commits
20 Commits
feat/list-
...
0.2.2
Author | SHA1 | Date | |
---|---|---|---|
c7538a98e5 | |||
fe84f97f18 | |||
52c60abcd7 | |||
9fdb3008db | |||
e2ef8a1a19 | |||
61cd376bfd | |||
ba9fc64fd9 | |||
03dafe9b7b | |||
12a767947a | |||
40557ba79f | |||
e4d1e21304 | |||
f7c6ee470e | |||
227ce5966d | |||
aa5e1c0661 | |||
b603081691 | |||
e76ea9da63 | |||
923d7f7372 | |||
009d717d80 | |||
4272fefa28 | |||
25f5cad557 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -18,6 +18,8 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
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!
|
Please read the contributing guide before working on any new pull requests!
|
||||||
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
- Task sorting & filtering
|
- Task sorting & filtering
|
||||||
- Redesigned the Task Details UI
|
- Redesigned the Task Details UI
|
||||||
|
- Implement task group actions (duplicate/delete all tasks/sort)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- removed CORS middleware to fix security issue
|
- removed CORS middleware to fix security issue
|
||||||
|
@ -21,6 +21,8 @@ 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**
|
**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
|
## Features
|
||||||
|
|
||||||
Currently Taskcafe only offers basic task tracking through a Kanban board.
|
Currently Taskcafe only offers basic task tracking through a Kanban board.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[general]
|
[server]
|
||||||
host = '0.0.0.0:3333'
|
hostname = '0.0.0.0:3333'
|
||||||
|
|
||||||
[email_notifications]
|
[email_notifications]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -6,14 +6,6 @@
|
|||||||
"@apollo/client": "^3.0.0-rc.8",
|
"@apollo/client": "^3.0.0-rc.8",
|
||||||
"@apollo/react-common": "^3.1.4",
|
"@apollo/react-common": "^3.1.4",
|
||||||
"@apollo/react-hooks": "^3.1.3",
|
"@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/axios": "^0.14.0",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/date-fns": "^2.6.0",
|
"@types/date-fns": "^2.6.0",
|
||||||
|
@ -171,7 +171,7 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
|
|||||||
|
|
||||||
const AdminRoute = () => {
|
const AdminRoute = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Taskcafé | Admin';
|
document.title = 'Admin | Taskcafé';
|
||||||
}, []);
|
}, []);
|
||||||
const { loading, data } = useUsersQuery();
|
const { loading, data } = useUsersQuery();
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
|
@ -59,7 +59,7 @@ const Install = () => {
|
|||||||
} else {
|
} else {
|
||||||
const response: RefreshTokenResponse = await x.data;
|
const response: RefreshTokenResponse = await x.data;
|
||||||
const { accessToken: newToken, isInstalled } = response;
|
const { accessToken: newToken, isInstalled } = response;
|
||||||
const claims: JWTToken = jwtDecode(accessToken);
|
const claims: JWTToken = jwtDecode(newToken);
|
||||||
const currentUser = {
|
const currentUser = {
|
||||||
id: claims.userId,
|
id: claims.userId,
|
||||||
roles: {
|
roles: {
|
||||||
@ -69,7 +69,7 @@ const Install = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
setUser(currentUser);
|
setUser(currentUser);
|
||||||
setAccessToken(accessToken);
|
setAccessToken(newToken);
|
||||||
if (!isInstalled) {
|
if (!isInstalled) {
|
||||||
history.replace('/install');
|
history.replace('/install');
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,20 @@ import styled from 'styled-components/macro';
|
|||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
import { getAccessToken } from 'shared/utils/accessToken';
|
import { getAccessToken } from 'shared/utils/accessToken';
|
||||||
import Settings from 'shared/components/Settings';
|
import Settings from 'shared/components/Settings';
|
||||||
import { useMeQuery, useClearProfileAvatarMutation, useUpdateUserPasswordMutation } from 'shared/generated/graphql';
|
import {
|
||||||
|
useMeQuery,
|
||||||
|
useClearProfileAvatarMutation,
|
||||||
|
useUpdateUserPasswordMutation,
|
||||||
|
useUpdateUserInfoMutation,
|
||||||
|
MeQuery,
|
||||||
|
MeDocument,
|
||||||
|
} from 'shared/generated/graphql';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
|
import produce from 'immer';
|
||||||
|
|
||||||
const MainContent = styled.div`
|
const MainContent = styled.div`
|
||||||
padding: 0 0 50px 80px;
|
padding: 0 0 50px 80px;
|
||||||
@ -19,6 +28,7 @@ const Projects = () => {
|
|||||||
const $fileUpload = useRef<HTMLInputElement>(null);
|
const $fileUpload = useRef<HTMLInputElement>(null);
|
||||||
const [clearProfileAvatar] = useClearProfileAvatarMutation();
|
const [clearProfileAvatar] = useClearProfileAvatarMutation();
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
const [updateUserInfo] = useUpdateUserInfoMutation();
|
||||||
const [updateUserPassword] = useUpdateUserPasswordMutation();
|
const [updateUserPassword] = useUpdateUserPasswordMutation();
|
||||||
const { loading, data, refetch } = useMeQuery();
|
const { loading, data, refetch } = useMeQuery();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -69,6 +79,13 @@ const Projects = () => {
|
|||||||
toast('Password was changed!');
|
toast('Password was changed!');
|
||||||
done();
|
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={() => {
|
onProfileAvatarRemove={() => {
|
||||||
clearProfileAvatar();
|
clearProfileAvatar();
|
||||||
}}
|
}}
|
||||||
|
@ -8,6 +8,8 @@ import {
|
|||||||
useCreateProjectMutation,
|
useCreateProjectMutation,
|
||||||
GetProjectsDocument,
|
GetProjectsDocument,
|
||||||
GetProjectsQuery,
|
GetProjectsQuery,
|
||||||
|
MeQuery,
|
||||||
|
MeDocument,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -260,11 +262,7 @@ const Projects = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
|
||||||
<>
|
|
||||||
<span>loading</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
||||||
|
@ -85,18 +85,30 @@ type TeamsRouteProps = {
|
|||||||
const Teams = () => {
|
const Teams = () => {
|
||||||
const { teamID } = useParams<TeamsRouteProps>();
|
const { teamID } = useParams<TeamsRouteProps>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
const { loading, data } = useGetTeamQuery({
|
||||||
|
variables: { teamID },
|
||||||
|
onCompleted: resp => {
|
||||||
|
document.title = `${resp.findTeam.name} | Taskcafé`;
|
||||||
|
},
|
||||||
|
});
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
useEffect(() => {
|
|
||||||
document.title = 'Teams | Taskcafé';
|
|
||||||
}, []);
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<>
|
<GlobalTopNavbar
|
||||||
<span>loading</span>
|
menuType={[
|
||||||
</>
|
{ name: 'Projects', link: `${match.url}` },
|
||||||
|
{ name: 'Members', link: `${match.url}/members` },
|
||||||
|
]}
|
||||||
|
currentTab={currentTab}
|
||||||
|
onSetTab={tab => {
|
||||||
|
setCurrentTab(tab);
|
||||||
|
}}
|
||||||
|
onSaveProjectName={NOOP}
|
||||||
|
projectID={null}
|
||||||
|
name={null}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
|
@ -557,6 +557,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<TabNavContent>
|
<TabNavContent>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
|
key={item.name}
|
||||||
onClick={(tab, top) => {
|
onClick={(tab, top) => {
|
||||||
if ($tabNav && $tabNav.current) {
|
if ($tabNav && $tabNav.current) {
|
||||||
const pos = $tabNav.current.getBoundingClientRect();
|
const pos = $tabNav.current.getBoundingClientRect();
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import styled, { css, keyframes } from 'styled-components';
|
import styled, { css, keyframes } from 'styled-components';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
import TextareaAutosize from 'react-autosize-textarea';
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
import { CheckCircle, CheckSquareOutline } from 'shared/icons';
|
import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
|
||||||
import { RefObject } from 'react';
|
|
||||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
|
|
||||||
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
|
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
|
||||||
@ -20,7 +18,9 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
|
|||||||
stroke: rgba(${props.theme.colors.success});
|
stroke: rgba(${props.theme.colors.success});
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
export const ClockIcon = styled(FontAwesomeIcon)``;
|
export const ClockIcon = styled(Clock)<{ color: string }>`
|
||||||
|
fill: ${props => props.color};
|
||||||
|
`;
|
||||||
|
|
||||||
export const EditorTextarea = styled(TextareaAutosize)`
|
export const EditorTextarea = styled(TextareaAutosize)`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -147,6 +147,11 @@ export const ListCardLabelText = styled.span`
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ListCardLabelsWrapper = styled.div`
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
||||||
${props =>
|
${props =>
|
||||||
props.variant === 'small'
|
props.variant === 'small'
|
||||||
@ -178,8 +183,6 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { Pencil, Eye, List } from 'shared/icons';
|
||||||
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { faClock, faEye } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import {
|
import {
|
||||||
EditorTextarea,
|
EditorTextarea,
|
||||||
CardMember,
|
CardMember,
|
||||||
@ -20,6 +18,7 @@ import {
|
|||||||
ListCardLabels,
|
ListCardLabels,
|
||||||
ListCardLabel,
|
ListCardLabel,
|
||||||
ListCardLabelText,
|
ListCardLabelText,
|
||||||
|
ListCardLabelsWrapper,
|
||||||
ListCardOperation,
|
ListCardOperation,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardMembers,
|
CardMembers,
|
||||||
@ -154,39 +153,42 @@ const Card = React.forwardRef(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
|
<Pencil width={8} height={8} />
|
||||||
</ListCardOperation>
|
</ListCardOperation>
|
||||||
)}
|
)}
|
||||||
<ListCardDetails complete={complete ?? false}>
|
<ListCardDetails complete={complete ?? false}>
|
||||||
<ListCardLabels
|
{labels && labels.length !== 0 && (
|
||||||
toggleLabels={toggleLabels}
|
<ListCardLabelsWrapper>
|
||||||
toggleDirection={toggleDirection}
|
<ListCardLabels
|
||||||
onClick={e => {
|
toggleLabels={toggleLabels}
|
||||||
e.stopPropagation();
|
toggleDirection={toggleDirection}
|
||||||
if (onCardLabelClick) {
|
onClick={e => {
|
||||||
onCardLabelClick();
|
e.stopPropagation();
|
||||||
}
|
if (onCardLabelClick) {
|
||||||
}}
|
onCardLabelClick();
|
||||||
>
|
}
|
||||||
{labels &&
|
}}
|
||||||
labels
|
>
|
||||||
.slice()
|
{labels
|
||||||
.sort((a, b) => a.labelColor.position - b.labelColor.position)
|
.slice()
|
||||||
.map(label => (
|
.sort((a, b) => a.labelColor.position - b.labelColor.position)
|
||||||
<ListCardLabel
|
.map(label => (
|
||||||
onAnimationEnd={() => {
|
<ListCardLabel
|
||||||
if (setToggleLabels) {
|
onAnimationEnd={() => {
|
||||||
setToggleLabels(false);
|
if (setToggleLabels) {
|
||||||
}
|
setToggleLabels(false);
|
||||||
}}
|
}
|
||||||
variant={labelVariant ?? 'large'}
|
}}
|
||||||
color={label.labelColor.colorHex}
|
variant={labelVariant ?? 'large'}
|
||||||
key={label.id}
|
color={label.labelColor.colorHex}
|
||||||
>
|
key={label.id}
|
||||||
<ListCardLabelText>{label.name}</ListCardLabelText>
|
>
|
||||||
</ListCardLabel>
|
<ListCardLabelText>{label.name}</ListCardLabelText>
|
||||||
))}
|
</ListCardLabel>
|
||||||
</ListCardLabels>
|
))}
|
||||||
|
</ListCardLabels>
|
||||||
|
</ListCardLabelsWrapper>
|
||||||
|
)}
|
||||||
{editable ? (
|
{editable ? (
|
||||||
<EditorContent>
|
<EditorContent>
|
||||||
{complete && <CompleteIcon width={16} height={16} />}
|
{complete && <CompleteIcon width={16} height={16} />}
|
||||||
@ -214,18 +216,18 @@ const Card = React.forwardRef(
|
|||||||
<ListCardBadges>
|
<ListCardBadges>
|
||||||
{watched && (
|
{watched && (
|
||||||
<ListCardBadge>
|
<ListCardBadge>
|
||||||
<FontAwesomeIcon color="#6b778c" icon={faEye} size="xs" />
|
<Eye width={8} height={8} />
|
||||||
</ListCardBadge>
|
</ListCardBadge>
|
||||||
)}
|
)}
|
||||||
{dueDate && (
|
{dueDate && (
|
||||||
<DueDateCardBadge isPastDue={dueDate.isPastDue}>
|
<DueDateCardBadge isPastDue={dueDate.isPastDue}>
|
||||||
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} icon={faClock} size="xs" />
|
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} width={8} height={8} />
|
||||||
<ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
|
<ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
|
||||||
</DueDateCardBadge>
|
</DueDateCardBadge>
|
||||||
)}
|
)}
|
||||||
{description && (
|
{description && (
|
||||||
<DescriptionBadge>
|
<DescriptionBadge>
|
||||||
<FontAwesomeIcon color="#6b778c" icon={faList} size="xs" />
|
<List width={8} height={8} />
|
||||||
</DescriptionBadge>
|
</DescriptionBadge>
|
||||||
)}
|
)}
|
||||||
{checklists && (
|
{checklists && (
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import TextareaAutosize from 'react-autosize-textarea';
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const CancelIcon = styled(FontAwesomeIcon)`
|
export const CancelIconWrapper = styled.div`
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
|
export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
display: ${props => (props.isOpen ? 'flex' : 'none')};
|
display: ${props => (props.isOpen ? 'flex' : 'none')};
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
||||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
|
import { Cross } from 'shared/icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CardComposerWrapper,
|
CardComposerWrapper,
|
||||||
CancelIcon,
|
CancelIconWrapper,
|
||||||
AddCardButton,
|
AddCardButton,
|
||||||
ComposerControls,
|
ComposerControls,
|
||||||
ComposerControlsSaveSection,
|
ComposerControlsSaveSection,
|
||||||
@ -52,7 +52,9 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
|
|||||||
>
|
>
|
||||||
Add Card
|
Add Card
|
||||||
</AddCardButton>
|
</AddCardButton>
|
||||||
<CancelIcon onClick={onClose} icon={faTimes} color="#42526e" />
|
<CancelIconWrapper onClick={() => onClose()}>
|
||||||
|
<Cross width={12} height={12} />
|
||||||
|
</CancelIconWrapper>
|
||||||
</ComposerControlsSaveSection>
|
</ComposerControlsSaveSection>
|
||||||
<ComposerControlsActionsSection />
|
<ComposerControlsActionsSection />
|
||||||
</ComposerControls>
|
</ComposerControls>
|
||||||
|
@ -78,6 +78,7 @@ const Icon = styled.div`
|
|||||||
|
|
||||||
type InputProps = {
|
type InputProps = {
|
||||||
variant?: 'normal' | 'alternate';
|
variant?: 'normal' | 'alternate';
|
||||||
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
floatingLabel?: boolean;
|
floatingLabel?: boolean;
|
||||||
@ -116,6 +117,7 @@ function useCombinedRefs(...refs: any) {
|
|||||||
const Input = React.forwardRef(
|
const Input = React.forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
disabled = false,
|
||||||
width = 'auto',
|
width = 'auto',
|
||||||
variant = 'normal',
|
variant = 'normal',
|
||||||
type = 'text',
|
type = 'text',
|
||||||
@ -160,6 +162,7 @@ const Input = React.forwardRef(
|
|||||||
onChange={e => {
|
onChange={e => {
|
||||||
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
|
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
hasValue={hasValue}
|
hasValue={hasValue}
|
||||||
ref={combinedRef}
|
ref={combinedRef}
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -27,6 +27,7 @@ export const Default = () => {
|
|||||||
<BaseStyles />
|
<BaseStyles />
|
||||||
<Settings
|
<Settings
|
||||||
profile={profile}
|
profile={profile}
|
||||||
|
onChangeUserInfo={action('change user info')}
|
||||||
onResetPassword={action('reset password')}
|
onResetPassword={action('reset password')}
|
||||||
onProfileAvatarRemove={action('remove')}
|
onProfileAvatarRemove={action('remove')}
|
||||||
onProfileAvatarChange={action('profile avatar change')}
|
onProfileAvatarChange={action('profile avatar change')}
|
||||||
|
@ -10,6 +10,11 @@ const PasswordInput = styled(Input)`
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const UserInfoInput = styled(Input)`
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
const FormError = styled.span`
|
const FormError = styled.span`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(${props => props.theme.colors.warning});
|
color: rgba(${props => props.theme.colors.warning});
|
||||||
@ -240,6 +245,7 @@ const SaveButton = styled(Button)`
|
|||||||
type SettingsProps = {
|
type SettingsProps = {
|
||||||
onProfileAvatarChange: () => void;
|
onProfileAvatarChange: () => void;
|
||||||
onProfileAvatarRemove: () => void;
|
onProfileAvatarRemove: () => void;
|
||||||
|
onChangeUserInfo: (data: UserInfoData, done: () => void) => void;
|
||||||
onResetPassword: (password: string, done: () => void) => void;
|
onResetPassword: (password: string, done: () => void) => void;
|
||||||
profile: TaskUser;
|
profile: TaskUser;
|
||||||
};
|
};
|
||||||
@ -300,9 +306,93 @@ 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> = ({
|
const Settings: React.FC<SettingsProps> = ({
|
||||||
onProfileAvatarRemove,
|
onProfileAvatarRemove,
|
||||||
onProfileAvatarChange,
|
onProfileAvatarChange,
|
||||||
|
onChangeUserInfo,
|
||||||
onResetPassword,
|
onResetPassword,
|
||||||
profile,
|
profile,
|
||||||
}) => {
|
}) => {
|
||||||
@ -315,6 +405,7 @@ const Settings: React.FC<SettingsProps> = ({
|
|||||||
<TabNavContent>
|
<TabNavContent>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
|
key={item.name}
|
||||||
onClick={(tab, top) => {
|
onClick={(tab, top) => {
|
||||||
if ($tabNav && $tabNav.current) {
|
if ($tabNav && $tabNav.current) {
|
||||||
const pos = $tabNav.current.getBoundingClientRect();
|
const pos = $tabNav.current.getBoundingClientRect();
|
||||||
@ -332,23 +423,12 @@ const Settings: React.FC<SettingsProps> = ({
|
|||||||
</TabNav>
|
</TabNav>
|
||||||
<TabContentWrapper>
|
<TabContentWrapper>
|
||||||
<Tab tab={0} currentTab={currentTab}>
|
<Tab tab={0} currentTab={currentTab}>
|
||||||
<AvatarSettings
|
<UserInfoTab
|
||||||
onProfileAvatarRemove={onProfileAvatarRemove}
|
|
||||||
onProfileAvatarChange={onProfileAvatarChange}
|
onProfileAvatarChange={onProfileAvatarChange}
|
||||||
profile={profile.profileIcon}
|
onProfileAvatarRemove={onProfileAvatarRemove}
|
||||||
|
profile={profile}
|
||||||
|
onChangeUserInfo={onChangeUserInfo}
|
||||||
/>
|
/>
|
||||||
<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 tab={1} currentTab={currentTab}>
|
<Tab tab={1} currentTab={currentTab}>
|
||||||
<ResetPasswordTab onResetPassword={onResetPassword} />
|
<ResetPasswordTab onResetPassword={onResetPassword} />
|
||||||
|
@ -585,3 +585,30 @@ export const ActivityItemLog = styled.span`
|
|||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
color: rgba(${props => props.theme.colors.text.primary});
|
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,6 +30,7 @@ import {
|
|||||||
AssignUserLabel,
|
AssignUserLabel,
|
||||||
AssignUsersButton,
|
AssignUsersButton,
|
||||||
AssignedUsersSection,
|
AssignedUsersSection,
|
||||||
|
ViewRawButton,
|
||||||
DueDateTitle,
|
DueDateTitle,
|
||||||
Container,
|
Container,
|
||||||
LeftSidebar,
|
LeftSidebar,
|
||||||
@ -65,6 +66,7 @@ import {
|
|||||||
CommentProfile,
|
CommentProfile,
|
||||||
CommentInnerWrapper,
|
CommentInnerWrapper,
|
||||||
ActivitySection,
|
ActivitySection,
|
||||||
|
TaskDetailsEditor,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
||||||
import onDragEnd from './onDragEnd';
|
import onDragEnd from './onDragEnd';
|
||||||
@ -153,6 +155,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const [saveTimeout, setSaveTimeout] = useState<any>(null);
|
const [saveTimeout, setSaveTimeout] = useState<any>(null);
|
||||||
|
const [showRaw, setShowRaw] = useState(false);
|
||||||
const [showCommentActions, setShowCommentActions] = useState(false);
|
const [showCommentActions, setShowCommentActions] = useState(false);
|
||||||
const taskDescriptionRef = useRef(task.description ?? '');
|
const taskDescriptionRef = useRef(task.description ?? '');
|
||||||
const $noMemberBtn = useRef<HTMLDivElement>(null);
|
const $noMemberBtn = useRef<HTMLDivElement>(null);
|
||||||
@ -169,7 +172,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<LeftSidebarSection>
|
<LeftSidebarSection>
|
||||||
<SidebarTitle>TASK GROUP</SidebarTitle>
|
<SidebarTitle>TASK GROUP</SidebarTitle>
|
||||||
<SidebarButton>
|
<SidebarButton>
|
||||||
<SidebarButtonText>Release 0.1.0</SidebarButtonText>
|
<SidebarButtonText>{task.taskGroup.name}</SidebarButtonText>
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
<DueDateTitle>DUE DATE</DueDateTitle>
|
<DueDateTitle>DUE DATE</DueDateTitle>
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
@ -309,28 +312,34 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
<InnerContentContainer>
|
<InnerContentContainer>
|
||||||
<DescriptionContainer>
|
<DescriptionContainer>
|
||||||
<EditorContainer
|
{showRaw ? (
|
||||||
onClick={e => {
|
<TaskDetailsEditor value={taskDescriptionRef.current} />
|
||||||
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;
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</EditorContainer>
|
<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>
|
||||||
</DescriptionContainer>
|
</DescriptionContainer>
|
||||||
<ChecklistSection>
|
<ChecklistSection>
|
||||||
<DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>
|
<DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>
|
||||||
|
@ -251,8 +251,8 @@ export const NavSeparator = styled.div`
|
|||||||
export const LogoContainer = styled(Link)`
|
export const LogoContainer = styled(Link)`
|
||||||
display: block;
|
display: block;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
right: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -105,6 +105,7 @@ export type UserAccount = {
|
|||||||
createdAt: Scalars['Time'];
|
createdAt: Scalars['Time'];
|
||||||
fullName: Scalars['String'];
|
fullName: Scalars['String'];
|
||||||
initials: Scalars['String'];
|
initials: Scalars['String'];
|
||||||
|
bio: Scalars['String'];
|
||||||
role: Role;
|
role: Role;
|
||||||
username: Scalars['String'];
|
username: Scalars['String'];
|
||||||
profileIcon: ProfileIcon;
|
profileIcon: ProfileIcon;
|
||||||
@ -303,6 +304,7 @@ export type Mutation = {
|
|||||||
updateTaskLocation: UpdateTaskLocationPayload;
|
updateTaskLocation: UpdateTaskLocationPayload;
|
||||||
updateTaskName: Task;
|
updateTaskName: Task;
|
||||||
updateTeamMemberRole: UpdateTeamMemberRolePayload;
|
updateTeamMemberRole: UpdateTeamMemberRolePayload;
|
||||||
|
updateUserInfo: UpdateUserInfoPayload;
|
||||||
updateUserPassword: UpdateUserPasswordPayload;
|
updateUserPassword: UpdateUserPasswordPayload;
|
||||||
updateUserRole: UpdateUserRolePayload;
|
updateUserRole: UpdateUserRolePayload;
|
||||||
};
|
};
|
||||||
@ -548,6 +550,11 @@ export type MutationUpdateTeamMemberRoleArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateUserInfoArgs = {
|
||||||
|
input: UpdateUserInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateUserPasswordArgs = {
|
export type MutationUpdateUserPasswordArgs = {
|
||||||
input: UpdateUserPassword;
|
input: UpdateUserPassword;
|
||||||
};
|
};
|
||||||
@ -979,6 +986,18 @@ export type UpdateTeamMemberRolePayload = {
|
|||||||
member: Member;
|
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 = {
|
export type UpdateUserPassword = {
|
||||||
userID: Scalars['UUID'];
|
userID: Scalars['UUID'];
|
||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
@ -1245,7 +1264,7 @@ export type FindTaskQuery = (
|
|||||||
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'>
|
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'>
|
||||||
& { taskGroup: (
|
& { taskGroup: (
|
||||||
{ __typename?: 'TaskGroup' }
|
{ __typename?: 'TaskGroup' }
|
||||||
& Pick<TaskGroup, 'id'>
|
& Pick<TaskGroup, 'id' | 'name'>
|
||||||
), badges: (
|
), badges: (
|
||||||
{ __typename?: 'TaskBadges' }
|
{ __typename?: 'TaskBadges' }
|
||||||
& { checklist?: Maybe<(
|
& { checklist?: Maybe<(
|
||||||
@ -1354,7 +1373,7 @@ export type MeQuery = (
|
|||||||
{ __typename?: 'MePayload' }
|
{ __typename?: 'MePayload' }
|
||||||
& { user: (
|
& { user: (
|
||||||
{ __typename?: 'UserAccount' }
|
{ __typename?: 'UserAccount' }
|
||||||
& Pick<UserAccount, 'id' | 'fullName'>
|
& Pick<UserAccount, 'id' | 'fullName' | 'username' | 'email' | 'bio'>
|
||||||
& { profileIcon: (
|
& { profileIcon: (
|
||||||
{ __typename?: 'ProfileIcon' }
|
{ __typename?: 'ProfileIcon' }
|
||||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||||
@ -2080,7 +2099,7 @@ export type CreateUserAccountMutation = (
|
|||||||
{ __typename?: 'Mutation' }
|
{ __typename?: 'Mutation' }
|
||||||
& { createUserAccount: (
|
& { createUserAccount: (
|
||||||
{ __typename?: 'UserAccount' }
|
{ __typename?: 'UserAccount' }
|
||||||
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username'>
|
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username' | 'bio'>
|
||||||
& { profileIcon: (
|
& { profileIcon: (
|
||||||
{ __typename?: 'ProfileIcon' }
|
{ __typename?: 'ProfileIcon' }
|
||||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||||
@ -2127,6 +2146,29 @@ 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 = {
|
export type UpdateUserPasswordMutationVariables = {
|
||||||
userID: Scalars['UUID'];
|
userID: Scalars['UUID'];
|
||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
@ -2660,6 +2702,7 @@ export const FindTaskDocument = gql`
|
|||||||
complete
|
complete
|
||||||
taskGroup {
|
taskGroup {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
checklist {
|
checklist {
|
||||||
@ -2795,6 +2838,9 @@ export const MeDocument = gql`
|
|||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
|
username
|
||||||
|
email
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
initials
|
initials
|
||||||
bgColor
|
bgColor
|
||||||
@ -4271,6 +4317,7 @@ export const CreateUserAccountDocument = gql`
|
|||||||
fullName
|
fullName
|
||||||
initials
|
initials
|
||||||
username
|
username
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
url
|
url
|
||||||
initials
|
initials
|
||||||
@ -4369,6 +4416,49 @@ export function useDeleteUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
|
|||||||
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
|
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
|
||||||
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
|
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
|
||||||
export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
|
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`
|
export const UpdateUserPasswordDocument = gql`
|
||||||
mutation updateUserPassword($userID: UUID!, $password: String!) {
|
mutation updateUserPassword($userID: UUID!, $password: String!) {
|
||||||
updateUserPassword(input: {userID: $userID, password: $password}) {
|
updateUserPassword(input: {userID: $userID, password: $password}) {
|
||||||
|
@ -8,6 +8,7 @@ query findTask($taskID: UUID!) {
|
|||||||
complete
|
complete
|
||||||
taskGroup {
|
taskGroup {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
checklist {
|
checklist {
|
||||||
|
@ -3,6 +3,9 @@ query me {
|
|||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
|
username
|
||||||
|
email
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
initials
|
initials
|
||||||
bgColor
|
bgColor
|
||||||
|
@ -24,6 +24,7 @@ export const CREATE_USER_MUTATION = gql`
|
|||||||
fullName
|
fullName
|
||||||
initials
|
initials
|
||||||
username
|
username
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
url
|
url
|
||||||
initials
|
initials
|
||||||
|
19
frontend/src/shared/graphql/user/updateUserInfo.ts
Normal file
19
frontend/src/shared/graphql/user/updateUserInfo.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
12
frontend/src/shared/icons/Eye.tsx
Normal file
12
frontend/src/shared/icons/Eye.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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;
|
12
frontend/src/shared/icons/List.tsx
Normal file
12
frontend/src/shared/icons/List.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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,5 +1,7 @@
|
|||||||
import Cross from './Cross';
|
import Cross from './Cross';
|
||||||
import Cog from './Cog';
|
import Cog from './Cog';
|
||||||
|
import Eye from './Eye';
|
||||||
|
import List from './List';
|
||||||
import At from './At';
|
import At from './At';
|
||||||
import Task from './Task';
|
import Task from './Task';
|
||||||
import Smile from './Smile';
|
import Smile from './Smile';
|
||||||
@ -85,4 +87,6 @@ export {
|
|||||||
Clone,
|
Clone,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
Share,
|
Share,
|
||||||
|
Eye,
|
||||||
|
List,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
let accessToken = '';
|
let accessToken = '';
|
||||||
|
|
||||||
export function setAccessToken(newToken: string) {
|
export function setAccessToken(newToken: string) {
|
||||||
|
console.log(newToken);
|
||||||
accessToken = newToken;
|
accessToken = newToken;
|
||||||
}
|
}
|
||||||
export function getAccessToken() {
|
export function getAccessToken() {
|
||||||
|
2
frontend/src/taskcafe.d.ts
vendored
2
frontend/src/taskcafe.d.ts
vendored
@ -46,6 +46,8 @@ type OwnedList = {
|
|||||||
type TaskUser = {
|
type TaskUser = {
|
||||||
id: string;
|
id: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
email?: string;
|
||||||
|
bio?: string;
|
||||||
profileIcon: ProfileIcon;
|
profileIcon: ProfileIcon;
|
||||||
username?: string;
|
username?: string;
|
||||||
role?: Role;
|
role?: Role;
|
||||||
|
@ -7,8 +7,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var jwtKey = []byte("taskcafe_test_key")
|
|
||||||
|
|
||||||
// RestrictedMode is used restrict JWT access to just the install route
|
// RestrictedMode is used restrict JWT access to just the install route
|
||||||
type RestrictedMode string
|
type RestrictedMode string
|
||||||
|
|
||||||
@ -54,7 +52,7 @@ func (r *ErrMalformedToken) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewAccessToken generates a new JWT access token with the correct claims
|
// NewAccessToken generates a new JWT access token with the correct claims
|
||||||
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string) (string, error) {
|
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string, jwtKey []byte) (string, error) {
|
||||||
role := RoleMember
|
role := RoleMember
|
||||||
if orgRole == "admin" {
|
if orgRole == "admin" {
|
||||||
role = RoleAdmin
|
role = RoleAdmin
|
||||||
@ -76,7 +74,7 @@ func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewAccessTokenCustomExpiration creates an access token with a custom duration
|
// NewAccessTokenCustomExpiration creates an access token with a custom duration
|
||||||
func NewAccessTokenCustomExpiration(userID string, dur time.Duration) (string, error) {
|
func NewAccessTokenCustomExpiration(userID string, dur time.Duration, jwtKey []byte) (string, error) {
|
||||||
accessExpirationTime := time.Now().Add(dur)
|
accessExpirationTime := time.Now().Add(dur)
|
||||||
accessClaims := &AccessTokenClaims{
|
accessClaims := &AccessTokenClaims{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@ -94,7 +92,7 @@ func NewAccessTokenCustomExpiration(userID string, dur time.Duration) (string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAccessToken validates a JWT access token and returns the contained claims or an error if it's invalid
|
// ValidateAccessToken validates a JWT access token and returns the contained claims or an error if it's invalid
|
||||||
func ValidateAccessToken(accessTokenString string) (AccessTokenClaims, error) {
|
func ValidateAccessToken(accessTokenString string, jwtKey []byte) (AccessTokenClaims, error) {
|
||||||
accessClaims := &AccessTokenClaims{}
|
accessClaims := &AccessTokenClaims{}
|
||||||
accessToken, err := jwt.ParseWithClaims(accessTokenString, accessClaims, func(token *jwt.Token) (interface{}, error) {
|
accessToken, err := jwt.ParseWithClaims(accessTokenString, accessClaims, func(token *jwt.Token) (interface{}, error) {
|
||||||
return jwtKey, nil
|
return jwtKey, nil
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
"github.com/jordanknott/taskcafe/internal/auth"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTokenCmd() *cobra.Command {
|
func newTokenCmd() *cobra.Command {
|
||||||
@ -15,13 +18,18 @@ func newTokenCmd() *cobra.Command {
|
|||||||
Short: "Create a long lived JWT token for dev purposes",
|
Short: "Create a long lived JWT token for dev purposes",
|
||||||
Long: "Create a long lived JWT token for dev purposes",
|
Long: "Create a long lived JWT token for dev purposes",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
token, err := auth.NewAccessTokenCustomExpiration(args[0], time.Hour*24)
|
secret := viper.GetString("server.secret")
|
||||||
|
if strings.TrimSpace(secret) == "" {
|
||||||
|
return errors.New("server.secret must be set (TASKCAFE_SERVER_SECRET)")
|
||||||
|
}
|
||||||
|
token, err := auth.NewAccessTokenCustomExpiration(args[0], time.Hour*24, []byte(secret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("issue while creating access token")
|
log.WithError(err).Error("issue while creating access token")
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println(token)
|
fmt.Println(token)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,13 @@ package commands
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
@ -38,16 +40,21 @@ func newWebCmd() *cobra.Command {
|
|||||||
)
|
)
|
||||||
var db *sqlx.DB
|
var db *sqlx.DB
|
||||||
var err error
|
var err error
|
||||||
retryNumber := 0
|
var retryDuration time.Duration
|
||||||
for i := 0; retryNumber <= 3; i++ {
|
maxRetryNumber := 4
|
||||||
retryNumber++
|
for i := 0; i < maxRetryNumber; i++ {
|
||||||
db, err = sqlx.Connect("postgres", connection)
|
db, err = sqlx.Connect("postgres", connection)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
retryDuration := time.Duration(i*2) * time.Second
|
retryDuration = time.Duration(i*2) * time.Second
|
||||||
log.WithFields(log.Fields{"retryNumber": retryNumber, "retryDuration": retryDuration}).WithError(err).Error("issue connecting to database, retrying")
|
log.WithFields(log.Fields{"retryNumber": i, "retryDuration": retryDuration}).WithError(err).Error("issue connecting to database, retrying")
|
||||||
time.Sleep(retryDuration)
|
if i != maxRetryNumber-1 {
|
||||||
|
time.Sleep(retryDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
db.SetMaxOpenConns(25)
|
db.SetMaxOpenConns(25)
|
||||||
db.SetMaxIdleConns(25)
|
db.SetMaxIdleConns(25)
|
||||||
@ -62,7 +69,12 @@ func newWebCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
|
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
|
||||||
r, _ := route.NewRouter(db)
|
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))
|
||||||
http.ListenAndServe(viper.GetString("server.hostname"), r)
|
http.ListenAndServe(viper.GetString("server.hostname"), r)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@ -157,4 +157,5 @@ type UserAccount struct {
|
|||||||
Initials string `json:"initials"`
|
Initials string `json:"initials"`
|
||||||
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
||||||
RoleCode string `json:"role_code"`
|
RoleCode string `json:"role_code"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,7 @@ type Querier interface {
|
|||||||
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
|
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
|
||||||
UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error)
|
UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error)
|
||||||
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, 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)
|
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
|
||||||
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
|
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,10 @@ INSERT INTO user_account(full_name, initials, email, username, created_at, passw
|
|||||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
||||||
RETURNING *;
|
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
|
-- name: DeleteUserAccountByID :exec
|
||||||
DELETE FROM user_account WHERE user_id = $1;
|
DELETE FROM user_account WHERE user_id = $1;
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
const createUserAccount = `-- name: CreateUserAccount :one
|
const createUserAccount = `-- name: CreateUserAccount :one
|
||||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
|
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
|
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
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserAccountParams struct {
|
type CreateUserAccountParams struct {
|
||||||
@ -48,6 +48,7 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -62,7 +63,7 @@ func (q *Queries) DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
|
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 FROM user_account WHERE username != 'system'
|
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username != 'system'
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
|
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
|
||||||
@ -85,6 +86,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -119,7 +121,7 @@ func (q *Queries) GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUserAccountByID = `-- name: GetUserAccountByID :one
|
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 FROM user_account WHERE user_id = $1
|
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE user_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
|
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
|
||||||
@ -136,12 +138,13 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
|
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 FROM user_account WHERE username = $1
|
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
|
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
|
||||||
@ -158,12 +161,13 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const setUserPassword = `-- name: SetUserPassword :one
|
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
|
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
|
||||||
`
|
`
|
||||||
|
|
||||||
type SetUserPasswordParams struct {
|
type SetUserPasswordParams struct {
|
||||||
@ -185,13 +189,52 @@ func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&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
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
|
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
|
||||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
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
|
RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserAccountProfileAvatarURLParams struct {
|
type UpdateUserAccountProfileAvatarURLParams struct {
|
||||||
@ -213,12 +256,13 @@ func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg Upd
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUserRole = `-- name: UpdateUserRole :one
|
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
|
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
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserRoleParams struct {
|
type UpdateUserRoleParams struct {
|
||||||
@ -240,6 +284,7 @@ func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams)
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
@ -210,6 +210,7 @@ type ComplexityRoot struct {
|
|||||||
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
|
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
|
||||||
UpdateTaskName func(childComplexity int, input UpdateTaskName) int
|
UpdateTaskName func(childComplexity int, input UpdateTaskName) int
|
||||||
UpdateTeamMemberRole func(childComplexity int, input UpdateTeamMemberRole) int
|
UpdateTeamMemberRole func(childComplexity int, input UpdateTeamMemberRole) int
|
||||||
|
UpdateUserInfo func(childComplexity int, input UpdateUserInfo) int
|
||||||
UpdateUserPassword func(childComplexity int, input UpdateUserPassword) int
|
UpdateUserPassword func(childComplexity int, input UpdateUserPassword) int
|
||||||
UpdateUserRole func(childComplexity int, input UpdateUserRole) int
|
UpdateUserRole func(childComplexity int, input UpdateUserRole) int
|
||||||
}
|
}
|
||||||
@ -404,6 +405,10 @@ type ComplexityRoot struct {
|
|||||||
TeamID func(childComplexity int) int
|
TeamID func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateUserInfoPayload struct {
|
||||||
|
User func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
UpdateUserPasswordPayload struct {
|
UpdateUserPasswordPayload struct {
|
||||||
Ok func(childComplexity int) int
|
Ok func(childComplexity int) int
|
||||||
User func(childComplexity int) int
|
User func(childComplexity int) int
|
||||||
@ -414,6 +419,7 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UserAccount struct {
|
UserAccount struct {
|
||||||
|
Bio func(childComplexity int) int
|
||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
Email func(childComplexity int) int
|
Email func(childComplexity int) int
|
||||||
FullName func(childComplexity int) int
|
FullName func(childComplexity int) int
|
||||||
@ -482,6 +488,7 @@ type MutationResolver interface {
|
|||||||
ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error)
|
ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error)
|
||||||
UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error)
|
UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error)
|
||||||
UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error)
|
UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error)
|
||||||
|
UpdateUserInfo(ctx context.Context, input UpdateUserInfo) (*UpdateUserInfoPayload, error)
|
||||||
}
|
}
|
||||||
type NotificationResolver interface {
|
type NotificationResolver interface {
|
||||||
ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error)
|
ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error)
|
||||||
@ -1493,6 +1500,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.Mutation.UpdateTeamMemberRole(childComplexity, args["input"].(UpdateTeamMemberRole)), true
|
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":
|
case "Mutation.updateUserPassword":
|
||||||
if e.complexity.Mutation.UpdateUserPassword == nil {
|
if e.complexity.Mutation.UpdateUserPassword == nil {
|
||||||
break
|
break
|
||||||
@ -2284,6 +2303,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.UpdateTeamMemberRolePayload.TeamID(childComplexity), true
|
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":
|
case "UpdateUserPasswordPayload.ok":
|
||||||
if e.complexity.UpdateUserPasswordPayload.Ok == nil {
|
if e.complexity.UpdateUserPasswordPayload.Ok == nil {
|
||||||
break
|
break
|
||||||
@ -2305,6 +2331,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.UpdateUserRolePayload.User(childComplexity), true
|
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":
|
case "UserAccount.createdAt":
|
||||||
if e.complexity.UserAccount.CreatedAt == nil {
|
if e.complexity.UserAccount.CreatedAt == nil {
|
||||||
break
|
break
|
||||||
@ -2519,6 +2552,7 @@ type UserAccount {
|
|||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
initials: String!
|
initials: String!
|
||||||
|
bio: String!
|
||||||
role: Role!
|
role: Role!
|
||||||
username: String!
|
username: String!
|
||||||
profileIcon: ProfileIcon!
|
profileIcon: ProfileIcon!
|
||||||
@ -3163,6 +3197,19 @@ extend type Mutation {
|
|||||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||||
updateUserRole(input: UpdateUserRole!):
|
updateUserRole(input: UpdateUserRole!):
|
||||||
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
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 {
|
input UpdateUserPassword {
|
||||||
@ -3921,6 +3968,20 @@ func (ec *executionContext) field_Mutation_updateTeamMemberRole_args(ctx context
|
|||||||
return args, nil
|
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) {
|
func (ec *executionContext) field_Mutation_updateUserPassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]interface{}{}
|
args := map[string]interface{}{}
|
||||||
@ -9221,6 +9282,79 @@ func (ec *executionContext) _Mutation_updateUserRole(ctx context.Context, field
|
|||||||
return ec.marshalNUpdateUserRolePayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserRolePayload(ctx, field.Selections, res)
|
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) {
|
func (ec *executionContext) _Notification_id(ctx context.Context, field graphql.CollectedField, obj *db.Notification) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -12905,6 +13039,40 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload_member(ctx context.Cont
|
|||||||
return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res)
|
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) {
|
func (ec *executionContext) _UpdateUserPasswordPayload_ok(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -13177,6 +13345,40 @@ func (ec *executionContext) _UserAccount_initials(ctx context.Context, field gra
|
|||||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
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) {
|
func (ec *executionContext) _UserAccount_role(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -15710,6 +15912,42 @@ func (ec *executionContext) unmarshalInputUpdateTeamMemberRole(ctx context.Conte
|
|||||||
return it, nil
|
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) {
|
func (ec *executionContext) unmarshalInputUpdateUserPassword(ctx context.Context, obj interface{}) (UpdateUserPassword, error) {
|
||||||
var it UpdateUserPassword
|
var it UpdateUserPassword
|
||||||
var asMap = obj.(map[string]interface{})
|
var asMap = obj.(map[string]interface{})
|
||||||
@ -16671,6 +16909,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
|
case "updateUserInfo":
|
||||||
|
out.Values[i] = ec._Mutation_updateUserInfo(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
}
|
}
|
||||||
@ -18235,6 +18478,33 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload(ctx context.Context, se
|
|||||||
return out
|
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"}
|
var updateUserPasswordPayloadImplementors = []string{"UpdateUserPasswordPayload"}
|
||||||
|
|
||||||
func (ec *executionContext) _UpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserPasswordPayload) graphql.Marshaler {
|
func (ec *executionContext) _UpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserPasswordPayload) graphql.Marshaler {
|
||||||
@ -18339,6 +18609,11 @@ func (ec *executionContext) _UserAccount(ctx context.Context, sel ast.SelectionS
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
atomic.AddUint32(&invalids, 1)
|
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":
|
case "role":
|
||||||
field := field
|
field := field
|
||||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||||
@ -20203,6 +20478,24 @@ func (ec *executionContext) marshalNUpdateTeamMemberRolePayload2ᚖgithubᚗcom
|
|||||||
return ec._UpdateTeamMemberRolePayload(ctx, sel, v)
|
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) {
|
func (ec *executionContext) unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserPassword(ctx context.Context, v interface{}) (UpdateUserPassword, error) {
|
||||||
return ec.unmarshalInputUpdateUserPassword(ctx, v)
|
return ec.unmarshalInputUpdateUserPassword(ctx, v)
|
||||||
}
|
}
|
||||||
|
@ -455,6 +455,17 @@ type UpdateTeamMemberRolePayload struct {
|
|||||||
Member *Member `json:"member"`
|
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 {
|
type UpdateUserPassword struct {
|
||||||
UserID uuid.UUID `json:"userID"`
|
UserID uuid.UUID `json:"userID"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
@ -78,6 +78,7 @@ type UserAccount {
|
|||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
initials: String!
|
initials: String!
|
||||||
|
bio: String!
|
||||||
role: Role!
|
role: Role!
|
||||||
username: String!
|
username: String!
|
||||||
profileIcon: ProfileIcon!
|
profileIcon: ProfileIcon!
|
||||||
@ -722,6 +723,19 @@ extend type Mutation {
|
|||||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||||
updateUserRole(input: UpdateUserRole!):
|
updateUserRole(input: UpdateUserRole!):
|
||||||
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
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 {
|
input UpdateUserPassword {
|
||||||
|
@ -826,6 +826,17 @@ func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserR
|
|||||||
return &UpdateUserRolePayload{User: &user}, nil
|
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) {
|
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
|
||||||
return obj.NotificationID, nil
|
return obj.NotificationID, nil
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@ type UserAccount {
|
|||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
initials: String!
|
initials: String!
|
||||||
|
bio: String!
|
||||||
role: Role!
|
role: Role!
|
||||||
username: String!
|
username: String!
|
||||||
profileIcon: ProfileIcon!
|
profileIcon: ProfileIcon!
|
||||||
|
@ -11,6 +11,19 @@ extend type Mutation {
|
|||||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||||
updateUserRole(input: UpdateUserRole!):
|
updateUserRole(input: UpdateUserRole!):
|
||||||
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
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 {
|
input UpdateUserPassword {
|
||||||
|
@ -14,8 +14,6 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var jwtKey = []byte("taskcafe_test_key")
|
|
||||||
|
|
||||||
type authResource struct{}
|
type authResource struct{}
|
||||||
|
|
||||||
// LoginRequestData is the request data when a user logs in
|
// LoginRequestData is the request data when a user logs in
|
||||||
@ -69,7 +67,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
|||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode)
|
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode, h.jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -123,7 +121,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
|||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode)
|
accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -190,7 +188,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
||||||
|
|
||||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode)
|
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -251,10 +249,12 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
||||||
|
|
||||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode)
|
log.WithField("userID", user.UserID.String()).Info("creating install access token")
|
||||||
|
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
log.Info(accessTokenString)
|
||||||
|
|
||||||
w.Header().Set("Content-type", "application/json")
|
w.Header().Set("Content-type", "application/json")
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
@ -5,7 +5,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@ -14,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/frontend"
|
"github.com/jordanknott/taskcafe/internal/frontend"
|
||||||
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frontend serves the index.html file
|
// Frontend serves the index.html file
|
||||||
@ -30,7 +33,7 @@ func (h *TaskcafeHandler) Frontend(w http.ResponseWriter, r *http.Request) {
|
|||||||
// ProfileImageUpload handles a user uploading a new avatar profile image
|
// ProfileImageUpload handles a user uploading a new avatar profile image
|
||||||
func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
|
func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Info("preparing to upload file")
|
log.Info("preparing to upload file")
|
||||||
userID, ok := r.Context().Value("userID").(uuid.UUID)
|
userID, ok := r.Context().Value(utils.UserIDKey).(uuid.UUID)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error("not a valid uuid")
|
log.Error("not a valid uuid")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
@ -47,22 +50,24 @@ func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
log.WithFields(log.Fields{"filename": handler.Filename, "size": handler.Size, "header": handler.Header}).Info("file metadata")
|
filename := strings.ReplaceAll(handler.Filename, " ", "-")
|
||||||
|
encodedFilename := url.QueryEscape(filename)
|
||||||
|
log.WithFields(log.Fields{"filename": encodedFilename, "size": handler.Size, "header": handler.Header}).Info("file metadata")
|
||||||
|
|
||||||
fileBytes, err := ioutil.ReadAll(file)
|
fileBytes, err := ioutil.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("while reading file")
|
log.WithError(err).Error("while reading file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile("uploads/"+handler.Filename, fileBytes, 0644)
|
err = ioutil.WriteFile("uploads/"+filename, fileBytes, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("while reading file")
|
log.WithError(err).Error("while reading file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.repo.UpdateUserAccountProfileAvatarURL(r.Context(), db.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{String: "http://localhost:3333/uploads/" + handler.Filename, Valid: true}})
|
h.repo.UpdateUserAccountProfileAvatarURL(r.Context(), db.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{String: "/uploads/" + encodedFilename, Valid: true}})
|
||||||
// return that we have successfully uploaded our file!
|
// return that we have successfully uploaded our file!
|
||||||
log.Info("file uploaded")
|
log.Info("file uploaded")
|
||||||
json.NewEncoder(w).Encode(AvatarUploadResponseData{URL: "http://localhost:3333/uploads/" + handler.Filename, UserID: userID.String()})
|
json.NewEncoder(w).Encode(AvatarUploadResponseData{URL: "/uploads/" + encodedFilename, UserID: userID.String()})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AuthenticationMiddleware is a middleware that requires a valid JWT token to be passed via the Authorization header
|
// AuthenticationMiddleware is a middleware that requires a valid JWT token to be passed via the Authorization header
|
||||||
func AuthenticationMiddleware(next http.Handler) http.Handler {
|
type AuthenticationMiddleware struct {
|
||||||
|
jwtKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware returns the middleware handler
|
||||||
|
func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
bearerTokenRaw := r.Header.Get("Authorization")
|
bearerTokenRaw := r.Header.Get("Authorization")
|
||||||
splitToken := strings.Split(bearerTokenRaw, "Bearer")
|
splitToken := strings.Split(bearerTokenRaw, "Bearer")
|
||||||
@ -21,7 +26,7 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
accessTokenString := strings.TrimSpace(splitToken[1])
|
accessTokenString := strings.TrimSpace(splitToken[1])
|
||||||
accessClaims, err := auth.ValidateAccessToken(accessTokenString)
|
accessClaims, err := auth.ValidateAccessToken(accessTokenString, m.jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*auth.ErrExpiredToken); ok {
|
if _, ok := err.(*auth.ErrExpiredToken); ok {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
@ -59,11 +59,12 @@ func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// TaskcafeHandler contains all the route handlers
|
// TaskcafeHandler contains all the route handlers
|
||||||
type TaskcafeHandler struct {
|
type TaskcafeHandler struct {
|
||||||
repo db.Repository
|
repo db.Repository
|
||||||
|
jwtKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter creates a new router for chi
|
// NewRouter creates a new router for chi
|
||||||
func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
|
func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
|
||||||
formatter := new(log.TextFormatter)
|
formatter := new(log.TextFormatter)
|
||||||
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||||
formatter.FullTimestamp = true
|
formatter.FullTimestamp = true
|
||||||
@ -79,7 +80,7 @@ func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
|
|||||||
r.Use(middleware.Timeout(60 * time.Second))
|
r.Use(middleware.Timeout(60 * time.Second))
|
||||||
|
|
||||||
repository := db.NewRepository(dbConnection)
|
repository := db.NewRepository(dbConnection)
|
||||||
taskcafeHandler := TaskcafeHandler{*repository}
|
taskcafeHandler := TaskcafeHandler{*repository, jwtKey}
|
||||||
|
|
||||||
var imgServer = http.FileServer(http.Dir("./uploads/"))
|
var imgServer = http.FileServer(http.Dir("./uploads/"))
|
||||||
r.Group(func(mux chi.Router) {
|
r.Group(func(mux chi.Router) {
|
||||||
@ -88,8 +89,9 @@ func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
|
|||||||
mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
|
mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
|
||||||
|
|
||||||
})
|
})
|
||||||
|
auth := AuthenticationMiddleware{jwtKey}
|
||||||
r.Group(func(mux chi.Router) {
|
r.Group(func(mux chi.Router) {
|
||||||
mux.Use(AuthenticationMiddleware)
|
mux.Use(auth.Middleware)
|
||||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
||||||
mux.Post("/auth/install", taskcafeHandler.InstallHandler)
|
mux.Post("/auth/install", taskcafeHandler.InstallHandler)
|
||||||
mux.Handle("/graphql", graph.NewHandler(*repository))
|
mux.Handle("/graphql", graph.NewHandler(*repository))
|
||||||
|
1
migrations/0052_add-bio-col-to-user_account.up.sql
Normal file
1
migrations/0052_add-bio-col-to-user_account.up.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE user_account ADD COLUMN bio text NOT NULL DEFAULT '';
|
Reference in New Issue
Block a user