mirror of
https://github.com/plankanban/planka.git
synced 2025-07-19 05:09:43 +02:00
feat: Invalidate access token on logout
This commit is contained in:
parent
f091de6827
commit
48ea62c0a0
26 changed files with 242 additions and 37 deletions
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,18 +1,34 @@
|
||||||
|
import { getAccessToken } from '../utils/access-token-storage';
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
accessToken: getAccessToken(),
|
||||||
userId: null,
|
userId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line default-param-last
|
// eslint-disable-next-line default-param-last
|
||||||
export default (state = initialState, { type, payload }) => {
|
export default (state = initialState, { type, payload }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accessToken: payload.accessToken,
|
||||||
|
};
|
||||||
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
||||||
case ActionTypes.CORE_INITIALIZE:
|
case ActionTypes.CORE_INITIALIZE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userId: payload.user.id,
|
userId: payload.user.id,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
|
||||||
|
if (payload.accessToken) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accessToken: payload.accessToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ModalTypes from '../constants/ModalTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isInitializing: true,
|
isInitializing: true,
|
||||||
|
isLogouting: false,
|
||||||
currentModal: null,
|
currentModal: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,6 +23,11 @@ export default (state = initialState, { type, payload }) => {
|
||||||
...state,
|
...state,
|
||||||
isInitializing: false,
|
isInitializing: false,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLogouting: true,
|
||||||
|
};
|
||||||
case ActionTypes.MODAL_OPEN:
|
case ActionTypes.MODAL_OPEN:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { all, apply, call, fork, take } from 'redux-saga/effects';
|
import { all, apply, fork, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
import watchers from './watchers';
|
import watchers from './watchers';
|
||||||
import services from './services';
|
import services from './services';
|
||||||
import { socket } from '../../api';
|
import { socket } from '../../api';
|
||||||
import { removeAccessToken } from '../../utils/access-token-storage';
|
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
|
|
||||||
|
@ -15,6 +14,5 @@ export default function* coreSaga() {
|
||||||
|
|
||||||
yield take(ActionTypes.LOGOUT);
|
yield take(ActionTypes.LOGOUT);
|
||||||
|
|
||||||
yield call(removeAccessToken);
|
|
||||||
window.location.href = Paths.LOGIN;
|
window.location.href = Paths.LOGIN;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { call, fork, join, put, take } from 'redux-saga/effects';
|
import { call, fork, join, put, select, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import selectors from '../../selectors';
|
||||||
import actions from '../../actions';
|
import actions from '../../actions';
|
||||||
import { getAccessToken } from '../../utils/access-token-storage';
|
import { removeAccessToken } from '../../utils/access-token-storage';
|
||||||
import ErrorCodes from '../../constants/ErrorCodes';
|
import ErrorCodes from '../../constants/ErrorCodes';
|
||||||
|
|
||||||
let lastRequestTask;
|
let lastRequestTask;
|
||||||
|
@ -13,7 +14,7 @@ function* queueRequest(method, ...args) {
|
||||||
} catch {} // eslint-disable-line no-empty
|
} catch {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = yield call(getAccessToken);
|
const accessToken = yield select(selectors.selectAccessToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return yield call(method, ...args, {
|
return yield call(method, ...args, {
|
||||||
|
@ -21,6 +22,7 @@ function* queueRequest(method, ...args) {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
||||||
|
yield call(removeAccessToken);
|
||||||
yield put(actions.logout()); // TODO: next url
|
yield put(actions.logout()); // TODO: next url
|
||||||
yield take();
|
yield take();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { call, put, take } from 'redux-saga/effects';
|
import { call, put, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import request from '../request';
|
||||||
import requests from '../requests';
|
import requests from '../requests';
|
||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
|
import api from '../../../api';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
|
import { removeAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
|
||||||
export function* initializeCore() {
|
export function* initializeCore() {
|
||||||
const {
|
const {
|
||||||
|
@ -60,7 +63,17 @@ export function* changeCoreLanguage(language) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* logout() {
|
export function* logout(invalidateAccessToken = true) {
|
||||||
|
yield call(removeAccessToken);
|
||||||
|
|
||||||
|
if (invalidateAccessToken) {
|
||||||
|
yield put(actions.logout.invalidateAccessToken());
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield call(request, api.deleteCurrentAccessToken);
|
||||||
|
} catch (error) {} // eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
yield put(actions.logout());
|
yield put(actions.logout());
|
||||||
yield take();
|
yield take();
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,11 +124,13 @@ export function* updateUserPassword(id, data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessTokens && accessTokens[0]) {
|
const accessToken = accessTokens && accessTokens[0];
|
||||||
yield call(setAccessToken, accessTokens[0]);
|
|
||||||
|
if (accessToken) {
|
||||||
|
yield call(setAccessToken, accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(actions.updateUserPassword.success(user));
|
yield put(actions.updateUserPassword.success(user, accessToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* updateCurrentUserPassword(data) {
|
export function* updateCurrentUserPassword(data) {
|
||||||
|
@ -215,7 +217,7 @@ export function* handleUserDelete(user) {
|
||||||
const currentUserId = yield select(selectors.selectCurrentUserId);
|
const currentUserId = yield select(selectors.selectCurrentUserId);
|
||||||
|
|
||||||
if (user.id === currentUserId) {
|
if (user.id === currentUserId) {
|
||||||
yield call(logout);
|
yield call(logout, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(actions.handleUserDelete(user));
|
yield put(actions.handleUserDelete(user));
|
||||||
|
|
|
@ -2,18 +2,13 @@ import { all, call, cancel, fork, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
import watchers from './watchers';
|
import watchers from './watchers';
|
||||||
import services from './services';
|
import services from './services';
|
||||||
import { setAccessToken } from '../../utils/access-token-storage';
|
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
|
|
||||||
export default function* loginSaga() {
|
export default function* loginSaga() {
|
||||||
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
||||||
|
|
||||||
const {
|
yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
||||||
payload: { accessToken },
|
|
||||||
} = yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
|
||||||
|
|
||||||
yield cancel(watcherTasks);
|
yield cancel(watcherTasks);
|
||||||
|
|
||||||
yield call(setAccessToken, accessToken);
|
|
||||||
yield call(services.goToRoot);
|
yield call(services.goToRoot);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { call, put } from 'redux-saga/effects';
|
||||||
|
|
||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
|
import { setAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
|
||||||
export function* authenticate(data) {
|
export function* authenticate(data) {
|
||||||
yield put(actions.authenticate(data));
|
yield put(actions.authenticate(data));
|
||||||
|
@ -14,6 +15,7 @@ export function* authenticate(data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield call(setAccessToken, accessToken);
|
||||||
yield put(actions.authenticate.success(accessToken));
|
yield put(actions.authenticate.success(accessToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
selectAccessToken,
|
|
||||||
};
|
|
|
@ -4,8 +4,12 @@ import isUndefined from 'lodash/isUndefined';
|
||||||
import orm from '../orm';
|
import orm from '../orm';
|
||||||
import Config from '../constants/Config';
|
import Config from '../constants/Config';
|
||||||
|
|
||||||
|
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
||||||
|
|
||||||
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
||||||
|
|
||||||
|
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
|
||||||
|
|
||||||
const nextPosition = (items, index, excludedId) => {
|
const nextPosition = (items, index, excludedId) => {
|
||||||
const filteredItems = isUndefined(excludedId)
|
const filteredItems = isUndefined(excludedId)
|
||||||
? items
|
? items
|
||||||
|
@ -94,7 +98,9 @@ export const selectNextTaskPosition = createSelector(
|
||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
selectAccessToken,
|
||||||
selectIsCoreInitializing,
|
selectIsCoreInitializing,
|
||||||
|
selectIsLogouting,
|
||||||
selectNextBoardPosition,
|
selectNextBoardPosition,
|
||||||
selectNextListPosition,
|
selectNextListPosition,
|
||||||
selectNextCardPosition,
|
selectNextCardPosition,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import auth from './auth';
|
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import modals from './modals';
|
import modals from './modals';
|
||||||
import users from './users';
|
import users from './users';
|
||||||
|
@ -14,7 +13,6 @@ import attachments from './attachments';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...router,
|
...router,
|
||||||
...auth,
|
|
||||||
...core,
|
...core,
|
||||||
...modals,
|
...modals,
|
||||||
...users,
|
...users,
|
||||||
|
|
|
@ -40,24 +40,33 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
|
const remoteAddress = getRemoteAddress(this.req);
|
||||||
|
|
||||||
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
sails.log.warn(
|
sails.log.warn(
|
||||||
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
|
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
|
||||||
this.req,
|
|
||||||
)})`,
|
|
||||||
);
|
);
|
||||||
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
||||||
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
|
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
|
||||||
throw Errors.INVALID_PASSWORD;
|
throw Errors.INVALID_PASSWORD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessToken = sails.helpers.utils.createToken(user.id);
|
||||||
|
|
||||||
|
await Session.create({
|
||||||
|
accessToken,
|
||||||
|
remoteAddress,
|
||||||
|
userId: user.id,
|
||||||
|
userAgent: this.req.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item: sails.helpers.utils.createToken(user.id),
|
item: accessToken,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
16
server/api/controllers/access-tokens/delete.js
Normal file
16
server/api/controllers/access-tokens/delete.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module.exports = {
|
||||||
|
async fn() {
|
||||||
|
const { accessToken } = this.req;
|
||||||
|
|
||||||
|
await Session.updateOne({
|
||||||
|
accessToken,
|
||||||
|
deletedAt: null,
|
||||||
|
}).set({
|
||||||
|
deletedAt: new Date().toUTCString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: accessToken,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,6 +1,8 @@
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const zxcvbn = require('zxcvbn');
|
const zxcvbn = require('zxcvbn');
|
||||||
|
|
||||||
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
|
@ -71,6 +73,13 @@ module.exports = {
|
||||||
if (user.id === currentUser.id) {
|
if (user.id === currentUser.id) {
|
||||||
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
|
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
|
||||||
|
|
||||||
|
await Session.create({
|
||||||
|
accessToken,
|
||||||
|
userId: user.id,
|
||||||
|
remoteAddress: getRemoteAddress(this.req),
|
||||||
|
userAgent: this.req.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item: user,
|
item: user,
|
||||||
included: {
|
included: {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -24,6 +25,9 @@ module.exports = {
|
||||||
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
|
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
|
||||||
},
|
},
|
||||||
sails.config.session.secret,
|
sails.config.session.secret,
|
||||||
|
{
|
||||||
|
keyid: uuid(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,15 @@ module.exports = function defineCurrentUserHook(sails) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = await Session.findOne({
|
||||||
|
accessToken,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const user = await sails.helpers.users.getOne(payload.subject);
|
const user = await sails.helpers.users.getOne(payload.subject);
|
||||||
|
|
||||||
if (user && user.passwordChangedAt > payload.issuedAt) {
|
if (user && user.passwordChangedAt > payload.issuedAt) {
|
||||||
|
@ -43,8 +52,14 @@ module.exports = function defineCurrentUserHook(sails) {
|
||||||
|
|
||||||
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
|
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
|
||||||
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, '');
|
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, '');
|
||||||
|
const currentUser = await getUser(accessToken);
|
||||||
|
|
||||||
req.currentUser = await getUser(accessToken);
|
if (currentUser) {
|
||||||
|
Object.assign(req, {
|
||||||
|
accessToken,
|
||||||
|
currentUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
@ -52,8 +67,17 @@ module.exports = function defineCurrentUserHook(sails) {
|
||||||
},
|
},
|
||||||
'/attachments/*': {
|
'/attachments/*': {
|
||||||
async fn(req, res, next) {
|
async fn(req, res, next) {
|
||||||
if (req.cookies.accessToken) {
|
const { accessToken } = req.cookies;
|
||||||
req.currentUser = await getUser(req.cookies.accessToken);
|
|
||||||
|
if (accessToken) {
|
||||||
|
const currentUser = await getUser(accessToken);
|
||||||
|
|
||||||
|
if (currentUser) {
|
||||||
|
Object.assign(req, {
|
||||||
|
accessToken,
|
||||||
|
currentUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|
49
server/api/models/Session.js
Executable file
49
server/api/models/Session.js
Executable 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -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',
|
||||||
|
|
24
server/db/migrations/20220906094517_create_session_table.js
Normal file
24
server/db/migrations/20220906094517_create_session_table.js
Normal file
|
@ -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');
|
Loading…
Add table
Add a link
Reference in a new issue