Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ce0fac281 | ||
|
|
e3d00c8791 | ||
|
|
e1a0b635ea | ||
|
|
cc7959bb15 | ||
|
|
56044e6d8a | ||
|
|
e5bd73f33e | ||
|
|
e65ab6ece7 | ||
|
|
14cc55a8c0 | ||
|
|
b4306d0a4b | ||
|
|
283c850ebf | ||
|
|
62de71bd97 | ||
|
|
447f88880d | ||
|
|
8109936ce2 | ||
|
|
640908320a |
Generated
+11
@@ -6,6 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "planka-client",
|
"name": "planka-client",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"connected-react-router": "^6.9.3",
|
"connected-react-router": "^6.9.3",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.1",
|
||||||
@@ -3030,6 +3031,11 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@juggle/resize-observer": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
|
},
|
||||||
"node_modules/@leichtgewicht/ip-codec": {
|
"node_modules/@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||||
@@ -27333,6 +27339,11 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@juggle/resize-observer": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
|
},
|
||||||
"@leichtgewicht/ip-codec": {
|
"@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"connected-react-router": "^6.9.3",
|
"connected-react-router": "^6.9.3",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.1",
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ const logout = () => ({
|
|||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logout.invalidateAccessToken = () => ({
|
||||||
|
type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
initializeCore,
|
initializeCore,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
@@ -103,10 +103,11 @@ const updateUserPassword = (id, data) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUserPassword.success = (user) => ({
|
updateUserPassword.success = (user, accessToken) => ({
|
||||||
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
|
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
|
||||||
payload: {
|
payload: {
|
||||||
user,
|
user,
|
||||||
|
accessToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import http from './http';
|
import http from './http';
|
||||||
|
import socket from './socket';
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
||||||
|
|
||||||
|
const deleteCurrentAccessToken = (headers) =>
|
||||||
|
socket.delete('/access-tokens/me', undefined, headers);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createAccessToken,
|
createAccessToken,
|
||||||
|
deleteCurrentAccessToken,
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
+3
-3
@@ -19,7 +19,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
|||||||
...defaultData,
|
...defaultData,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleRoleSelectClick = useCallback((role) => {
|
const handleSelectRoleClick = useCallback((role) => {
|
||||||
setData((prevData) => ({
|
setData((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
role,
|
role,
|
||||||
@@ -50,7 +50,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
|||||||
<Menu secondary vertical className={styles.menu}>
|
<Menu secondary vertical className={styles.menu}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
active={data.role === BoardMembershipRoles.EDITOR}
|
active={data.role === BoardMembershipRoles.EDITOR}
|
||||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)}
|
onClick={() => handleSelectRoleClick(BoardMembershipRoles.EDITOR)}
|
||||||
>
|
>
|
||||||
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
|
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
|
||||||
<div className={styles.menuItemDescription}>
|
<div className={styles.menuItemDescription}>
|
||||||
@@ -59,7 +59,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
active={data.role === BoardMembershipRoles.VIEWER}
|
active={data.role === BoardMembershipRoles.VIEWER}
|
||||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)}
|
onClick={() => handleSelectRoleClick(BoardMembershipRoles.VIEWER)}
|
||||||
>
|
>
|
||||||
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
|
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
|
||||||
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
|
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button, Icon } from 'semantic-ui-react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { startTimer, stopTimer } from '../../utils/timer';
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
import Tasks from './Tasks';
|
import Tasks from './Tasks';
|
||||||
import NameEdit from './NameEdit';
|
import NameEdit from './NameEdit';
|
||||||
@@ -57,6 +58,17 @@ const Card = React.memo(
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleToggleTimerClick = useCallback(
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[timer, onUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
const handleNameUpdate = useCallback(
|
const handleNameUpdate = useCallback(
|
||||||
(newName) => {
|
(newName) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@@ -108,7 +120,13 @@ const Card = React.memo(
|
|||||||
)}
|
)}
|
||||||
{timer && (
|
{timer && (
|
||||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||||
<Timer startedAt={timer.startedAt} total={timer.total} size="tiny" />
|
<Timer
|
||||||
|
as="span"
|
||||||
|
startedAt={timer.startedAt}
|
||||||
|
total={timer.total}
|
||||||
|
size="tiny"
|
||||||
|
onClick={canEdit ? handleToggleTimerClick : undefined}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -148,14 +166,11 @@ const Card = React.memo(
|
|||||||
{canEdit && (
|
{canEdit && (
|
||||||
<ActionsPopup
|
<ActionsPopup
|
||||||
card={{
|
card={{
|
||||||
id,
|
|
||||||
name,
|
|
||||||
dueDate,
|
dueDate,
|
||||||
timer,
|
timer,
|
||||||
boardId,
|
boardId,
|
||||||
listId,
|
listId,
|
||||||
projectId,
|
projectId,
|
||||||
isPersisted,
|
|
||||||
}}
|
}}
|
||||||
projectsToLists={allProjectsToLists}
|
projectsToLists={allProjectsToLists}
|
||||||
boardMemberships={allBoardMemberships}
|
boardMemberships={allBoardMemberships}
|
||||||
|
|||||||
@@ -78,7 +78,9 @@
|
|||||||
.cover {
|
.cover {
|
||||||
border-radius: 3px 3px 0 0;
|
border-radius: 3px 3px 0 0;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
padding: 2px 2px 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
||||||
import { Markdown } from '../../lib/custom-ui';
|
import { Markdown } from '../../lib/custom-ui';
|
||||||
|
|
||||||
|
import { startTimer, stopTimer } from '../../utils/timer';
|
||||||
import NameField from './NameField';
|
import NameField from './NameField';
|
||||||
import DescriptionEdit from './DescriptionEdit';
|
import DescriptionEdit from './DescriptionEdit';
|
||||||
import Tasks from './Tasks';
|
import Tasks from './Tasks';
|
||||||
@@ -80,6 +81,12 @@ const CardModal = React.memo(
|
|||||||
|
|
||||||
const isGalleryOpened = useRef(false);
|
const isGalleryOpened = useRef(false);
|
||||||
|
|
||||||
|
const handleToggleTimerClick = useCallback(() => {
|
||||||
|
onUpdate({
|
||||||
|
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
|
||||||
|
});
|
||||||
|
}, [timer, onUpdate]);
|
||||||
|
|
||||||
const handleNameUpdate = useCallback(
|
const handleNameUpdate = useCallback(
|
||||||
(newName) => {
|
(newName) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@@ -291,6 +298,19 @@ const CardModal = React.memo(
|
|||||||
<Timer startedAt={timer.startedAt} total={timer.total} />
|
<Timer startedAt={timer.startedAt} total={timer.total} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
{canEdit && (
|
||||||
|
<button
|
||||||
|
onClick={handleToggleTimerClick}
|
||||||
|
type="button"
|
||||||
|
className={classNames(styles.attachment, styles.dueDate)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={timer.startedAt ? 'pause' : 'play'}
|
||||||
|
size="small"
|
||||||
|
className={styles.addAttachment}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const Header = React.memo(
|
|||||||
project,
|
project,
|
||||||
user,
|
user,
|
||||||
notifications,
|
notifications,
|
||||||
|
isLogouting,
|
||||||
canEditProject,
|
canEditProject,
|
||||||
canEditUsers,
|
canEditUsers,
|
||||||
onProjectSettingsClick,
|
onProjectSettingsClick,
|
||||||
@@ -75,7 +76,11 @@ const Header = React.memo(
|
|||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</NotificationsPopup>
|
</NotificationsPopup>
|
||||||
<UserPopup onSettingsClick={onUserSettingsClick} onLogout={onLogout}>
|
<UserPopup
|
||||||
|
isLogouting={isLogouting}
|
||||||
|
onSettingsClick={onUserSettingsClick}
|
||||||
|
onLogout={onLogout}
|
||||||
|
>
|
||||||
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
||||||
{user.name}
|
{user.name}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -93,6 +98,7 @@ Header.propTypes = {
|
|||||||
user: PropTypes.object.isRequired,
|
user: PropTypes.object.isRequired,
|
||||||
notifications: PropTypes.array.isRequired,
|
notifications: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
|
isLogouting: PropTypes.bool.isRequired,
|
||||||
canEditProject: PropTypes.bool.isRequired,
|
canEditProject: PropTypes.bool.isRequired,
|
||||||
canEditUsers: PropTypes.bool.isRequired,
|
canEditUsers: PropTypes.bool.isRequired,
|
||||||
onProjectSettingsClick: PropTypes.func.isRequired,
|
onProjectSettingsClick: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const SIZES = {
|
|||||||
MEDIUM: 'medium',
|
MEDIUM: 'medium',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
const Timer = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) => {
|
||||||
const prevStartedAt = usePrevious(startedAt);
|
const prevStartedAt = usePrevious(startedAt);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
@@ -52,6 +52,7 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
styles.wrapper,
|
styles.wrapper,
|
||||||
styles[`wrapper${upperFirst(size)}`],
|
styles[`wrapper${upperFirst(size)}`],
|
||||||
|
startedAt && styles.wrapperActive,
|
||||||
onClick && styles.wrapperHoverable,
|
onClick && styles.wrapperHoverable,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -59,16 +60,19 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ElementType = as;
|
||||||
|
|
||||||
return onClick ? (
|
return onClick ? (
|
||||||
<button type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
|
<ElementType type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
|
||||||
{contentNode}
|
{contentNode}
|
||||||
</button>
|
</ElementType>
|
||||||
) : (
|
) : (
|
||||||
contentNode
|
contentNode
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Timer.propTypes = {
|
Timer.propTypes = {
|
||||||
|
as: PropTypes.elementType,
|
||||||
startedAt: PropTypes.instanceOf(Date),
|
startedAt: PropTypes.instanceOf(Date),
|
||||||
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
|
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||||
size: PropTypes.oneOf(Object.values(SIZES)),
|
size: PropTypes.oneOf(Object.values(SIZES)),
|
||||||
@@ -77,6 +81,7 @@ Timer.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Timer.defaultProps = {
|
Timer.defaultProps = {
|
||||||
|
as: 'button',
|
||||||
startedAt: undefined,
|
startedAt: undefined,
|
||||||
size: SIZES.MEDIUM,
|
size: SIZES.MEDIUM,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
|||||||
@@ -9,17 +9,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
background: #f3f4f6;
|
background: #dce0e4;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: #7e8ea3;
|
color: #6a808b;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: background 0.3s ease;
|
transition: background 0.3s ease;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapperActive {
|
||||||
|
background: #21ba45;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&.wrapperHoverable:hover {
|
||||||
|
background: #16ab39;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wrapperHoverable:hover {
|
.wrapperHoverable:hover {
|
||||||
background: #d2d8dc;
|
background: #d2d8dc;
|
||||||
color: #17394d;
|
color: #17394d;
|
||||||
@@ -42,6 +53,5 @@
|
|||||||
.wrapperMedium {
|
.wrapperMedium {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
|
||||||
font-size: 11px;
|
|
||||||
margin-top: 4px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
color: #444444;
|
color: #444444;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
|
||||||
font-size: 11px;
|
|
||||||
margin-top: 4px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
color: #444444;
|
color: #444444;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Menu } from 'semantic-ui-react';
|
import { Button, Menu } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../lib/popup';
|
import { withPopup } from '../../lib/popup';
|
||||||
import { Popup } from '../../lib/custom-ui';
|
import { Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import styles from './UserPopup.module.scss';
|
import styles from './UserPopup.module.scss';
|
||||||
|
|
||||||
const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
const UserStep = React.memo(({ isLogouting, onSettingsClick, onLogout, onClose }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
|
||||||
const handleSettingsClick = useCallback(() => {
|
const handleSettingsClick = useCallback(() => {
|
||||||
@@ -15,6 +15,17 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||||||
onClose();
|
onClose();
|
||||||
}, [onSettingsClick, onClose]);
|
}, [onSettingsClick, onClose]);
|
||||||
|
|
||||||
|
let logoutMenuItemProps;
|
||||||
|
if (isLogouting) {
|
||||||
|
logoutMenuItemProps = {
|
||||||
|
as: Button,
|
||||||
|
fluid: true,
|
||||||
|
basic: true,
|
||||||
|
loading: true,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header>
|
<Popup.Header>
|
||||||
@@ -29,7 +40,11 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item className={styles.menuItem} onClick={onLogout}>
|
<Menu.Item
|
||||||
|
{...logoutMenuItemProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
|
className={styles.menuItem}
|
||||||
|
onClick={onLogout}
|
||||||
|
>
|
||||||
{t('action.logOut', {
|
{t('action.logOut', {
|
||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
@@ -41,6 +56,7 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
UserStep.propTypes = {
|
UserStep.propTypes = {
|
||||||
|
isLogouting: PropTypes.bool.isRequired,
|
||||||
onSettingsClick: PropTypes.func.isRequired,
|
onSettingsClick: PropTypes.func.isRequired,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
// import { useTranslation } from 'react-i18next';
|
||||||
|
import { Image, Tab } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
import logo from '../../assets/images/logo.png';
|
||||||
|
|
||||||
|
import styles from './AboutPane.module.scss';
|
||||||
|
|
||||||
|
const AboutPane = React.memo(() => {
|
||||||
|
// const [t] = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||||
|
<Image centered src={logo} size="large" className={styles.logo} />
|
||||||
|
</Tab.Pane>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AboutPane;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
:global(#app) {
|
||||||
|
/* .logo {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { Modal, Tab } from 'semantic-ui-react';
|
|||||||
|
|
||||||
import AccountPane from './AccountPane';
|
import AccountPane from './AccountPane';
|
||||||
import PreferencesPane from './PreferencesPane';
|
import PreferencesPane from './PreferencesPane';
|
||||||
|
import AboutPane from './AboutPane';
|
||||||
|
|
||||||
const UserSettingsModal = React.memo(
|
const UserSettingsModal = React.memo(
|
||||||
({
|
({
|
||||||
@@ -71,6 +72,10 @@ const UserSettingsModal = React.memo(
|
|||||||
<PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} />
|
<PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
menuItem: 'About Planka',
|
||||||
|
render: () => <AboutPane />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default {
|
|||||||
|
|
||||||
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
||||||
LOGOUT: 'LOGOUT',
|
LOGOUT: 'LOGOUT',
|
||||||
|
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
|
||||||
|
|
||||||
/* Modals */
|
/* Modals */
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import entryActions from '../entry-actions';
|
|||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
|
const isLogouting = selectors.selectIsLogouting(state);
|
||||||
const currentUser = selectors.selectCurrentUser(state);
|
const currentUser = selectors.selectCurrentUser(state);
|
||||||
const currentProject = selectors.selectCurrentProject(state);
|
const currentProject = selectors.selectCurrentProject(state);
|
||||||
const notifications = selectors.selectNotificationsForCurrentUser(state);
|
const notifications = selectors.selectNotificationsForCurrentUser(state);
|
||||||
@@ -13,6 +14,7 @@ const mapStateToProps = (state) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
|
isLogouting,
|
||||||
project: currentProject,
|
project: currentProject,
|
||||||
user: currentUser,
|
user: currentUser,
|
||||||
canEditProject: isCurrentUserManager,
|
canEditProject: isCurrentUserManager,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import { ResizeObserver } from '@juggle/resize-observer';
|
||||||
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
|
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
|
||||||
|
|
||||||
@@ -8,6 +9,9 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
const Popup = React.memo(({ children, ...props }) => {
|
const Popup = React.memo(({ children, ...props }) => {
|
||||||
const [isOpened, setIsOpened] = useState(false);
|
const [isOpened, setIsOpened] = useState(false);
|
||||||
|
|
||||||
|
const wrapper = useRef(null);
|
||||||
|
const resizeObserver = useRef(null);
|
||||||
|
|
||||||
const handleOpen = useCallback(() => {
|
const handleOpen = useCallback(() => {
|
||||||
setIsOpened(true);
|
setIsOpened(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -37,6 +41,29 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
[children],
|
[children],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleContentRef = useCallback((element) => {
|
||||||
|
if (resizeObserver.current) {
|
||||||
|
resizeObserver.current.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
resizeObserver.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeObserver.current = new ResizeObserver(() => {
|
||||||
|
if (resizeObserver.current.isInitial) {
|
||||||
|
resizeObserver.current.isInitial = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.current.positionUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.current.isInitial = true;
|
||||||
|
resizeObserver.current.observe(element);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const tigger = React.cloneElement(children, {
|
const tigger = React.cloneElement(children, {
|
||||||
onClick: handleTriggerClick,
|
onClick: handleTriggerClick,
|
||||||
});
|
});
|
||||||
@@ -45,6 +72,7 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
<SemanticUIPopup
|
<SemanticUIPopup
|
||||||
basic
|
basic
|
||||||
wide
|
wide
|
||||||
|
ref={wrapper}
|
||||||
trigger={tigger}
|
trigger={tigger}
|
||||||
on="click"
|
on="click"
|
||||||
open={isOpened}
|
open={isOpened}
|
||||||
@@ -64,9 +92,11 @@ export default (WrappedComponent, defaultProps) => {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
|
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
>
|
>
|
||||||
|
<div ref={handleContentRef}>
|
||||||
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
|
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
<WrappedComponent {...props} onClose={handleClose} />
|
<WrappedComponent {...props} onClose={handleClose} />
|
||||||
|
</div>
|
||||||
</SemanticUIPopup>
|
</SemanticUIPopup>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import core from './core';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
language: 'en',
|
language: 'en',
|
||||||
country: 'us',
|
country: 'gb',
|
||||||
name: 'English',
|
name: 'English',
|
||||||
embeddedLocale: merge(login, core),
|
embeddedLocale: merge(login, core),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
|
import { getAccessToken } from '../utils/access-token-storage';
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
accessToken: getAccessToken(),
|
||||||
userId: null,
|
userId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line default-param-last
|
// eslint-disable-next-line default-param-last
|
||||||
export default (state = initialState, { type, payload }) => {
|
export default (state = initialState, { type, payload }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accessToken: payload.accessToken,
|
||||||
|
};
|
||||||
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
||||||
case ActionTypes.CORE_INITIALIZE:
|
case ActionTypes.CORE_INITIALIZE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userId: payload.user.id,
|
userId: payload.user.id,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
|
||||||
|
if (payload.accessToken) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accessToken: payload.accessToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ModalTypes from '../constants/ModalTypes';
|
|||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isInitializing: true,
|
isInitializing: true,
|
||||||
|
isLogouting: false,
|
||||||
currentModal: null,
|
currentModal: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,6 +23,11 @@ export default (state = initialState, { type, payload }) => {
|
|||||||
...state,
|
...state,
|
||||||
isInitializing: false,
|
isInitializing: false,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLogouting: true,
|
||||||
|
};
|
||||||
case ActionTypes.MODAL_OPEN:
|
case ActionTypes.MODAL_OPEN:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { all, apply, call, fork, take } from 'redux-saga/effects';
|
import { all, apply, fork, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
import watchers from './watchers';
|
import watchers from './watchers';
|
||||||
import services from './services';
|
import services from './services';
|
||||||
import { socket } from '../../api';
|
import { socket } from '../../api';
|
||||||
import { removeAccessToken } from '../../utils/access-token-storage';
|
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
|
|
||||||
@@ -15,6 +14,5 @@ export default function* coreSaga() {
|
|||||||
|
|
||||||
yield take(ActionTypes.LOGOUT);
|
yield take(ActionTypes.LOGOUT);
|
||||||
|
|
||||||
yield call(removeAccessToken);
|
|
||||||
window.location.href = Paths.LOGIN;
|
window.location.href = Paths.LOGIN;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { call, fork, join, put, take } from 'redux-saga/effects';
|
import { call, fork, join, put, select, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import selectors from '../../selectors';
|
||||||
import actions from '../../actions';
|
import actions from '../../actions';
|
||||||
import { getAccessToken } from '../../utils/access-token-storage';
|
import { removeAccessToken } from '../../utils/access-token-storage';
|
||||||
import ErrorCodes from '../../constants/ErrorCodes';
|
import ErrorCodes from '../../constants/ErrorCodes';
|
||||||
|
|
||||||
let lastRequestTask;
|
let lastRequestTask;
|
||||||
@@ -13,7 +14,7 @@ function* queueRequest(method, ...args) {
|
|||||||
} catch {} // eslint-disable-line no-empty
|
} catch {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = yield call(getAccessToken);
|
const accessToken = yield select(selectors.selectAccessToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return yield call(method, ...args, {
|
return yield call(method, ...args, {
|
||||||
@@ -21,6 +22,7 @@ function* queueRequest(method, ...args) {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
||||||
|
yield call(removeAccessToken);
|
||||||
yield put(actions.logout()); // TODO: next url
|
yield put(actions.logout()); // TODO: next url
|
||||||
yield take();
|
yield take();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { call, put, take } from 'redux-saga/effects';
|
import { call, put, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import request from '../request';
|
||||||
import requests from '../requests';
|
import requests from '../requests';
|
||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
|
import api from '../../../api';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
|
import { removeAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
|
||||||
export function* initializeCore() {
|
export function* initializeCore() {
|
||||||
const {
|
const {
|
||||||
@@ -60,7 +63,17 @@ export function* changeCoreLanguage(language) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* logout() {
|
export function* logout(invalidateAccessToken = true) {
|
||||||
|
yield call(removeAccessToken);
|
||||||
|
|
||||||
|
if (invalidateAccessToken) {
|
||||||
|
yield put(actions.logout.invalidateAccessToken());
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield call(request, api.deleteCurrentAccessToken);
|
||||||
|
} catch (error) {} // eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
yield put(actions.logout());
|
yield put(actions.logout());
|
||||||
yield take();
|
yield take();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,11 +124,13 @@ export function* updateUserPassword(id, data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessTokens && accessTokens[0]) {
|
const accessToken = accessTokens && accessTokens[0];
|
||||||
yield call(setAccessToken, accessTokens[0]);
|
|
||||||
|
if (accessToken) {
|
||||||
|
yield call(setAccessToken, accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(actions.updateUserPassword.success(user));
|
yield put(actions.updateUserPassword.success(user, accessToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* updateCurrentUserPassword(data) {
|
export function* updateCurrentUserPassword(data) {
|
||||||
@@ -215,7 +217,7 @@ export function* handleUserDelete(user) {
|
|||||||
const currentUserId = yield select(selectors.selectCurrentUserId);
|
const currentUserId = yield select(selectors.selectCurrentUserId);
|
||||||
|
|
||||||
if (user.id === currentUserId) {
|
if (user.id === currentUserId) {
|
||||||
yield call(logout);
|
yield call(logout, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(actions.handleUserDelete(user));
|
yield put(actions.handleUserDelete(user));
|
||||||
|
|||||||
@@ -2,18 +2,13 @@ import { all, call, cancel, fork, take } from 'redux-saga/effects';
|
|||||||
|
|
||||||
import watchers from './watchers';
|
import watchers from './watchers';
|
||||||
import services from './services';
|
import services from './services';
|
||||||
import { setAccessToken } from '../../utils/access-token-storage';
|
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
|
|
||||||
export default function* loginSaga() {
|
export default function* loginSaga() {
|
||||||
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
||||||
|
|
||||||
const {
|
yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
||||||
payload: { accessToken },
|
|
||||||
} = yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
|
||||||
|
|
||||||
yield cancel(watcherTasks);
|
yield cancel(watcherTasks);
|
||||||
|
|
||||||
yield call(setAccessToken, accessToken);
|
|
||||||
yield call(services.goToRoot);
|
yield call(services.goToRoot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { call, put } from 'redux-saga/effects';
|
|||||||
|
|
||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
|
import { setAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
|
||||||
export function* authenticate(data) {
|
export function* authenticate(data) {
|
||||||
yield put(actions.authenticate(data));
|
yield put(actions.authenticate(data));
|
||||||
@@ -14,6 +15,7 @@ export function* authenticate(data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield call(setAccessToken, accessToken);
|
||||||
yield put(actions.authenticate.success(accessToken));
|
yield put(actions.authenticate.success(accessToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
selectAccessToken,
|
|
||||||
};
|
|
||||||
@@ -4,8 +4,12 @@ import isUndefined from 'lodash/isUndefined';
|
|||||||
import orm from '../orm';
|
import orm from '../orm';
|
||||||
import Config from '../constants/Config';
|
import Config from '../constants/Config';
|
||||||
|
|
||||||
|
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
||||||
|
|
||||||
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
||||||
|
|
||||||
|
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
|
||||||
|
|
||||||
const nextPosition = (items, index, excludedId) => {
|
const nextPosition = (items, index, excludedId) => {
|
||||||
const filteredItems = isUndefined(excludedId)
|
const filteredItems = isUndefined(excludedId)
|
||||||
? items
|
? items
|
||||||
@@ -94,7 +98,9 @@ export const selectNextTaskPosition = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
selectAccessToken,
|
||||||
selectIsCoreInitializing,
|
selectIsCoreInitializing,
|
||||||
|
selectIsLogouting,
|
||||||
selectNextBoardPosition,
|
selectNextBoardPosition,
|
||||||
selectNextListPosition,
|
selectNextListPosition,
|
||||||
selectNextCardPosition,
|
selectNextCardPosition,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import auth from './auth';
|
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import modals from './modals';
|
import modals from './modals';
|
||||||
import users from './users';
|
import users from './users';
|
||||||
@@ -14,7 +13,6 @@ import attachments from './attachments';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
...router,
|
...router,
|
||||||
...auth,
|
|
||||||
...core,
|
...core,
|
||||||
...modals,
|
...modals,
|
||||||
...users,
|
...users,
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.7",
|
"version": "1.8.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.7",
|
"version": "1.8.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.7",
|
"version": "1.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://plankanban.github.io/planka",
|
"homepage": "https://plankanban.github.io/planka",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -40,24 +40,33 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
|
const remoteAddress = getRemoteAddress(this.req);
|
||||||
|
|
||||||
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
sails.log.warn(
|
sails.log.warn(
|
||||||
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
|
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
|
||||||
this.req,
|
|
||||||
)})`,
|
|
||||||
);
|
);
|
||||||
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
||||||
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
|
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
|
||||||
throw Errors.INVALID_PASSWORD;
|
throw Errors.INVALID_PASSWORD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessToken = sails.helpers.utils.createToken(user.id);
|
||||||
|
|
||||||
|
await Session.create({
|
||||||
|
accessToken,
|
||||||
|
remoteAddress,
|
||||||
|
userId: user.id,
|
||||||
|
userAgent: this.req.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item: sails.helpers.utils.createToken(user.id),
|
item: accessToken,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
async fn() {
|
||||||
|
const { accessToken } = this.req;
|
||||||
|
|
||||||
|
await Session.updateOne({
|
||||||
|
accessToken,
|
||||||
|
deletedAt: null,
|
||||||
|
}).set({
|
||||||
|
deletedAt: new Date().toUTCString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: accessToken,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const zxcvbn = require('zxcvbn');
|
const zxcvbn = require('zxcvbn');
|
||||||
|
|
||||||
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
@@ -71,6 +73,13 @@ module.exports = {
|
|||||||
if (user.id === currentUser.id) {
|
if (user.id === currentUser.id) {
|
||||||
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
|
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
|
||||||
|
|
||||||
|
await Session.create({
|
||||||
|
accessToken,
|
||||||
|
userId: user.id,
|
||||||
|
remoteAddress: getRemoteAddress(this.req),
|
||||||
|
userAgent: this.req.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item: user,
|
item: user,
|
||||||
included: {
|
included: {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { v4: uuid } = require('uuid');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -24,6 +25,9 @@ module.exports = {
|
|||||||
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
|
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
|
||||||
},
|
},
|
||||||
sails.config.session.secret,
|
sails.config.session.secret,
|
||||||
|
{
|
||||||
|
keyid: uuid(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ module.exports = function defineCurrentUserHook(sails) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = await Session.findOne({
|
||||||
|
accessToken,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const user = await sails.helpers.users.getOne(payload.subject);
|
const user = await sails.helpers.users.getOne(payload.subject);
|
||||||
|
|
||||||
if (user && user.passwordChangedAt > payload.issuedAt) {
|
if (user && user.passwordChangedAt > payload.issuedAt) {
|
||||||
@@ -43,8 +52,14 @@ module.exports = function defineCurrentUserHook(sails) {
|
|||||||
|
|
||||||
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
|
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
|
||||||
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, '');
|
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, '');
|
||||||
|
const currentUser = await getUser(accessToken);
|
||||||
|
|
||||||
req.currentUser = await getUser(accessToken);
|
if (currentUser) {
|
||||||
|
Object.assign(req, {
|
||||||
|
accessToken,
|
||||||
|
currentUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
@@ -52,8 +67,17 @@ module.exports = function defineCurrentUserHook(sails) {
|
|||||||
},
|
},
|
||||||
'/attachments/*': {
|
'/attachments/*': {
|
||||||
async fn(req, res, next) {
|
async fn(req, res, next) {
|
||||||
if (req.cookies.accessToken) {
|
const { accessToken } = req.cookies;
|
||||||
req.currentUser = await getUser(req.cookies.accessToken);
|
|
||||||
|
if (accessToken) {
|
||||||
|
const currentUser = await getUser(accessToken);
|
||||||
|
|
||||||
|
if (currentUser) {
|
||||||
|
Object.assign(req, {
|
||||||
|
accessToken,
|
||||||
|
currentUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Session.js
|
||||||
|
*
|
||||||
|
* @description :: A model definition represents a database table/collection.
|
||||||
|
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
attributes: {
|
||||||
|
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||||
|
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||||
|
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||||
|
|
||||||
|
accessToken: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
columnName: 'access_token',
|
||||||
|
},
|
||||||
|
remoteAddress: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
columnName: 'remote_address',
|
||||||
|
},
|
||||||
|
userAgent: {
|
||||||
|
type: 'string',
|
||||||
|
isNotEmptyString: true,
|
||||||
|
allowNull: true,
|
||||||
|
columnName: 'user_agent',
|
||||||
|
},
|
||||||
|
deletedAt: {
|
||||||
|
type: 'ref',
|
||||||
|
columnName: 'deleted_at',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||||
|
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||||
|
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||||
|
|
||||||
|
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||||
|
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||||
|
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||||
|
|
||||||
|
userId: {
|
||||||
|
model: 'User',
|
||||||
|
required: true,
|
||||||
|
columnName: 'user_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
module.exports.routes = {
|
module.exports.routes = {
|
||||||
'POST /api/access-tokens': 'access-tokens/create',
|
'POST /api/access-tokens': 'access-tokens/create',
|
||||||
|
'DELETE /api/access-tokens/me': 'access-tokens/delete',
|
||||||
|
|
||||||
'GET /api/users': 'users/index',
|
'GET /api/users': 'users/index',
|
||||||
'POST /api/users': 'users/create',
|
'POST /api/users': 'users/create',
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
module.exports.up = (knex) =>
|
||||||
|
knex.schema.createTable('session', (table) => {
|
||||||
|
/* Columns */
|
||||||
|
|
||||||
|
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
|
||||||
|
|
||||||
|
table.bigInteger('user_id').notNullable();
|
||||||
|
|
||||||
|
table.text('access_token').notNullable();
|
||||||
|
table.text('remote_address').notNullable();
|
||||||
|
table.text('user_agent');
|
||||||
|
|
||||||
|
table.timestamp('created_at', true);
|
||||||
|
table.timestamp('updated_at', true);
|
||||||
|
table.timestamp('deleted_at', true);
|
||||||
|
|
||||||
|
/* Indexes */
|
||||||
|
|
||||||
|
table.index('user_id');
|
||||||
|
table.unique('access_token');
|
||||||
|
table.index('remote_address');
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.down = (knex) => knex.schema.dropTable('session');
|
||||||
Reference in New Issue
Block a user