Compare commits

...
11 Commits
43 changed files with 373 additions and 101 deletions
+12 -1
View File
@@ -44,7 +44,8 @@
"semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.3.1",
"validator": "^13.7.0",
"whatwg-fetch": "^3.6.2"
"whatwg-fetch": "^3.6.2",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
@@ -25310,6 +25311,11 @@
"type": "github",
"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": {
@@ -43805,6 +43811,11 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
"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
View File
@@ -101,7 +101,8 @@
"semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.3.1",
"validator": "^13.7.0",
"whatwg-fetch": "^3.6.2"
"whatwg-fetch": "^3.6.2",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
+5
View File
@@ -44,6 +44,11 @@ const logout = () => ({
payload: {},
});
logout.invalidateAccessToken = () => ({
type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE,
payload: {},
});
export default {
initializeCore,
logout,
+2 -1
View File
@@ -103,10 +103,11 @@ const updateUserPassword = (id, data) => ({
},
});
updateUserPassword.success = (user) => ({
updateUserPassword.success = (user, accessToken) => ({
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
payload: {
user,
accessToken,
},
});
+5
View File
@@ -1,9 +1,14 @@
import http from './http';
import socket from './socket';
/* Actions */
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers);
export default {
createAccessToken,
deleteCurrentAccessToken,
};
+7 -1
View File
@@ -15,6 +15,7 @@ const Header = React.memo(
project,
user,
notifications,
isLogouting,
canEditProject,
canEditUsers,
onProjectSettingsClick,
@@ -75,7 +76,11 @@ const Header = React.memo(
)}
</Menu.Item>
</NotificationsPopup>
<UserPopup onSettingsClick={onUserSettingsClick} onLogout={onLogout}>
<UserPopup
isLogouting={isLogouting}
onSettingsClick={onUserSettingsClick}
onLogout={onLogout}
>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
{user.name}
</Menu.Item>
@@ -93,6 +98,7 @@ Header.propTypes = {
user: PropTypes.object.isRequired,
notifications: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isLogouting: PropTypes.bool.isRequired,
canEditProject: PropTypes.bool.isRequired,
canEditUsers: PropTypes.bool.isRequired,
onProjectSettingsClick: PropTypes.func.isRequired,
@@ -8,7 +8,7 @@ import { withPopup } from '../../lib/popup';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm } from '../../hooks';
import { isUsername } from '../../utils/validator';
import { isPassword, isUsername } from '../../utils/validator';
import styles from './UserAddPopup.module.scss';
@@ -69,7 +69,7 @@ const UserAddStep = React.memo(
return;
}
if (!cleanData.password) {
if (!cleanData.password || !isPassword(cleanData.password)) {
passwordField.current.focus();
return;
}
@@ -143,6 +143,7 @@ const UserAddStep = React.memo(
/>
<div className={styles.text}>{t('common.password')}</div>
<Input.Password
withStrengthBar
fluid
ref={passwordField}
name="password"
@@ -113,18 +113,15 @@ const UserPasswordEditStep = React.memo(
)}
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.newPassword')}</div>
<div className={styles.field}>
<Input.Password
fluid
ref={passwordField}
name="password"
value={data.password}
onChange={handleFieldChange}
/>
<div className={styles.note}>
{t('common.mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber')}
</div>
</div>
<Input.Password
withStrengthBar
fluid
ref={passwordField}
name="password"
value={data.password}
className={styles.field}
onChange={handleFieldChange}
/>
{usePasswordConfirmation && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
@@ -3,12 +3,6 @@
margin-bottom: 8px;
}
.note {
font-size: 11px;
margin-top: 4px;
opacity: 0.5;
}
.text {
color: #444444;
font-size: 12px;
+19 -3
View File
@@ -1,13 +1,13 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { Button, Menu } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Popup } from '../../lib/custom-ui';
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 handleSettingsClick = useCallback(() => {
@@ -15,6 +15,17 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
onClose();
}, [onSettingsClick, onClose]);
let logoutMenuItemProps;
if (isLogouting) {
logoutMenuItemProps = {
as: Button,
fluid: true,
basic: true,
loading: true,
disabled: true,
};
}
return (
<>
<Popup.Header>
@@ -29,7 +40,11 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
context: 'title',
})}
</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', {
context: 'title',
})}
@@ -41,6 +56,7 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
});
UserStep.propTypes = {
isLogouting: PropTypes.bool.isRequired,
onSettingsClick: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
+1
View File
@@ -21,6 +21,7 @@ export default {
CORE_INITIALIZE: 'CORE_INITIALIZE',
LOGOUT: 'LOGOUT',
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
/* Modals */
+2
View File
@@ -6,6 +6,7 @@ import entryActions from '../entry-actions';
import Header from '../components/Header';
const mapStateToProps = (state) => {
const isLogouting = selectors.selectIsLogouting(state);
const currentUser = selectors.selectCurrentUser(state);
const currentProject = selectors.selectCurrentProject(state);
const notifications = selectors.selectNotificationsForCurrentUser(state);
@@ -13,6 +14,7 @@ const mapStateToProps = (state) => {
return {
notifications,
isLogouting,
project: currentProject,
user: currentUser,
canEditProject: isCurrentUserManager,
@@ -1,22 +1,74 @@
import React, { useCallback } from 'react';
import { Icon, Input } from 'semantic-ui-react';
import zxcvbn from 'zxcvbn';
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Icon, Input, Progress } from 'semantic-ui-react';
import { useToggle } from '../../../hooks';
const InputPassword = React.forwardRef((props, ref) => {
const [isVisible, toggleVisible] = useToggle();
import styles from './InputPassword.module.css';
const handleToggleClick = useCallback(() => {
toggleVisible();
}, [toggleVisible]);
const STRENGTH_SCORE_COLORS = ['red', 'orange', 'yellow', 'olive', 'green'];
return (
<Input
{...props} // eslint-disable-line react/jsx-props-no-spreading
ref={ref}
type={isVisible ? 'text' : 'password'}
icon={<Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />}
/>
);
});
const InputPassword = React.forwardRef(
({ value, withStrengthBar, minStrengthScore, className, ...props }, ref) => {
const [isVisible, toggleVisible] = useToggle();
const strengthScore = useMemo(() => {
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);
@@ -0,0 +1,4 @@
.strengthBar {
margin: 4px 0 0 !important;
opacity: 0.5;
}
+5 -5
View File
@@ -14453,11 +14453,11 @@ img.ui.bordered.image {
---------------------*/
.ui.input.error > input {
background-color: #fff6f6;
border-color: #e0b4b4;
color: #9f3a38;
-webkit-box-shadow: none;
box-shadow: none;
background-color: #fff6f6 !important;
border-color: #e0b4b4 !important;
color: #9f3a38 !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
/* Error Placeholder */
-2
View File
@@ -105,8 +105,6 @@ export default {
members: 'Members',
minutes: 'Minutes',
moveCard_title: 'Move Card',
mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber:
'Must be at least 6 characters long and contain at least one letter and number',
name: 'Name',
newEmail: 'New e-mail',
newPassword: 'New password',
-2
View File
@@ -100,8 +100,6 @@ export default {
members: 'Участники',
minutes: 'Минуты',
moveCard: 'Перемещение карточки',
mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber:
'Должен быть не менее 6 символов и содержать хотя бы одну букву и цифру',
name: 'Имя',
newEmail: 'Новый e-mail',
newPassword: 'Новый пароль',
+16
View File
@@ -1,18 +1,34 @@
import { getAccessToken } from '../utils/access-token-storage';
import ActionTypes from '../constants/ActionTypes';
const initialState = {
accessToken: getAccessToken(),
userId: null,
};
// eslint-disable-next-line default-param-last
export default (state = initialState, { type, payload }) => {
switch (type) {
case ActionTypes.AUTHENTICATE__SUCCESS:
return {
...state,
accessToken: payload.accessToken,
};
case ActionTypes.SOCKET_RECONNECT_HANDLE:
case ActionTypes.CORE_INITIALIZE:
return {
...state,
userId: payload.user.id,
};
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
if (payload.accessToken) {
return {
...state,
accessToken: payload.accessToken,
};
}
return state;
default:
return state;
}
+6
View File
@@ -5,6 +5,7 @@ import ModalTypes from '../constants/ModalTypes';
const initialState = {
isInitializing: true,
isLogouting: false,
currentModal: null,
};
@@ -22,6 +23,11 @@ export default (state = initialState, { type, payload }) => {
...state,
isInitializing: false,
};
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
return {
...state,
isLogouting: true,
};
case ActionTypes.MODAL_OPEN:
return {
...state,
+1 -3
View File
@@ -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 services from './services';
import { socket } from '../../api';
import { removeAccessToken } from '../../utils/access-token-storage';
import ActionTypes from '../../constants/ActionTypes';
import Paths from '../../constants/Paths';
@@ -15,6 +14,5 @@ export default function* coreSaga() {
yield take(ActionTypes.LOGOUT);
yield call(removeAccessToken);
window.location.href = Paths.LOGIN;
}
+5 -3
View File
@@ -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 { getAccessToken } from '../../utils/access-token-storage';
import { removeAccessToken } from '../../utils/access-token-storage';
import ErrorCodes from '../../constants/ErrorCodes';
let lastRequestTask;
@@ -13,7 +14,7 @@ function* queueRequest(method, ...args) {
} catch {} // eslint-disable-line no-empty
}
const accessToken = yield call(getAccessToken);
const accessToken = yield select(selectors.selectAccessToken);
try {
return yield call(method, ...args, {
@@ -21,6 +22,7 @@ function* queueRequest(method, ...args) {
});
} catch (error) {
if (error.code === ErrorCodes.UNAUTHORIZED) {
yield call(removeAccessToken);
yield put(actions.logout()); // TODO: next url
yield take();
}
+14 -1
View File
@@ -1,8 +1,11 @@
import { call, put, take } from 'redux-saga/effects';
import request from '../request';
import requests from '../requests';
import actions from '../../../actions';
import api from '../../../api';
import i18n from '../../../i18n';
import { removeAccessToken } from '../../../utils/access-token-storage';
export function* initializeCore() {
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 take();
}
+6 -4
View File
@@ -124,11 +124,13 @@ export function* updateUserPassword(id, data) {
return;
}
if (accessTokens && accessTokens[0]) {
yield call(setAccessToken, accessTokens[0]);
const accessToken = accessTokens && 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) {
@@ -215,7 +217,7 @@ export function* handleUserDelete(user) {
const currentUserId = yield select(selectors.selectCurrentUserId);
if (user.id === currentUserId) {
yield call(logout);
yield call(logout, false);
}
yield put(actions.handleUserDelete(user));
+1 -6
View File
@@ -2,18 +2,13 @@ import { all, call, cancel, fork, take } from 'redux-saga/effects';
import watchers from './watchers';
import services from './services';
import { setAccessToken } from '../../utils/access-token-storage';
import ActionTypes from '../../constants/ActionTypes';
export default function* loginSaga() {
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
const {
payload: { accessToken },
} = yield take(ActionTypes.AUTHENTICATE__SUCCESS);
yield take(ActionTypes.AUTHENTICATE__SUCCESS);
yield cancel(watcherTasks);
yield call(setAccessToken, accessToken);
yield call(services.goToRoot);
}
+2
View File
@@ -2,6 +2,7 @@ import { call, put } from 'redux-saga/effects';
import actions from '../../../actions';
import api from '../../../api';
import { setAccessToken } from '../../../utils/access-token-storage';
export function* authenticate(data) {
yield put(actions.authenticate(data));
@@ -14,6 +15,7 @@ export function* authenticate(data) {
return;
}
yield call(setAccessToken, accessToken);
yield put(actions.authenticate.success(accessToken));
}
-5
View File
@@ -1,5 +0,0 @@
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
export default {
selectAccessToken,
};
+6
View File
@@ -4,8 +4,12 @@ import isUndefined from 'lodash/isUndefined';
import orm from '../orm';
import Config from '../constants/Config';
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
const nextPosition = (items, index, excludedId) => {
const filteredItems = isUndefined(excludedId)
? items
@@ -94,7 +98,9 @@ export const selectNextTaskPosition = createSelector(
);
export default {
selectAccessToken,
selectIsCoreInitializing,
selectIsLogouting,
selectNextBoardPosition,
selectNextListPosition,
selectNextCardPosition,
-2
View File
@@ -1,5 +1,4 @@
import router from './router';
import auth from './auth';
import core from './core';
import modals from './modals';
import users from './users';
@@ -14,7 +13,6 @@ import attachments from './attachments';
export default {
...router,
...auth,
...core,
...modals,
...users,
+5 -7
View File
@@ -1,10 +1,8 @@
const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d).+$/;
import zxcvbn from 'zxcvbn';
const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
export const isPassword = (string) => {
return string.length >= 3 && PASSWORD_REGEX.test(string);
};
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);
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "1.7.4",
"version": "1.8.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.7.4",
"version": "1.8.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.7.4",
"version": "1.8.0",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
+14 -5
View File
@@ -40,24 +40,33 @@ module.exports = {
},
async fn(inputs) {
const remoteAddress = getRemoteAddress(this.req);
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
if (!user) {
sails.log.warn(
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
this.req,
)})`,
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
);
throw Errors.INVALID_EMAIL_OR_USERNAME;
}
if (!bcrypt.compareSync(inputs.password, user.password)) {
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
throw Errors.INVALID_PASSWORD;
}
const accessToken = sails.helpers.utils.createToken(user.id);
await Session.create({
accessToken,
remoteAddress,
userId: user.id,
userAgent: this.req.headers['user-agent'],
});
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,
};
},
};
+3 -2
View File
@@ -1,3 +1,5 @@
const zxcvbn = require('zxcvbn');
const Errors = {
EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use',
@@ -16,8 +18,7 @@ module.exports = {
},
password: {
type: 'string',
minLength: 6,
regex: /^(?=.*[A-Za-z])(?=.*\d).+$/,
custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
required: true,
},
name: {
@@ -1,4 +1,7 @@
const bcrypt = require('bcrypt');
const zxcvbn = require('zxcvbn');
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = {
USER_NOT_FOUND: {
@@ -18,8 +21,7 @@ module.exports = {
},
password: {
type: 'string',
minLength: 6,
regex: /^(?=.*[A-Za-z])(?=.*\d).+$/,
custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
required: true,
},
currentPassword: {
@@ -71,6 +73,13 @@ module.exports = {
if (user.id === currentUser.id) {
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 {
item: user,
included: {
+4
View File
@@ -1,3 +1,4 @@
const { v4: uuid } = require('uuid');
const jwt = require('jsonwebtoken');
module.exports = {
@@ -24,6 +25,9 @@ module.exports = {
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
},
sails.config.session.secret,
{
keyid: uuid(),
},
);
},
};
+27 -3
View File
@@ -17,6 +17,15 @@ module.exports = function defineCurrentUserHook(sails) {
return null;
}
const session = await Session.findOne({
accessToken,
deletedAt: null,
});
if (!session) {
return null;
}
const user = await sails.helpers.users.getOne(payload.subject);
if (user && user.passwordChangedAt > payload.issuedAt) {
@@ -43,8 +52,14 @@ module.exports = function defineCurrentUserHook(sails) {
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
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();
@@ -52,8 +67,17 @@ module.exports = function defineCurrentUserHook(sails) {
},
'/attachments/*': {
async fn(req, res, next) {
if (req.cookies.accessToken) {
req.currentUser = await getUser(req.cookies.accessToken);
const { accessToken } = req.cookies;
if (accessToken) {
const currentUser = await getUser(accessToken);
if (currentUser) {
Object.assign(req, {
accessToken,
currentUser,
});
}
}
return next();
+49
View File
@@ -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',
},
},
};
+1 -1
View File
@@ -290,7 +290,7 @@ module.exports = {
*
*/
trustProxy: process.env.TRUST_PROXY,
trustProxy: !!process.env.TRUST_PROXY,
},
/**
+1
View File
@@ -10,6 +10,7 @@
module.exports.routes = {
'POST /api/access-tokens': 'access-tokens/create',
'DELETE /api/access-tokens/me': 'access-tokens/delete',
'GET /api/users': 'users/index',
'POST /api/users': 'users/create',
@@ -0,0 +1,24 @@
module.exports.up = (knex) =>
knex.schema.createTable('session', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.text('access_token').notNullable();
table.text('remote_address').notNullable();
table.text('user_agent');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
table.timestamp('deleted_at', true);
/* Indexes */
table.index('user_id');
table.unique('access_token');
table.index('remote_address');
});
module.exports.down = (knex) => knex.schema.dropTable('session');
+12 -1
View File
@@ -24,7 +24,8 @@
"stream-to-array": "^2.3.0",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"winston": "^3.8.1"
"winston": "^3.8.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"chai": "^4.3.6",
@@ -7984,6 +7985,11 @@
"funding": {
"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": {
@@ -14244,6 +14250,11 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"zxcvbn": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
}
}
}
+2 -1
View File
@@ -56,7 +56,8 @@
"stream-to-array": "^2.3.0",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"winston": "^3.8.1"
"winston": "^3.8.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"chai": "^4.3.6",