Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
447f88880d | ||
|
|
8109936ce2 | ||
|
|
640908320a | ||
|
|
236c1020b3 | ||
|
|
3df07c10fa | ||
|
|
543a992d98 | ||
|
|
9cde4e63f9 | ||
|
|
95738ee7d3 | ||
|
|
bb7cdf5dd4 | ||
|
|
253af55790 | ||
|
|
62c3cca9a7 | ||
|
|
b476a19b79 | ||
|
|
3379c65a87 | ||
|
|
1a6fc40804 | ||
|
|
42092efcd2 | ||
|
|
5c91bddfe7 | ||
|
|
fd8f40f1b0 | ||
|
|
5b64d465e2 | ||
|
|
212fb8ac8c | ||
|
|
cece2254d7 | ||
|
|
c940805f14 | ||
|
|
052ab7f653 | ||
|
|
5a38805588 | ||
|
|
2dbbead2e9 | ||
|
|
039eee0aac | ||
|
|
07867fc0b2 | ||
|
|
55d07b4560 | ||
|
|
289d2bf5ae | ||
|
|
6429e22d59 | ||
|
|
21ef850913 |
@@ -7,4 +7,5 @@
|
|||||||
|
|
||||||
server/public/*
|
server/public/*
|
||||||
server/views/*
|
server/views/*
|
||||||
|
server/logs
|
||||||
client/build
|
client/build
|
||||||
|
|||||||
@@ -44,6 +44,79 @@ docker-compose up -d
|
|||||||
|
|
||||||
Demo user: demo@demo.demo demo
|
Demo user: demo@demo.demo demo
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Planka currently allows you to expose the applciation's logfile directory to the host machine via a shared volume. This feature is not enabled by default.
|
||||||
|
|
||||||
|
To expose the logfile director to the host machine, add the item `./logs/:/app/logs/` under `services.planka.volumes`.
|
||||||
|
|
||||||
|
Note that the directory to the left of the semicolon is regarding the host machine while the directory to the right of the semicolon is regarding the Docker container.
|
||||||
|
|
||||||
|
For example, in the above step, `./logs/:/app/logs/` will create the folder `logs` in the same directory where the `docker-compose.yml` file lives.
|
||||||
|
|
||||||
|
### Rotating Logs
|
||||||
|
|
||||||
|
Logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large.
|
||||||
|
|
||||||
|
#### Setup logrotate for Planka logs
|
||||||
|
|
||||||
|
Create a file in `/etc/logrotate.d` named `planka` with the following contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
/path/to/planka/logs/planka.log {
|
||||||
|
daily
|
||||||
|
missingok
|
||||||
|
rotate 14
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
notifempty
|
||||||
|
create 640 root adm
|
||||||
|
sharedscripts
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure to replace logfile directory with your installation’s `/logs/planka.log` location.
|
||||||
|
|
||||||
|
Restart the logrotate service.
|
||||||
|
|
||||||
|
### Fail2ban
|
||||||
|
|
||||||
|
Fail2ban is a service that uses iptables to automatically drop connections for a pre-defined amount of time from IPs that continuously failed to authenticate to the configured services.
|
||||||
|
|
||||||
|
#### Setup a filter and a jail for Planka
|
||||||
|
|
||||||
|
A filter defines regex rules to identify when users fail to authenticate on Planka's user interface.
|
||||||
|
|
||||||
|
Create a file in `/etc/fail2ban/filter.d` named `planka.conf` with the following contents:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[Definition]
|
||||||
|
failregex = ^(.*) Invalid (email or username:|password!) (\"(.*)\"!)? ?\(IP: <ADDR>\)$
|
||||||
|
ignoreregex =
|
||||||
|
```
|
||||||
|
|
||||||
|
The jail file defines how to handle the failed authentication attempts found by the Planka filter.
|
||||||
|
|
||||||
|
Create a file in `/etc/fail2ban/jail.d` named `planka.local` with the following contents:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[planka]
|
||||||
|
enabled = true
|
||||||
|
port = http,https
|
||||||
|
filter = planka
|
||||||
|
logpath = /path/to/planka/logs/planka.log
|
||||||
|
maxretry = 5
|
||||||
|
bantime = 900
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure to replace `logpath`'s value with your installation’s `/logs/planka.log` location. If you are using ports other than 80 and 443 for your Web server you should replace those too. The bantime and findtime are defined in seconds.
|
||||||
|
|
||||||
|
Restart the fail2ban service. You can check the status of your Planka jail by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fail2ban-client status planka
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Clone the repository and install dependencies:
|
Clone the repository and install dependencies:
|
||||||
|
|||||||
Generated
+12
-1
@@ -44,7 +44,8 @@
|
|||||||
"semantic-ui-react": "^2.1.3",
|
"semantic-ui-react": "^2.1.3",
|
||||||
"socket.io-client": "^2.3.1",
|
"socket.io-client": "^2.3.1",
|
||||||
"validator": "^13.7.0",
|
"validator": "^13.7.0",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
||||||
@@ -25310,6 +25311,11 @@
|
|||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxcvbn": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -43805,6 +43811,11 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
|
||||||
"integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA=="
|
"integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA=="
|
||||||
|
},
|
||||||
|
"zxcvbn": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -101,7 +101,8 @@
|
|||||||
"semantic-ui-react": "^2.1.3",
|
"semantic-ui-react": "^2.1.3",
|
||||||
"socket.io-client": "^2.3.1",
|
"socket.io-client": "^2.3.1",
|
||||||
"validator": "^13.7.0",
|
"validator": "^13.7.0",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ import styles from './BoardMembershipsStep.module.scss';
|
|||||||
const BoardMembershipsStep = React.memo(
|
const BoardMembershipsStep = React.memo(
|
||||||
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [searchValue, handleSearchFieldChange] = useField('');
|
const [search, handleSearchChange] = useField('');
|
||||||
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||||
|
|
||||||
const filteredItems = useMemo(
|
const filteredItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
items.filter(
|
items.filter(
|
||||||
({ user }) =>
|
({ user }) =>
|
||||||
user.email.includes(search) ||
|
user.email.includes(cleanSearch) ||
|
||||||
user.name.toLowerCase().includes(search) ||
|
user.name.toLowerCase().includes(cleanSearch) ||
|
||||||
(user.username && user.username.includes(search)),
|
(user.username && user.username.includes(cleanSearch)),
|
||||||
),
|
),
|
||||||
[items, search],
|
[items, cleanSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchField = useRef(null);
|
const searchField = useRef(null);
|
||||||
@@ -48,15 +48,19 @@ const BoardMembershipsStep = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
<Popup.Header onBack={onBack}>
|
||||||
|
{t(title, {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
ref={searchField}
|
ref={searchField}
|
||||||
value={searchValue}
|
value={search}
|
||||||
placeholder={t('common.searchMembers')}
|
placeholder={t('common.searchMembers')}
|
||||||
icon="search"
|
icon="search"
|
||||||
onChange={handleSearchFieldChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
{filteredItems.length > 0 && (
|
{filteredItems.length > 0 && (
|
||||||
<Menu secondary vertical className={styles.menu}>
|
<Menu secondary vertical className={styles.menu}>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const CardMoveStep = React.memo(
|
|||||||
[selectedBoard, path.listId],
|
[selectedBoard, path.listId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBoardIdFieldChange = useCallback(
|
const handleBoardIdChange = useCallback(
|
||||||
(event, data) => {
|
(event, data) => {
|
||||||
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
|
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
|
||||||
onBoardFetch(data.value);
|
onBoardFetch(data.value);
|
||||||
@@ -102,7 +102,7 @@ const CardMoveStep = React.memo(
|
|||||||
}
|
}
|
||||||
disabled={selectedProject.boards.length === 0}
|
disabled={selectedProject.boards.length === 0}
|
||||||
className={styles.field}
|
className={styles.field}
|
||||||
onChange={handleBoardIdFieldChange}
|
onChange={handleBoardIdChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header>{t('common.notifications')}</Popup.Header>
|
<Popup.Header>
|
||||||
|
{t('common.notifications', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
{items.length > 0
|
{items.length > 0
|
||||||
? items.map((item) => (
|
? items.map((item) => (
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ const LabelsStep = React.memo(
|
|||||||
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
|
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
const [searchValue, handleSearchFieldChange] = useField('');
|
const [search, handleSearchChange] = useField('');
|
||||||
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||||
|
|
||||||
const filteredItems = useMemo(
|
const filteredItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
items.filter(
|
items.filter(
|
||||||
(label) =>
|
(label) =>
|
||||||
(label.name && label.name.toLowerCase().includes(search)) ||
|
(label.name && label.name.toLowerCase().includes(cleanSearch)) ||
|
||||||
label.color.includes(search),
|
label.color.includes(cleanSearch),
|
||||||
),
|
),
|
||||||
[items, search],
|
[items, cleanSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchField = useRef(null);
|
const searchField = useRef(null);
|
||||||
@@ -109,15 +109,19 @@ const LabelsStep = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
<Popup.Header onBack={onBack}>
|
||||||
|
{t(title, {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
ref={searchField}
|
ref={searchField}
|
||||||
value={searchValue}
|
value={search}
|
||||||
placeholder={t('common.searchLabels')}
|
placeholder={t('common.searchLabels')}
|
||||||
icon="search"
|
icon="search"
|
||||||
onChange={handleSearchFieldChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
{filteredItems.length > 0 && (
|
{filteredItems.length > 0 && (
|
||||||
<div className={styles.items}>
|
<div className={styles.items}>
|
||||||
|
|||||||
@@ -17,18 +17,18 @@ const AddStep = React.memo(
|
|||||||
({ users, currentUserIds, permissionsSelectStep, title, onCreate, onClose }) => {
|
({ users, currentUserIds, permissionsSelectStep, title, onCreate, onClose }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
const [searchValue, handleSearchFieldChange] = useField('');
|
const [search, handleSearchChange] = useField('');
|
||||||
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||||
|
|
||||||
const filteredUsers = useMemo(
|
const filteredUsers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
users.filter(
|
users.filter(
|
||||||
(user) =>
|
(user) =>
|
||||||
user.email.includes(search) ||
|
user.email.includes(cleanSearch) ||
|
||||||
user.name.toLowerCase().includes(search) ||
|
user.name.toLowerCase().includes(cleanSearch) ||
|
||||||
(user.username && user.username.includes(search)),
|
(user.username && user.username.includes(cleanSearch)),
|
||||||
),
|
),
|
||||||
[users, search],
|
[users, cleanSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchField = useRef(null);
|
const searchField = useRef(null);
|
||||||
@@ -102,10 +102,10 @@ const AddStep = React.memo(
|
|||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
ref={searchField}
|
ref={searchField}
|
||||||
value={searchValue}
|
value={search}
|
||||||
placeholder={t('common.searchUsers')}
|
placeholder={t('common.searchUsers')}
|
||||||
icon="search"
|
icon="search"
|
||||||
onChange={handleSearchFieldChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
{filteredUsers.length > 0 && (
|
{filteredUsers.length > 0 && (
|
||||||
<div className={styles.users}>
|
<div className={styles.users}>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.menuItem {
|
.menuItem {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
:global(#app) {
|
:global(#app) {
|
||||||
.action {
|
.action {
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
:global(#app) {
|
:global(#app) {
|
||||||
.action {
|
.action {
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { withPopup } from '../../lib/popup';
|
|||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useForm } from '../../hooks';
|
import { useForm } from '../../hooks';
|
||||||
import { isUsername } from '../../utils/validator';
|
import { isPassword, isUsername } from '../../utils/validator';
|
||||||
|
|
||||||
import styles from './UserAddPopup.module.scss';
|
import styles from './UserAddPopup.module.scss';
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ const UserAddStep = React.memo(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cleanData.password) {
|
if (!cleanData.password || !isPassword(cleanData.password)) {
|
||||||
passwordField.current.focus();
|
passwordField.current.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -143,6 +143,7 @@ const UserAddStep = React.memo(
|
|||||||
/>
|
/>
|
||||||
<div className={styles.text}>{t('common.password')}</div>
|
<div className={styles.text}>{t('common.password')}</div>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
|
withStrengthBar
|
||||||
fluid
|
fluid
|
||||||
ref={passwordField}
|
ref={passwordField}
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
|||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useForm } from '../../hooks';
|
import { useForm } from '../../hooks';
|
||||||
|
import { isPassword } from '../../utils/validator';
|
||||||
|
|
||||||
import styles from './UserPasswordEditStep.module.scss';
|
import styles from './UserPasswordEditStep.module.scss';
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ const UserPasswordEditStep = React.memo(
|
|||||||
const currentPasswordField = useRef(null);
|
const currentPasswordField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
if (!data.password) {
|
if (!data.password || !isPassword(data.password)) {
|
||||||
passwordField.current.select();
|
passwordField.current.select();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -113,6 +114,7 @@ const UserPasswordEditStep = React.memo(
|
|||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<div className={styles.text}>{t('common.newPassword')}</div>
|
<div className={styles.text}>{t('common.newPassword')}</div>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
|
withStrengthBar
|
||||||
fluid
|
fluid
|
||||||
ref={passwordField}
|
ref={passwordField}
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
:global(#app) {
|
:global(#app) {
|
||||||
.action {
|
.action {
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
:global(#app) {
|
:global(#app) {
|
||||||
.action {
|
.action {
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -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,22 +1,74 @@
|
|||||||
import React, { useCallback } from 'react';
|
import zxcvbn from 'zxcvbn';
|
||||||
import { Icon, Input } from 'semantic-ui-react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon, Input, Progress } from 'semantic-ui-react';
|
||||||
import { useToggle } from '../../../hooks';
|
import { useToggle } from '../../../hooks';
|
||||||
|
|
||||||
const InputPassword = React.forwardRef((props, ref) => {
|
import styles from './InputPassword.module.css';
|
||||||
const [isVisible, toggleVisible] = useToggle();
|
|
||||||
|
|
||||||
const handleToggleClick = useCallback(() => {
|
const STRENGTH_SCORE_COLORS = ['red', 'orange', 'yellow', 'olive', 'green'];
|
||||||
toggleVisible();
|
|
||||||
}, [toggleVisible]);
|
|
||||||
|
|
||||||
return (
|
const InputPassword = React.forwardRef(
|
||||||
<Input
|
({ value, withStrengthBar, minStrengthScore, className, ...props }, ref) => {
|
||||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
const [isVisible, toggleVisible] = useToggle();
|
||||||
ref={ref}
|
|
||||||
type={isVisible ? 'text' : 'password'}
|
const strengthScore = useMemo(() => {
|
||||||
icon={<Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />}
|
if (!withStrengthBar) {
|
||||||
/>
|
return undefined;
|
||||||
);
|
}
|
||||||
});
|
|
||||||
|
return zxcvbn(value).score;
|
||||||
|
}, [value, withStrengthBar]);
|
||||||
|
|
||||||
|
const handleToggleClick = useCallback(() => {
|
||||||
|
toggleVisible();
|
||||||
|
}, [toggleVisible]);
|
||||||
|
|
||||||
|
const inputProps = {
|
||||||
|
...props,
|
||||||
|
ref,
|
||||||
|
type: isVisible ? 'text' : 'password',
|
||||||
|
icon: <Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!withStrengthBar) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
{...inputProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Input
|
||||||
|
{...inputProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
|
error={!!value && strengthScore < minStrengthScore}
|
||||||
|
/>
|
||||||
|
<Progress
|
||||||
|
value={value ? strengthScore + 1 : 0}
|
||||||
|
total={5}
|
||||||
|
color={STRENGTH_SCORE_COLORS[strengthScore]}
|
||||||
|
size="tiny"
|
||||||
|
className={styles.strengthBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
InputPassword.propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
withStrengthBar: PropTypes.bool,
|
||||||
|
minStrengthScore: PropTypes.number,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
InputPassword.defaultProps = {
|
||||||
|
withStrengthBar: false,
|
||||||
|
minStrengthScore: 2,
|
||||||
|
className: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export default React.memo(InputPassword);
|
export default React.memo(InputPassword);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.strengthBar {
|
||||||
|
margin: 4px 0 0 !important;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import remarkBreaks from 'remark-breaks';
|
|||||||
|
|
||||||
import './Markdown.module.scss'; // FIXME: import as styles?
|
import './Markdown.module.scss'; // FIXME: import as styles?
|
||||||
|
|
||||||
|
const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
|
||||||
|
|
||||||
const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
||||||
const handleLinkClick = useCallback((event) => {
|
const handleLinkClick = useCallback((event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -16,25 +18,30 @@ const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
|
|||||||
jsx-a11y/click-events-have-key-events,
|
jsx-a11y/click-events-have-key-events,
|
||||||
jsx-a11y/no-static-element-interactions,
|
jsx-a11y/no-static-element-interactions,
|
||||||
react/jsx-props-no-spreading */
|
react/jsx-props-no-spreading */
|
||||||
({ node, ...linkProps }) => <a {...linkProps} onClick={handleLinkClick} />,
|
({ node, ...linkProps }) => (
|
||||||
|
<a
|
||||||
|
{...linkProps}
|
||||||
|
rel={
|
||||||
|
ABSOLUTE_URL_REGEX.test(linkProps.href) && linkProps.target === '_blank'
|
||||||
|
? 'noreferrer'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={linkStopPropagation ? handleLinkClick : undefined}
|
||||||
|
/>
|
||||||
|
),
|
||||||
/* eslint-enable jsx-a11y/anchor-has-content,
|
/* eslint-enable jsx-a11y/anchor-has-content,
|
||||||
jsx-a11y/click-events-have-key-events,
|
jsx-a11y/click-events-have-key-events,
|
||||||
jsx-a11y/no-static-element-interactions,
|
jsx-a11y/no-static-element-interactions,
|
||||||
react/jsx-props-no-spreading */
|
react/jsx-props-no-spreading */
|
||||||
[handleLinkClick],
|
[linkStopPropagation, handleLinkClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
let components;
|
|
||||||
if (linkStopPropagation) {
|
|
||||||
components = {
|
|
||||||
a: linkRenderer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
components={components}
|
components={{
|
||||||
|
a: linkRenderer,
|
||||||
|
}}
|
||||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||||
className="markdown-body"
|
className="markdown-body"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Vendored
+5
-5
@@ -14453,11 +14453,11 @@ img.ui.bordered.image {
|
|||||||
---------------------*/
|
---------------------*/
|
||||||
|
|
||||||
.ui.input.error > input {
|
.ui.input.error > input {
|
||||||
background-color: #fff6f6;
|
background-color: #fff6f6 !important;
|
||||||
border-color: #e0b4b4;
|
border-color: #e0b4b4 !important;
|
||||||
color: #9f3a38;
|
color: #9f3a38 !important;
|
||||||
-webkit-box-shadow: none;
|
-webkit-box-shadow: none !important;
|
||||||
box-shadow: none;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error Placeholder */
|
/* Error Placeholder */
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Volby',
|
preferences: 'Volby',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky.',
|
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky',
|
||||||
project: 'Projekt',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt nenalezen',
|
projectNotFound_title: 'Projekt nenalezen',
|
||||||
removeManager_title: 'Odstranit vedoucího',
|
removeManager_title: 'Odstranit vedoucího',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
administrator: 'Administrator',
|
administrator: 'Administrator',
|
||||||
all: 'Alle',
|
all: 'Alle',
|
||||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||||
'Alle Änderungen werden automatisch gespeichert,<br />sobald die Verbindung wiederhergestellt wurde.',
|
'Alle Änderungen werden automatisch gespeichert,<br />sobald die Verbindung wiederhergestellt wurde',
|
||||||
areYouSureYouWantToDeleteThisAttachment:
|
areYouSureYouWantToDeleteThisAttachment:
|
||||||
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
|
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
|
||||||
areYouSureYouWantToDeleteThisBoard: 'Sind Sie sicher, dass Sie dieses Board löschen möchten?',
|
areYouSureYouWantToDeleteThisBoard: 'Sind Sie sicher, dass Sie dieses Board löschen möchten?',
|
||||||
@@ -123,7 +123,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Voreinstellungen',
|
preferences: 'Voreinstellungen',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
|
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen',
|
||||||
project: 'Projekt',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt nicht gefunden',
|
projectNotFound_title: 'Projekt nicht gefunden',
|
||||||
removeManager_title: 'Manager entfernen',
|
removeManager_title: 'Manager entfernen',
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default {
|
|||||||
phone: 'Phone',
|
phone: 'Phone',
|
||||||
preferences: 'Preferences',
|
preferences: 'Preferences',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
|
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard',
|
||||||
project: 'Project',
|
project: 'Project',
|
||||||
projectNotFound_title: 'Project Not Found',
|
projectNotFound_title: 'Project Not Found',
|
||||||
removeManager_title: 'Remove Manager',
|
removeManager_title: 'Remove Manager',
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
phone: 'Teléfono',
|
phone: 'Teléfono',
|
||||||
preferences: 'Preferencias',
|
preferences: 'Preferencias',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles.',
|
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles',
|
||||||
project: 'Proyecto',
|
project: 'Proyecto',
|
||||||
projectNotFound_title: 'Proyecto no encontrado',
|
projectNotFound_title: 'Proyecto no encontrado',
|
||||||
removeMember_title: 'Remover Miembro',
|
removeMember_title: 'Remover Miembro',
|
||||||
@@ -115,7 +115,7 @@ export default {
|
|||||||
selectList: 'Seleccionar Lista',
|
selectList: 'Seleccionar Lista',
|
||||||
selectProject: 'Seleccionar Proyecto',
|
selectProject: 'Seleccionar Proyecto',
|
||||||
settings: 'Configuraciones',
|
settings: 'Configuraciones',
|
||||||
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto.',
|
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto',
|
||||||
taskActions_title: 'Acciones de Tareas',
|
taskActions_title: 'Acciones de Tareas',
|
||||||
tasks: 'Tareas',
|
tasks: 'Tareas',
|
||||||
time: 'Tiempo',
|
time: 'Tiempo',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import de from './de';
|
|||||||
import en from './en';
|
import en from './en';
|
||||||
import es from './es';
|
import es from './es';
|
||||||
import fr from './fr';
|
import fr from './fr';
|
||||||
|
import it from './it';
|
||||||
import ja from './ja';
|
import ja from './ja';
|
||||||
import ko from './ko';
|
import ko from './ko';
|
||||||
import pl from './pl';
|
import pl from './pl';
|
||||||
@@ -13,7 +14,7 @@ import sv from './sv';
|
|||||||
import uz from './uz';
|
import uz from './uz';
|
||||||
import zh from './zh';
|
import zh from './zh';
|
||||||
|
|
||||||
const locales = [cs, da, de, en, es, fr, ja, ko, pl, ru, sk, sv, uz, zh];
|
const locales = [cs, da, de, en, es, fr, it, ja, ko, pl, ru, sk, sv, uz, zh];
|
||||||
|
|
||||||
export default locales;
|
export default locales;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
export default {
|
||||||
|
format: {
|
||||||
|
date: 'd/M/yyyy',
|
||||||
|
time: 'p',
|
||||||
|
dateTime: '$t(format:date) $t(format:time)',
|
||||||
|
longDate: 'MMM d',
|
||||||
|
longDateTime: "MMMM d 'at' p",
|
||||||
|
},
|
||||||
|
|
||||||
|
translation: {
|
||||||
|
common: {
|
||||||
|
account: 'Account',
|
||||||
|
actions: 'Azioni',
|
||||||
|
addAttachment_title: 'Aggiungi Allegato',
|
||||||
|
addComment: 'Aggiungi commento',
|
||||||
|
addManager_title: 'Aggiungi Manager',
|
||||||
|
addMember_title: 'Aggiungi Membro',
|
||||||
|
addUser_title: 'Aggiungi Utente',
|
||||||
|
administrator: 'Amministratore',
|
||||||
|
all: 'tutto',
|
||||||
|
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||||
|
'All changes will be automatically saved<br />after connection restored',
|
||||||
|
areYouSureYouWantToDeleteThisAttachment: 'Sei sicuro di voler eliminare questo allegato?',
|
||||||
|
areYouSureYouWantToDeleteThisBoard: 'Sei sicuro di voler eliminare questa bacheca?',
|
||||||
|
areYouSureYouWantToDeleteThisCard: 'Sei sicuro di voler eliminare questa card?',
|
||||||
|
areYouSureYouWantToDeleteThisComment: 'Sei sicuro di voler eliminare questo commento?',
|
||||||
|
areYouSureYouWantToDeleteThisLabel: 'Sei sicuro di voler eliminare questa etichetta?',
|
||||||
|
areYouSureYouWantToDeleteThisList: 'Sei sicuro di voler eliminare questo elenco?',
|
||||||
|
areYouSureYouWantToDeleteThisProject: 'Sei sicuro di voler eliminare questo progetto?',
|
||||||
|
areYouSureYouWantToDeleteThisTask: 'Sei sicuro di voler eliminare questo task?',
|
||||||
|
areYouSureYouWantToDeleteThisUser: 'Sei sicuro di voler eliminare questo utente?',
|
||||||
|
areYouSureYouWantToLeaveBoard: 'Sei sicuro di voler abbandonare la bacheca?',
|
||||||
|
areYouSureYouWantToLeaveProject: 'Sei sicuro di voler abbandonare il progetto?',
|
||||||
|
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||||
|
'Sei sicuro di voler rimuovere questo amministratore dal progetto?',
|
||||||
|
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||||
|
'Sei sicuro di voler rimuovere questo membro dalla bacheca?',
|
||||||
|
attachment: 'Allegato',
|
||||||
|
attachments: 'Allegati',
|
||||||
|
authentication: 'Autenticazione',
|
||||||
|
background: 'Background',
|
||||||
|
board: 'Bacheca',
|
||||||
|
boardNotFound_title: 'Bacheca non trovata',
|
||||||
|
canComment: 'Può commentare',
|
||||||
|
canEditContentOfBoard: 'Può modificare il contenuto della bacheca',
|
||||||
|
canOnlyViewBoard: 'Può solo vedere la bacheca',
|
||||||
|
cardActions_title: 'Card Azioni',
|
||||||
|
cardNotFound_title: 'Card non trovata',
|
||||||
|
cardOrActionAreDeleted: "La card o l'azione vengono eliminate",
|
||||||
|
color: 'Colore',
|
||||||
|
createBoard_title: 'Crea Bacheca',
|
||||||
|
createLabel_title: 'Crea Etichetta',
|
||||||
|
createNewOneOrSelectExistingOne: 'Crea nuovo o seleziona <br /> esistente',
|
||||||
|
createProject_title: 'Crea Progetto',
|
||||||
|
createTextFile_title: 'Create File di Testo',
|
||||||
|
currentPassword: 'Password corrente',
|
||||||
|
dangerZone_title: 'Danger Zone',
|
||||||
|
date: 'Data',
|
||||||
|
dueDate_title: 'Data di scadenza',
|
||||||
|
deleteAttachment_title: 'Elimina Allegato',
|
||||||
|
deleteBoard_title: 'Elimina Bacheca',
|
||||||
|
deleteCard_title: 'Elimina card',
|
||||||
|
deleteComment_title: 'Elimina Commento',
|
||||||
|
deleteLabel_title: 'Elimina Etichetta',
|
||||||
|
deleteList_title: 'Elimina Elenco',
|
||||||
|
deleteProject_title: 'Elimina Progetto',
|
||||||
|
deleteTask_title: 'Elimina Task',
|
||||||
|
deleteUser_title: 'Elimina Utente',
|
||||||
|
description: 'Descrizione',
|
||||||
|
detectAutomatically: 'Rileva automaticamente',
|
||||||
|
dropFileToUpload: 'Trascina il file da caricare',
|
||||||
|
editor: 'Editor',
|
||||||
|
editAttachment_title: 'Modifica Allegato',
|
||||||
|
editAvatar_title: 'Modifica Avatar',
|
||||||
|
editBoard_title: 'Modifica Board',
|
||||||
|
editDueDate_title: 'Modifica Data di Scadenza',
|
||||||
|
editEmail_title: 'Modifica E-mail',
|
||||||
|
editInformation_title: 'Modifica Informazioni',
|
||||||
|
editLabel_title: 'Modifica Etichetta',
|
||||||
|
editPassword_title: 'Modifica Password',
|
||||||
|
editPermissions_title: 'Modifica Permessi',
|
||||||
|
editTimer_title: 'Modifica Timer',
|
||||||
|
editUsername_title: 'Modifica Username',
|
||||||
|
email: 'E-mail',
|
||||||
|
emailAlreadyInUse: 'E-mail già in uso',
|
||||||
|
enterCardTitle: 'Inserire titolo della card...',
|
||||||
|
enterDescription: 'Inserire descrizione...',
|
||||||
|
enterFilename: 'Inserire nome del file',
|
||||||
|
enterListTitle: 'Inserire titolo della lista...',
|
||||||
|
enterProjectTitle: 'Inserire titolo del progetto',
|
||||||
|
enterTaskDescription: 'Inserire descrizione del task...',
|
||||||
|
filterByLabels_title: 'Filtra per Etichetta',
|
||||||
|
filterByMembers_title: 'Filtra per Membro',
|
||||||
|
fromComputer_title: 'Dal Computer',
|
||||||
|
general: 'Generale',
|
||||||
|
hours: 'Ore',
|
||||||
|
invalidCurrentPassword: 'Password corrente non valida',
|
||||||
|
labels: 'Etichette',
|
||||||
|
language: 'Lingua',
|
||||||
|
leaveBoard_title: 'Lascia Bacheca',
|
||||||
|
leaveProject_title: 'Lascia Progetto',
|
||||||
|
list: 'Lista',
|
||||||
|
listActions_title: 'Lista delle Azioni',
|
||||||
|
managers: 'Managers',
|
||||||
|
members: 'Membri',
|
||||||
|
minutes: 'Minuti',
|
||||||
|
moveCard_title: 'Sposta card',
|
||||||
|
name: 'Nome',
|
||||||
|
newEmail: 'Nuova e-mail',
|
||||||
|
newPassword: 'Nuova password',
|
||||||
|
newUsername: 'Nuovo username',
|
||||||
|
noConnectionToServer: 'Nessuna connessione al server',
|
||||||
|
noBoards: 'No bacheche',
|
||||||
|
noLists: 'No liste',
|
||||||
|
noProjects: 'No progetti',
|
||||||
|
notifications: 'Notifiche',
|
||||||
|
noUnreadNotifications: 'Nessuna notifica non letta',
|
||||||
|
openBoard_title: 'Bacheca aperta',
|
||||||
|
optional_inline: 'opzionale',
|
||||||
|
organization: 'Organizazzione',
|
||||||
|
phone: 'Telefono',
|
||||||
|
preferences: 'Preferenze',
|
||||||
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
|
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard',
|
||||||
|
project: 'Progetto',
|
||||||
|
projectNotFound_title: 'Progetto non trovato',
|
||||||
|
removeManager_title: 'Rimuovi Manager',
|
||||||
|
removeMember_title: 'Rimuovi Membro',
|
||||||
|
searchLabels: 'Cerca etichette...',
|
||||||
|
searchMembers: 'Cerca membri...',
|
||||||
|
searchUsers: 'Cerca utenti...',
|
||||||
|
seconds: 'Secondi',
|
||||||
|
selectBoard: 'Seleziona bacheca',
|
||||||
|
selectList: 'Seleziona lista',
|
||||||
|
selectPermissions_title: 'Seleziona Permessi',
|
||||||
|
selectProject: 'Seleziona progetto',
|
||||||
|
settings: 'Impostazioni',
|
||||||
|
subscribeToMyOwnCardsByDefault: 'Abbonati alle mie card per impostazione predefinita',
|
||||||
|
taskActions_title: 'Task Azioni',
|
||||||
|
tasks: 'Tasks',
|
||||||
|
thereIsNoPreviewAvailableForThisAttachment:
|
||||||
|
'Non è disponibile alcuna anteprima per questo allegato',
|
||||||
|
time: 'Tempo',
|
||||||
|
timer: 'Timer',
|
||||||
|
title: 'Titolo',
|
||||||
|
userActions_title: 'Azioni Utente',
|
||||||
|
userAddedThisCardToList: '<0>{{user}}</0><1> ha aggiunto questa card a {{list}}</1>',
|
||||||
|
userLeftNewCommentToCard:
|
||||||
|
'{{user}} ha lasciato un nuovo commento «{{comment}}» a <2>{{card}}</2>',
|
||||||
|
userMovedCardFromListToList: '{{user}} ha mosso <2>{{card}}</2> da {{fromList}} a {{toList}}',
|
||||||
|
userMovedThisCardFromListToList:
|
||||||
|
'<0>{{user}}</0><1> ha mosso questa card da {{fromList}} a {{toList}}</1>',
|
||||||
|
username: 'Username',
|
||||||
|
usernameAlreadyInUse: 'Username già in uso',
|
||||||
|
users: 'Utenti',
|
||||||
|
viewer: 'Viewer',
|
||||||
|
writeComment: 'Scrivi un commento...',
|
||||||
|
},
|
||||||
|
|
||||||
|
action: {
|
||||||
|
addAnotherCard: "Aggiungi un'altra card",
|
||||||
|
addAnotherList: "Aggiungi un'altra lista",
|
||||||
|
addAnotherTask: 'Aggiungi un altro task',
|
||||||
|
addCard: 'Aggiungi card',
|
||||||
|
addCard_title: 'Aggiungi Card',
|
||||||
|
addComment: 'Aggiungi commento',
|
||||||
|
addList: 'Aggiungi lista',
|
||||||
|
addMember: 'Add membro',
|
||||||
|
addMoreDetailedDescription: 'Aggiungi una descrizione più dettagliata',
|
||||||
|
addTask: 'Aggiungi task',
|
||||||
|
addToCard: 'Aggiungi alla card',
|
||||||
|
addUser: 'Aggiungi utente',
|
||||||
|
createBoard: 'Crea bacheca',
|
||||||
|
createFile: 'Crea file',
|
||||||
|
createLabel: 'Crea etichetta',
|
||||||
|
createNewLabel: 'Crea nuova etichetta',
|
||||||
|
createProject: 'Crea progetto',
|
||||||
|
delete: 'Elimina',
|
||||||
|
deleteAttachment: 'Elimina allegato',
|
||||||
|
deleteAvatar: 'Elimina avatar',
|
||||||
|
deleteBoard: 'Elimina bacheca',
|
||||||
|
deleteCard: 'Elimina card',
|
||||||
|
deleteCard_title: 'Elimina Card',
|
||||||
|
deleteComment: 'Elimina commento',
|
||||||
|
deleteImage: 'Elimina immagine',
|
||||||
|
deleteLabel: 'Elimina etichetta',
|
||||||
|
deleteList: 'Elimina lista',
|
||||||
|
deleteList_title: 'Elimina Lista',
|
||||||
|
deleteProject: 'Elimina progetto',
|
||||||
|
deleteProject_title: 'Elimina Progetto',
|
||||||
|
deleteTask: 'Elimina task',
|
||||||
|
deleteTask_title: 'Elimina Task',
|
||||||
|
deleteUser: 'Elimina utente',
|
||||||
|
edit: 'Modifica',
|
||||||
|
editDueDate_title: 'Modifica data di scadenza',
|
||||||
|
editDescription_title: 'Modifica Descrizione',
|
||||||
|
editEmail_title: 'Modifica E-mail',
|
||||||
|
editInformation_title: 'Modifca Informazioni',
|
||||||
|
editPassword_title: 'Modifica Password',
|
||||||
|
editPermissions: 'Modifica permessi',
|
||||||
|
editTimer_title: 'Modifica Timer',
|
||||||
|
editTitle_title: 'Modifica Titolo',
|
||||||
|
editUsername_title: 'Modifica Username',
|
||||||
|
hideDetails: 'Nascondi dettagli',
|
||||||
|
leaveBoard: 'Lascia bacheca',
|
||||||
|
leaveProject: 'Lascia progetto',
|
||||||
|
logOut_title: 'Log Out',
|
||||||
|
makeCover_title: 'Crea Cover',
|
||||||
|
move: 'Muovi',
|
||||||
|
moveCard_title: 'Muovi Card',
|
||||||
|
remove: 'Rimuovi',
|
||||||
|
removeBackground: 'Rimuovi background',
|
||||||
|
removeCover_title: 'Rimuovi Cover',
|
||||||
|
removeFromBoard: 'Rimuovi dalla bacheca',
|
||||||
|
removeFromProject: 'Rimuovi dal Progetto',
|
||||||
|
removeManager: 'Rimuovi manager',
|
||||||
|
removeMember: 'Rimuovi membro',
|
||||||
|
save: 'Salva',
|
||||||
|
showAllAttachments: 'Mostra tutti gli allegati ({{hidden}} hidden)',
|
||||||
|
showDetails: 'Mostra dettagli',
|
||||||
|
showFewerAttachments: 'Mostra meno allegati',
|
||||||
|
start: 'Start',
|
||||||
|
stop: 'Stop',
|
||||||
|
subscribe: 'Subscribe',
|
||||||
|
unsubscribe: 'Unsubscribe',
|
||||||
|
uploadNewAvatar: 'Carica nuovo avatar',
|
||||||
|
uploadNewImage: 'Carica nuova immagine',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import login from './login';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
language: 'it',
|
||||||
|
country: 'it',
|
||||||
|
name: 'Italiano',
|
||||||
|
embeddedLocale: login,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
translation: {
|
||||||
|
common: {
|
||||||
|
emailOrUsername: 'E-mail o username',
|
||||||
|
invalidEmailOrUsername: 'E-mail or username invalido',
|
||||||
|
invalidPassword: 'Password non valida',
|
||||||
|
logInToPlanka: 'Log in Planka',
|
||||||
|
noInternetConnection: 'Nessuna connessione internet',
|
||||||
|
pageNotFound_title: 'Pagina non trovata',
|
||||||
|
password: 'Password',
|
||||||
|
projectManagement: 'Project management',
|
||||||
|
serverConnectionFailed: 'Connesione al server fallita',
|
||||||
|
unknownError: 'Errore sconosciuto, prova ancora',
|
||||||
|
},
|
||||||
|
|
||||||
|
action: {
|
||||||
|
logIn: 'Log in',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -117,7 +117,7 @@ export default {
|
|||||||
phone: '電話番号',
|
phone: '電話番号',
|
||||||
preferences: '環境設定',
|
preferences: '環境設定',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します。',
|
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します',
|
||||||
project: 'プロジェクト',
|
project: 'プロジェクト',
|
||||||
projectNotFound_title: 'プロジェクトがありません',
|
projectNotFound_title: 'プロジェクトがありません',
|
||||||
removeManager_title: 'マネージャーを削除',
|
removeManager_title: 'マネージャーを削除',
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Preferencje',
|
preferences: 'Preferencje',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka.',
|
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka',
|
||||||
project: 'Projekt',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt Nie Znaleziony',
|
projectNotFound_title: 'Projekt Nie Znaleziony',
|
||||||
removeMember_title: 'Usuń Członka',
|
removeMember_title: 'Usuń Członka',
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default {
|
|||||||
phone: 'Телефон',
|
phone: 'Телефон',
|
||||||
preferences: 'Предпочтения',
|
preferences: 'Предпочтения',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена.',
|
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена',
|
||||||
project: 'Проект',
|
project: 'Проект',
|
||||||
projectActions: 'Действия с проектом',
|
projectActions: 'Действия с проектом',
|
||||||
projectNotFound: 'Проект не найден',
|
projectNotFound: 'Проект не найден',
|
||||||
@@ -137,7 +137,7 @@ export default {
|
|||||||
time: 'Время',
|
time: 'Время',
|
||||||
timer: 'Таймер',
|
timer: 'Таймер',
|
||||||
title: 'Название',
|
title: 'Название',
|
||||||
userActions_title: 'Действия с пользователем',
|
userActions: 'Действия с пользователем',
|
||||||
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>',
|
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>',
|
||||||
userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>',
|
userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>',
|
||||||
userMovedCardFromListToList:
|
userMovedCardFromListToList:
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default {
|
|||||||
phone: 'Telefón',
|
phone: 'Telefón',
|
||||||
preferences: 'Voľby',
|
preferences: 'Voľby',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky.',
|
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky',
|
||||||
project: 'Projekt',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt neexistuje',
|
projectNotFound_title: 'Projekt neexistuje',
|
||||||
removeManager_title: 'Odstrániť správcu',
|
removeManager_title: 'Odstrániť správcu',
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Preferenser',
|
preferences: 'Preferenser',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp.',
|
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp',
|
||||||
project: 'Projekt',
|
project: 'Projekt',
|
||||||
projectNotFound_title: 'Projekt Ej Funnet',
|
projectNotFound_title: 'Projekt Ej Funnet',
|
||||||
removeManager_title: 'Ta Bort Projektledare',
|
removeManager_title: 'Ta Bort Projektledare',
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default {
|
|||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
preferences: 'Afzalliklar',
|
preferences: 'Afzalliklar',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing.",
|
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing",
|
||||||
project: 'Loyiha',
|
project: 'Loyiha',
|
||||||
projectNotFound_title: 'Loyiha Topilmadi',
|
projectNotFound_title: 'Loyiha Topilmadi',
|
||||||
removeManager_title: "Boshqaruvchini O'chirish",
|
removeManager_title: "Boshqaruvchini O'chirish",
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default {
|
|||||||
phone: '电话',
|
phone: '电话',
|
||||||
preferences: '偏好',
|
preferences: '偏好',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件.',
|
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件',
|
||||||
project: '项目',
|
project: '项目',
|
||||||
projectNotFound_title: '项目未找到',
|
projectNotFound_title: '项目未找到',
|
||||||
removeManager_title: '删除管理员',
|
removeManager_title: '删除管理员',
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function* handleBoardMembershipCreate(boardMembership) {
|
|||||||
|
|
||||||
let body;
|
let body;
|
||||||
try {
|
try {
|
||||||
body = yield call(requests.fetchBoardByCurrentPathRequest);
|
body = yield call(requests.fetchBoardByCurrentPath);
|
||||||
} catch {} // eslint-disable-line no-empty
|
} catch {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
if (body && body.project && body.project.id === board2.projectId) {
|
if (body && body.project && body.project.id === board2.projectId) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import zxcvbn from 'zxcvbn';
|
||||||
|
|
||||||
const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
|
const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
export const isPassword = (string) => zxcvbn(string).score >= 2; // TODO: move to config
|
||||||
export const isUsername = (string) => {
|
|
||||||
return string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
|
export const isUsername = (string) =>
|
||||||
};
|
string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://plankanban.github.io/planka",
|
"homepage": "https://plankanban.github.io/planka",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -143,3 +143,5 @@ private/attachments/*
|
|||||||
|
|
||||||
views/*
|
views/*
|
||||||
!views/.gitkeep
|
!views/.gitkeep
|
||||||
|
|
||||||
|
logs
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
|
|
||||||
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
INVALID_EMAIL_OR_USERNAME: {
|
INVALID_EMAIL_OR_USERNAME: {
|
||||||
invalidEmailOrUsername: 'Invalid email or username',
|
invalidEmailOrUsername: 'Invalid email or username',
|
||||||
@@ -38,18 +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(
|
||||||
|
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
|
||||||
|
);
|
||||||
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: ${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,3 +1,6 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
NOT_ENOUGH_RIGHTS: {
|
NOT_ENOUGH_RIGHTS: {
|
||||||
notEnoughRights: 'Not enough rights',
|
notEnoughRights: 'Not enough rights',
|
||||||
@@ -5,6 +8,9 @@ const Errors = {
|
|||||||
CARD_NOT_FOUND: {
|
CARD_NOT_FOUND: {
|
||||||
cardNotFound: 'Card not found',
|
cardNotFound: 'Card not found',
|
||||||
},
|
},
|
||||||
|
NO_FILE_WAS_UPLOADED: {
|
||||||
|
noFileWasUploaded: 'No file was uploaded',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -27,6 +33,9 @@ module.exports = {
|
|||||||
cardNotFound: {
|
cardNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
noFileWasUploaded: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
uploadError: {
|
uploadError: {
|
||||||
responseType: 'unprocessableEntity',
|
responseType: 'unprocessableEntity',
|
||||||
},
|
},
|
||||||
@@ -52,33 +61,37 @@ module.exports = {
|
|||||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.req
|
const upload = util.promisify((options, callback) =>
|
||||||
.file('file')
|
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||||
.upload(sails.helpers.utils.createAttachmentReceiver(), async (error, files) => {
|
);
|
||||||
if (error) {
|
|
||||||
return exits.uploadError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
let files;
|
||||||
return exits.uploadError('No file was uploaded');
|
try {
|
||||||
}
|
files = await upload({
|
||||||
|
saveAs: uuid(),
|
||||||
const file = files[0];
|
maxBytes: null,
|
||||||
|
|
||||||
const attachment = await sails.helpers.attachments.createOne(
|
|
||||||
{
|
|
||||||
...file.extra,
|
|
||||||
filename: file.filename,
|
|
||||||
},
|
|
||||||
currentUser,
|
|
||||||
card,
|
|
||||||
inputs.requestId,
|
|
||||||
this.req,
|
|
||||||
);
|
|
||||||
|
|
||||||
return exits.success({
|
|
||||||
item: attachment.toJSON(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = _.last(files);
|
||||||
|
const fileData = await sails.helpers.attachments.processUploadedFile(file);
|
||||||
|
|
||||||
|
const attachment = await sails.helpers.attachments.createOne(
|
||||||
|
fileData,
|
||||||
|
currentUser,
|
||||||
|
card,
|
||||||
|
inputs.requestId,
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: attachment,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs, exits) {
|
async fn(inputs) {
|
||||||
const { currentUser } = this.req;
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
const { board } = await sails.helpers.boards
|
const { board } = await sails.helpers.boards
|
||||||
@@ -61,7 +61,7 @@ module.exports = {
|
|||||||
const tasks = await sails.helpers.cards.getTasks(cardIds);
|
const tasks = await sails.helpers.cards.getTasks(cardIds);
|
||||||
const attachments = await sails.helpers.cards.getAttachments(cardIds);
|
const attachments = await sails.helpers.cards.getAttachments(cardIds);
|
||||||
|
|
||||||
return exits.success({
|
return {
|
||||||
items: cards,
|
items: cards,
|
||||||
included: {
|
included: {
|
||||||
cardMemberships,
|
cardMemberships,
|
||||||
@@ -69,6 +69,6 @@ module.exports = {
|
|||||||
tasks,
|
tasks,
|
||||||
attachments,
|
attachments,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
PROJECT_NOT_FOUND: {
|
PROJECT_NOT_FOUND: {
|
||||||
projectNotFound: 'Project not found',
|
projectNotFound: 'Project not found',
|
||||||
},
|
},
|
||||||
|
NO_FILE_WAS_UPLOADED: {
|
||||||
|
noFileWasUploaded: 'No file was uploaded',
|
||||||
|
},
|
||||||
|
FILE_IS_NOT_IMAGE: {
|
||||||
|
fileIsNotImage: 'File is not image',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -17,6 +27,12 @@ module.exports = {
|
|||||||
projectNotFound: {
|
projectNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
noFileWasUploaded: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
|
fileIsNotImage: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
uploadError: {
|
uploadError: {
|
||||||
responseType: 'unprocessableEntity',
|
responseType: 'unprocessableEntity',
|
||||||
},
|
},
|
||||||
@@ -37,32 +53,52 @@ module.exports = {
|
|||||||
throw Errors.PROJECT_NOT_FOUND; // Forbidden
|
throw Errors.PROJECT_NOT_FOUND; // Forbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
this.req
|
const upload = util.promisify((options, callback) =>
|
||||||
.file('file')
|
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||||
.upload(sails.helpers.utils.createProjectBackgroundImageReceiver(), async (error, files) => {
|
);
|
||||||
if (error) {
|
|
||||||
return exits.uploadError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
let files;
|
||||||
return exits.uploadError('No file was uploaded');
|
try {
|
||||||
}
|
files = await upload({
|
||||||
|
saveAs: uuid(),
|
||||||
project = await sails.helpers.projects.updateOne(
|
maxBytes: null,
|
||||||
project,
|
|
||||||
{
|
|
||||||
backgroundImageDirname: files[0].extra.dirname,
|
|
||||||
},
|
|
||||||
this.req,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw Errors.PROJECT_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
return exits.success({
|
|
||||||
item: project.toJSON(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = _.last(files);
|
||||||
|
|
||||||
|
const fileData = await sails.helpers.projects
|
||||||
|
.processUploadedBackgroundImageFile(file)
|
||||||
|
.intercept('fileIsNotImage', () => {
|
||||||
|
try {
|
||||||
|
rimraf.sync(file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors.FILE_IS_NOT_IMAGE;
|
||||||
|
});
|
||||||
|
|
||||||
|
project = await sails.helpers.projects.updateOne(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
backgroundImageDirname: fileData.dirname,
|
||||||
|
},
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw Errors.PROJECT_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: project,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const zxcvbn = require('zxcvbn');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
EMAIL_ALREADY_IN_USE: {
|
EMAIL_ALREADY_IN_USE: {
|
||||||
emailAlreadyInUse: 'Email already in use',
|
emailAlreadyInUse: 'Email already in use',
|
||||||
@@ -16,6 +18,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
},
|
},
|
||||||
|
NO_FILE_WAS_UPLOADED: {
|
||||||
|
noFileWasUploaded: 'No file was uploaded',
|
||||||
|
},
|
||||||
|
FILE_IS_NOT_IMAGE: {
|
||||||
|
fileIsNotImage: 'File is not image',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -17,6 +27,12 @@ module.exports = {
|
|||||||
userNotFound: {
|
userNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
noFileWasUploaded: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
|
fileIsNotImage: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
uploadError: {
|
uploadError: {
|
||||||
responseType: 'unprocessableEntity',
|
responseType: 'unprocessableEntity',
|
||||||
},
|
},
|
||||||
@@ -38,33 +54,53 @@ module.exports = {
|
|||||||
user = currentUser;
|
user = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.req
|
const upload = util.promisify((options, callback) =>
|
||||||
.file('file')
|
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||||
.upload(sails.helpers.utils.createUserAvatarReceiver(), async (error, files) => {
|
);
|
||||||
if (error) {
|
|
||||||
return exits.uploadError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
let files;
|
||||||
return exits.uploadError('No file was uploaded');
|
try {
|
||||||
}
|
files = await upload({
|
||||||
|
saveAs: uuid(),
|
||||||
user = await sails.helpers.users.updateOne(
|
maxBytes: null,
|
||||||
user,
|
|
||||||
{
|
|
||||||
avatarDirname: files[0].extra.dirname,
|
|
||||||
},
|
|
||||||
currentUser,
|
|
||||||
this.req,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw Errors.USER_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
return exits.success({
|
|
||||||
item: user.toJSON(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = _.last(files);
|
||||||
|
|
||||||
|
const fileData = await sails.helpers.users
|
||||||
|
.processUploadedAvatarFile(file)
|
||||||
|
.intercept('fileIsNotImage', () => {
|
||||||
|
try {
|
||||||
|
rimraf.sync(file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors.FILE_IS_NOT_IMAGE;
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await sails.helpers.users.updateOne(
|
||||||
|
user,
|
||||||
|
{
|
||||||
|
avatarDirname: fileData.dirname,
|
||||||
|
},
|
||||||
|
currentUser,
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: user,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
const zxcvbn = require('zxcvbn');
|
||||||
|
|
||||||
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
@@ -18,6 +21,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
currentPassword: {
|
currentPassword: {
|
||||||
@@ -69,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: {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const moveFile = require('move-file');
|
||||||
|
const filenamify = require('filenamify');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
file: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const dirname = uuid();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/sindresorhus/filenamify/issues/13
|
||||||
|
const filename = filenamify(inputs.file.filename);
|
||||||
|
|
||||||
|
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
||||||
|
const filePath = path.join(rootPath, filename);
|
||||||
|
|
||||||
|
fs.mkdirSync(rootPath);
|
||||||
|
await moveFile(inputs.file.fd, filePath);
|
||||||
|
|
||||||
|
const image = sharp(filePath);
|
||||||
|
let metadata;
|
||||||
|
|
||||||
|
try {
|
||||||
|
metadata = await image.metadata();
|
||||||
|
} catch (error) {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
|
const fileData = {
|
||||||
|
dirname,
|
||||||
|
filename,
|
||||||
|
image: null,
|
||||||
|
name: inputs.file.filename,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
|
||||||
|
const thumbnailsPath = path.join(rootPath, 'thumbnails');
|
||||||
|
fs.mkdirSync(thumbnailsPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image
|
||||||
|
.resize(
|
||||||
|
metadata.height > metadata.width
|
||||||
|
? {
|
||||||
|
width: 256,
|
||||||
|
height: 320,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width: 256,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(thumbnailsPath, 'cover-256.jpg'));
|
||||||
|
|
||||||
|
fileData.image = _.pick(metadata, ['width', 'height']);
|
||||||
|
} catch (error1) {
|
||||||
|
try {
|
||||||
|
rimraf.sync(thumbnailsPath);
|
||||||
|
} catch (error2) {
|
||||||
|
console.warn(error2.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileData;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
file: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
fileIsNotImage: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const image = sharp(inputs.file.fd);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image.metadata();
|
||||||
|
} catch (error) {
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirname = uuid();
|
||||||
|
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
|
||||||
|
|
||||||
|
fs.mkdirSync(rootPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image.jpeg().toFile(path.join(rootPath, 'original.jpg'));
|
||||||
|
|
||||||
|
await image
|
||||||
|
.resize(336, 200)
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(rootPath, 'cover-336.jpg'));
|
||||||
|
} catch (error1) {
|
||||||
|
try {
|
||||||
|
rimraf.sync(rootPath);
|
||||||
|
} catch (error2) {
|
||||||
|
console.warn(error2.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rimraf.sync(inputs.file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dirname,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
file: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
fileIsNotImage: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const image = sharp(inputs.file.fd);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image.metadata();
|
||||||
|
} catch (error) {
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirname = uuid();
|
||||||
|
const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname);
|
||||||
|
|
||||||
|
fs.mkdirSync(rootPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await image
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(rootPath, 'original.jpg'));
|
||||||
|
|
||||||
|
await image
|
||||||
|
.resize(100, 100)
|
||||||
|
.jpeg({
|
||||||
|
quality: 100,
|
||||||
|
chromaSubsampling: '4:4:4',
|
||||||
|
})
|
||||||
|
.toFile(path.join(rootPath, 'square-100.jpg'));
|
||||||
|
} catch (error1) {
|
||||||
|
try {
|
||||||
|
rimraf.sync(rootPath);
|
||||||
|
} catch (error2) {
|
||||||
|
console.warn(error2.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'fileIsNotImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rimraf.sync(inputs.file.fd);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dirname,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const util = require('util');
|
|
||||||
const stream = require('stream');
|
|
||||||
const streamToArray = require('stream-to-array');
|
|
||||||
const filenamify = require('filenamify');
|
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
|
||||||
|
|
||||||
const writeFile = util.promisify(fs.writeFile);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sync: true,
|
|
||||||
|
|
||||||
fn() {
|
|
||||||
const receiver = stream.Writable({
|
|
||||||
objectMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let firstFileHandled = false;
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
receiver._write = async (file, receiverEncoding, done) => {
|
|
||||||
if (firstFileHandled) {
|
|
||||||
file.pipe(new stream.Writable());
|
|
||||||
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
firstFileHandled = true;
|
|
||||||
|
|
||||||
const buffer = await streamToArray(file).then((parts) =>
|
|
||||||
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dirname = uuid();
|
|
||||||
|
|
||||||
// FIXME: https://github.com/sindresorhus/filenamify/issues/13
|
|
||||||
const filename = filenamify(file.filename);
|
|
||||||
|
|
||||||
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
|
||||||
fs.mkdirSync(rootPath);
|
|
||||||
|
|
||||||
await writeFile(path.join(rootPath, filename), buffer);
|
|
||||||
|
|
||||||
const image = sharp(buffer);
|
|
||||||
let metadata;
|
|
||||||
|
|
||||||
try {
|
|
||||||
metadata = await image.metadata();
|
|
||||||
} catch (error) {} // eslint-disable-line no-empty
|
|
||||||
|
|
||||||
const extra = {
|
|
||||||
dirname,
|
|
||||||
name: file.filename,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
|
|
||||||
let cover256Buffer;
|
|
||||||
if (metadata.height > metadata.width) {
|
|
||||||
cover256Buffer = await image
|
|
||||||
.resize(256, 320)
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
} else {
|
|
||||||
cover256Buffer = await image
|
|
||||||
.resize({
|
|
||||||
width: 256,
|
|
||||||
})
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailsPath = path.join(rootPath, 'thumbnails');
|
|
||||||
fs.mkdirSync(thumbnailsPath);
|
|
||||||
|
|
||||||
await writeFile(path.join(thumbnailsPath, 'cover-256.jpg'), cover256Buffer);
|
|
||||||
|
|
||||||
extra.image = _.pick(metadata, ['width', 'height']);
|
|
||||||
} else {
|
|
||||||
extra.image = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(file, {
|
|
||||||
filename,
|
|
||||||
extra,
|
|
||||||
});
|
|
||||||
|
|
||||||
return done();
|
|
||||||
} catch (error) {
|
|
||||||
return done(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return receiver;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const util = require('util');
|
|
||||||
const stream = require('stream');
|
|
||||||
const streamToArray = require('stream-to-array');
|
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
|
||||||
|
|
||||||
const writeFile = util.promisify(fs.writeFile);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sync: true,
|
|
||||||
|
|
||||||
fn() {
|
|
||||||
const receiver = stream.Writable({
|
|
||||||
objectMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let firstFileHandled = false;
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
receiver._write = async (file, receiverEncoding, done) => {
|
|
||||||
if (firstFileHandled) {
|
|
||||||
file.pipe(new stream.Writable());
|
|
||||||
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
firstFileHandled = true;
|
|
||||||
|
|
||||||
const buffer = await streamToArray(file).then((parts) =>
|
|
||||||
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const originalBuffer = await sharp(buffer).jpeg().toBuffer();
|
|
||||||
|
|
||||||
const cover336Buffer = await sharp(buffer)
|
|
||||||
.resize(336, 200)
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const dirname = uuid();
|
|
||||||
|
|
||||||
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
|
|
||||||
fs.mkdirSync(rootPath);
|
|
||||||
|
|
||||||
await writeFile(path.join(rootPath, 'original.jpg'), originalBuffer);
|
|
||||||
await writeFile(path.join(rootPath, 'cover-336.jpg'), cover336Buffer);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
file.extra = {
|
|
||||||
dirname,
|
|
||||||
};
|
|
||||||
|
|
||||||
return done();
|
|
||||||
} catch (error) {
|
|
||||||
return done(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return receiver;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const util = require('util');
|
|
||||||
const stream = require('stream');
|
|
||||||
const streamToArray = require('stream-to-array');
|
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
|
||||||
|
|
||||||
const writeFile = util.promisify(fs.writeFile);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sync: true,
|
|
||||||
|
|
||||||
fn() {
|
|
||||||
const receiver = stream.Writable({
|
|
||||||
objectMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let firstFileHandled = false;
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
receiver._write = async (file, receiverEncoding, done) => {
|
|
||||||
if (firstFileHandled) {
|
|
||||||
file.pipe(new stream.Writable());
|
|
||||||
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
firstFileHandled = true;
|
|
||||||
|
|
||||||
const buffer = await streamToArray(file).then((parts) =>
|
|
||||||
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const originalBuffer = await sharp(buffer)
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const square100Buffer = await sharp(buffer)
|
|
||||||
.resize(100, 100)
|
|
||||||
.jpeg({
|
|
||||||
quality: 100,
|
|
||||||
chromaSubsampling: '4:4:4',
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const dirname = uuid();
|
|
||||||
|
|
||||||
const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname);
|
|
||||||
fs.mkdirSync(rootPath);
|
|
||||||
|
|
||||||
await writeFile(path.join(rootPath, 'original.jpg'), originalBuffer);
|
|
||||||
await writeFile(path.join(rootPath, 'square-100.jpg'), square100Buffer);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
file.extra = {
|
|
||||||
dirname,
|
|
||||||
};
|
|
||||||
|
|
||||||
return done();
|
|
||||||
} catch (error) {
|
|
||||||
return done(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return receiver;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Vendored
+19
-2
@@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
|
||||||
|
const { customLogger } = require('../../utils/logger');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -244,7 +246,22 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
log: {
|
log: {
|
||||||
level: 'debug',
|
/**
|
||||||
|
* Passthrough plain log message(s) to
|
||||||
|
* custom Winston console and file logger.
|
||||||
|
*
|
||||||
|
* Note that Winston's log levels override Sails' log levels.
|
||||||
|
* Refer: https://github.com/winstonjs/winston#logging
|
||||||
|
*/
|
||||||
|
|
||||||
|
inspect: false,
|
||||||
|
custom: customLogger,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the Sail.js init success logs (ASCII ship art).
|
||||||
|
*/
|
||||||
|
|
||||||
|
noShip: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
http: {
|
http: {
|
||||||
@@ -273,7 +290,7 @@ module.exports = {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
trustProxy: process.env.TRUST_PROXY,
|
trustProxy: !!process.env.TRUST_PROXY,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
module.exports.up = async (knex) =>
|
module.exports.up = (knex) =>
|
||||||
knex.schema.table('user_account', (table) => {
|
knex.schema.table('user_account', (table) => {
|
||||||
/* Columns */
|
/* Columns */
|
||||||
|
|
||||||
table.text('language');
|
table.text('language');
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.down = async (knex) =>
|
module.exports.down = (knex) =>
|
||||||
knex.schema.table('user_account', (table) => {
|
knex.schema.table('user_account', (table) => {
|
||||||
table.dropColumn('language');
|
table.dropColumn('language');
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
module.exports.up = async (knex) =>
|
module.exports.up = (knex) =>
|
||||||
knex.schema.table('user_account', (table) => {
|
knex.schema.table('user_account', (table) => {
|
||||||
/* Columns */
|
/* Columns */
|
||||||
|
|
||||||
table.timestamp('password_changed_at', true);
|
table.timestamp('password_changed_at', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.down = async (knex) =>
|
module.exports.down = (knex) =>
|
||||||
knex.schema.table('user_account', (table) => {
|
knex.schema.table('user_account', (table) => {
|
||||||
table.dropColumn('password_changed_at');
|
table.dropColumn('password_changed_at');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
Generated
+379
-31
@@ -14,6 +14,7 @@
|
|||||||
"knex": "^2.2.0",
|
"knex": "^2.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"move-file": "^2.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sails": "^1.5.2",
|
"sails": "^1.5.2",
|
||||||
"sails-hook-orm": "^4.0.1",
|
"sails-hook-orm": "^4.0.1",
|
||||||
@@ -22,7 +23,9 @@
|
|||||||
"sharp": "^0.30.7",
|
"sharp": "^0.30.7",
|
||||||
"stream-to-array": "^2.3.0",
|
"stream-to-array": "^2.3.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"validator": "^13.7.0"
|
"validator": "^13.7.0",
|
||||||
|
"winston": "^3.8.1",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
@@ -40,6 +43,24 @@
|
|||||||
"node": "^12.10"
|
"node": "^12.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@colors/colors": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.90"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dabh/diagnostics": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"colorspace": "1.1.x",
|
||||||
|
"enabled": "2.0.x",
|
||||||
|
"kuler": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
|
||||||
@@ -901,6 +922,37 @@
|
|||||||
"node": ">=0.1.90"
|
"node": ">=0.1.90"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/colorspace": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^3.1.3",
|
||||||
|
"text-hex": "1.0.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/colorspace/node_modules/color": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^1.9.3",
|
||||||
|
"color-string": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/colorspace/node_modules/color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/colorspace/node_modules/color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -1402,6 +1454,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/enabled": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
@@ -2223,6 +2280,11 @@
|
|||||||
"pend": "~1.2.0"
|
"pend": "~1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fecha": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -2406,6 +2468,11 @@
|
|||||||
"@sailshq/lodash": "^3.10.2"
|
"@sailshq/lodash": "^3.10.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fn.name": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||||
|
},
|
||||||
"node_modules/for-in": {
|
"node_modules/for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||||
@@ -3380,6 +3447,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-stream": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-string": {
|
"node_modules/is-string": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
||||||
@@ -3650,6 +3728,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kuler": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -3777,6 +3860,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/logform": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@colors/colors": "1.5.0",
|
||||||
|
"fecha": "^4.2.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"triple-beam": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loupe": {
|
"node_modules/loupe": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
|
||||||
@@ -4305,6 +4400,28 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/move-file": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/move-file/-/move-file-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/move-file/node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@@ -4608,6 +4725,14 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/one-time": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"fn.name": "1.x.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/opn": {
|
"node_modules/opn": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
|
||||||
@@ -5043,6 +5168,35 @@
|
|||||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
|
||||||
"integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="
|
"integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/prompt/node_modules/winston": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.3",
|
||||||
|
"colors": "1.0.x",
|
||||||
|
"cycle": "1.0.x",
|
||||||
|
"eyes": "0.1.x",
|
||||||
|
"isstream": "0.1.x",
|
||||||
|
"stack-trace": "0.0.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prompt/node_modules/winston/node_modules/async": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||||
|
},
|
||||||
|
"node_modules/prompt/node_modules/winston/node_modules/colors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.90"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -5507,6 +5661,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-stable-stringify": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -6913,6 +7075,11 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-hex": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -7008,6 +7175,11 @@
|
|||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/triple-beam": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
|
||||||
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
||||||
@@ -7623,19 +7795,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/winston": {
|
"node_modules/winston": {
|
||||||
"version": "2.4.6",
|
"version": "3.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz",
|
||||||
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==",
|
"integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dabh/diagnostics": "^2.0.2",
|
||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"colors": "1.0.x",
|
"is-stream": "^2.0.0",
|
||||||
"cycle": "1.0.x",
|
"logform": "^2.4.0",
|
||||||
"eyes": "0.1.x",
|
"one-time": "^1.0.0",
|
||||||
"isstream": "0.1.x",
|
"readable-stream": "^3.4.0",
|
||||||
"stack-trace": "0.0.x"
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"stack-trace": "0.0.x",
|
||||||
|
"triple-beam": "^1.3.0",
|
||||||
|
"winston-transport": "^4.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/winston-transport": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"logform": "^2.3.2",
|
||||||
|
"readable-stream": "^3.6.0",
|
||||||
|
"triple-beam": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/winston/node_modules/async": {
|
"node_modules/winston/node_modules/async": {
|
||||||
@@ -7643,14 +7832,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||||
},
|
},
|
||||||
"node_modules/winston/node_modules/colors": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.1.90"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
@@ -7804,9 +7985,29 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxcvbn": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@colors/colors": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="
|
||||||
|
},
|
||||||
|
"@dabh/diagnostics": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
|
||||||
|
"requires": {
|
||||||
|
"colorspace": "1.1.x",
|
||||||
|
"enabled": "2.0.x",
|
||||||
|
"kuler": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@eslint/eslintrc": {
|
"@eslint/eslintrc": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
|
||||||
@@ -8473,6 +8674,39 @@
|
|||||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
||||||
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
|
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
|
||||||
},
|
},
|
||||||
|
"colorspace": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||||
|
"requires": {
|
||||||
|
"color": "^3.1.3",
|
||||||
|
"text-hex": "1.0.x"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"color": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^1.9.3",
|
||||||
|
"color-string": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"combined-stream": {
|
"combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -8874,6 +9108,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
},
|
},
|
||||||
|
"enabled": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
|
||||||
|
},
|
||||||
"encodeurl": {
|
"encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
@@ -9548,6 +9787,11 @@
|
|||||||
"pend": "~1.2.0"
|
"pend": "~1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fecha": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
|
||||||
|
},
|
||||||
"file-entry-cache": {
|
"file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -9690,6 +9934,11 @@
|
|||||||
"@sailshq/lodash": "^3.10.2"
|
"@sailshq/lodash": "^3.10.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fn.name": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||||
|
},
|
||||||
"for-in": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||||
@@ -10407,6 +10656,11 @@
|
|||||||
"call-bind": "^1.0.2"
|
"call-bind": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-stream": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
|
||||||
|
},
|
||||||
"is-string": {
|
"is-string": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
||||||
@@ -10605,6 +10859,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"kuler": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||||
|
},
|
||||||
"levn": {
|
"levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -10716,6 +10975,18 @@
|
|||||||
"is-unicode-supported": "^0.1.0"
|
"is-unicode-supported": "^0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logform": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==",
|
||||||
|
"requires": {
|
||||||
|
"@colors/colors": "1.5.0",
|
||||||
|
"fecha": "^4.2.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"triple-beam": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"loupe": {
|
"loupe": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
|
||||||
@@ -11128,6 +11399,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||||
},
|
},
|
||||||
|
"move-file": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/move-file/-/move-file-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==",
|
||||||
|
"requires": {
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@@ -11345,6 +11631,14 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"one-time": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||||
|
"requires": {
|
||||||
|
"fn.name": "1.x.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"opn": {
|
"opn": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
|
||||||
@@ -11669,6 +11963,31 @@
|
|||||||
"version": "0.9.2",
|
"version": "0.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
|
||||||
"integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="
|
"integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="
|
||||||
|
},
|
||||||
|
"winston": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==",
|
||||||
|
"requires": {
|
||||||
|
"async": "^3.2.3",
|
||||||
|
"colors": "1.0.x",
|
||||||
|
"cycle": "1.0.x",
|
||||||
|
"eyes": "0.1.x",
|
||||||
|
"isstream": "0.1.x",
|
||||||
|
"stack-trace": "0.0.x"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"async": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -12021,6 +12340,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||||
},
|
},
|
||||||
|
"safe-stable-stringify": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg=="
|
||||||
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -13206,6 +13530,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
|
||||||
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="
|
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="
|
||||||
},
|
},
|
||||||
|
"text-hex": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||||
|
},
|
||||||
"text-table": {
|
"text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -13281,6 +13610,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"triple-beam": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
|
||||||
|
},
|
||||||
"tsconfig-paths": {
|
"tsconfig-paths": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
||||||
@@ -13780,30 +14114,39 @@
|
|||||||
"integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg=="
|
"integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg=="
|
||||||
},
|
},
|
||||||
"winston": {
|
"winston": {
|
||||||
"version": "2.4.6",
|
"version": "3.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz",
|
||||||
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==",
|
"integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"@dabh/diagnostics": "^2.0.2",
|
||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"colors": "1.0.x",
|
"is-stream": "^2.0.0",
|
||||||
"cycle": "1.0.x",
|
"logform": "^2.4.0",
|
||||||
"eyes": "0.1.x",
|
"one-time": "^1.0.0",
|
||||||
"isstream": "0.1.x",
|
"readable-stream": "^3.4.0",
|
||||||
"stack-trace": "0.0.x"
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"stack-trace": "0.0.x",
|
||||||
|
"triple-beam": "^1.3.0",
|
||||||
|
"winston-transport": "^4.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": {
|
"async": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||||
},
|
|
||||||
"colors": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"winston-transport": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
|
||||||
|
"requires": {
|
||||||
|
"logform": "^2.3.2",
|
||||||
|
"readable-stream": "^3.6.0",
|
||||||
|
"triple-beam": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"word-wrap": {
|
"word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
@@ -13907,6 +14250,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"zxcvbn": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -10,7 +10,7 @@
|
|||||||
"lint": "eslint . --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look good.'",
|
"lint": "eslint . --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look good.'",
|
||||||
"start": "nodemon",
|
"start": "nodemon",
|
||||||
"start:prod": "node app.js --prod",
|
"start:prod": "node app.js --prod",
|
||||||
"test": "mocha test/lifecycle.test.js test/integration/**/*.test.js"
|
"test": "mocha test/lifecycle.test.js test/integration/**/*.test.js test/utils/**/*.test.js"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
"knex": "^2.2.0",
|
"knex": "^2.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"move-file": "^2.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sails": "^1.5.2",
|
"sails": "^1.5.2",
|
||||||
"sails-hook-orm": "^4.0.1",
|
"sails-hook-orm": "^4.0.1",
|
||||||
@@ -54,7 +55,9 @@
|
|||||||
"sharp": "^0.30.7",
|
"sharp": "^0.30.7",
|
||||||
"stream-to-array": "^2.3.0",
|
"stream-to-array": "^2.3.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"validator": "^13.7.0"
|
"validator": "^13.7.0",
|
||||||
|
"winston": "^3.8.1",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
const { expect } = require('chai');
|
||||||
|
const { getRemoteAddress } = require('../../utils/remoteAddress');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake HTTP request object
|
||||||
|
* given to all api controllers.
|
||||||
|
*/
|
||||||
|
const MOCK_REQUEST = {
|
||||||
|
ip: '',
|
||||||
|
ips: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mocks the `MOCK_REQUEST` value.
|
||||||
|
* Should be called before asserting `getRemoteAddress`.
|
||||||
|
* @param {string} ip Mock remote IP address
|
||||||
|
* @param {any[]} ips Mock array of proxy IP addresses
|
||||||
|
*/
|
||||||
|
const mockRequest = (ip, ips) => {
|
||||||
|
MOCK_REQUEST.ip = ip;
|
||||||
|
MOCK_REQUEST.ips = ips;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mocks the `TRUST_PROXY` environment variable passed through `docker-compose` file.
|
||||||
|
* @param {boolean} trustProxy Whether the TRUST_PROXY environment variable was enabled.
|
||||||
|
*/
|
||||||
|
const mockProxyFlag = (trustProxy) => {
|
||||||
|
process.env.TRUST_PROXY = !!trustProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('remoteAddress', () => {
|
||||||
|
describe('#getRemoteAddress(Request)', () => {
|
||||||
|
it('should get IPv4 remote address while not behind proxy and TRUST_PROXY=false', async () => {
|
||||||
|
const expectedAddress = '172.2.109.132';
|
||||||
|
|
||||||
|
mockRequest(`::ffff:${expectedAddress}`, null);
|
||||||
|
mockProxyFlag(false);
|
||||||
|
|
||||||
|
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get IPv6 remote address while not behind proxy and TRUST_PROXY=false', async () => {
|
||||||
|
const expectedAddress = 'f53f:5832:9f1c:fe38:ce3d:1be8:81a2:115e';
|
||||||
|
|
||||||
|
mockRequest(expectedAddress, null);
|
||||||
|
mockProxyFlag(false);
|
||||||
|
|
||||||
|
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get IPv4 remote address while behind proxy and TRUST_PROXY=true', async () => {
|
||||||
|
const expectedAddress = '172.2.109.132';
|
||||||
|
|
||||||
|
mockRequest(`::ffff:${expectedAddress}`, [
|
||||||
|
`::ffff:${expectedAddress}`,
|
||||||
|
'::ffff:192.182.23.111',
|
||||||
|
'::ffff:120.210.132.14',
|
||||||
|
]);
|
||||||
|
mockProxyFlag(true);
|
||||||
|
|
||||||
|
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get IPv6 remote address while behind proxy and TRUST_PROXY=true', async () => {
|
||||||
|
const expectedAddress = 'f53f:5832:9f1c:fe38:ce3d:1be8:81a2:115e';
|
||||||
|
|
||||||
|
mockRequest(expectedAddress, [
|
||||||
|
expectedAddress,
|
||||||
|
'9d74:fb18:3b95:801f:8751:8d18:8207:b322',
|
||||||
|
'598e:4291:e1b3:2991:5d17:00af:1b6b:802c',
|
||||||
|
]);
|
||||||
|
mockProxyFlag(true);
|
||||||
|
|
||||||
|
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const winston = require('winston');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default timestamp used by the logger.
|
||||||
|
* Format example: "2022-08-18 6:30:02"
|
||||||
|
*/
|
||||||
|
const defaultLogTimestampFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
|
||||||
|
const logfile = `${process.cwd()}/logs/planka.log`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log level for both console and file log sinks.
|
||||||
|
*
|
||||||
|
* Refer {@link https://github.com/winstonjs/winston#logging here}
|
||||||
|
* for more information on Winston log levels.
|
||||||
|
*/
|
||||||
|
const logLevel = 'warn'; // process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
||||||
|
|
||||||
|
const logFormat = winston.format.combine(
|
||||||
|
winston.format.uncolorize(),
|
||||||
|
winston.format.timestamp({ format: defaultLogTimestampFormat }),
|
||||||
|
winston.format.printf((log) => `${log.timestamp} [${log.level[0].toUpperCase()}] ${log.message}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const customLogger = new winston.createLogger({
|
||||||
|
transports: [
|
||||||
|
new winston.transports.File({
|
||||||
|
level: logLevel,
|
||||||
|
format: logFormat,
|
||||||
|
filename: logfile,
|
||||||
|
}),
|
||||||
|
new winston.transports.Console({
|
||||||
|
level: logLevel,
|
||||||
|
format: logFormat,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
customLogger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* The IP address of the client that just made a request to this application, whether
|
||||||
|
* or not the TRUST_PROXY env variable is true and if endpoint accessed through a proxy.
|
||||||
|
* @param {Request} request The endpoint Request object
|
||||||
|
* @returns The IP address of the client that just made a request
|
||||||
|
*/
|
||||||
|
const getRemoteAddress = (request) => {
|
||||||
|
let remoteAddress = request.ip;
|
||||||
|
|
||||||
|
// Assert if "X-Forwarded-For" header contains any addresses
|
||||||
|
if (process.env.TRUST_PROXY && !_.isEmpty(request.ips)) {
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
remoteAddress = request.ips[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert address from IPV6 to IPV4 if client device is IPV4.
|
||||||
|
const defaultIPV6Regex = /^::ffff:((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/g;
|
||||||
|
if (remoteAddress.match(defaultIPV6Regex)) {
|
||||||
|
remoteAddress = remoteAddress.replace('::ffff:', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteAddress;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getRemoteAddress,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user