From 891b7ce90a255e0fea832102f4cdec69cf6ea82e Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Tue, 9 Aug 2022 17:59:10 +0200 Subject: [PATCH] ref: Add log out event, refactoring --- client/src/api/access-tokens.js | 2 +- client/src/api/activities.js | 4 +- client/src/api/attachments.js | 12 ++--- client/src/api/board-memberships.js | 7 +-- client/src/api/boards.js | 11 +++-- client/src/api/card-labels.js | 6 ++- client/src/api/card-memberships.js | 7 +-- client/src/api/cards.js | 20 ++++---- client/src/api/comment-activities.js | 12 ++--- client/src/api/http.js | 4 +- client/src/api/labels.js | 7 +-- client/src/api/lists.js | 7 +-- client/src/api/notifications.js | 12 ++--- client/src/api/project-managers.js | 7 +-- client/src/api/projects.js | 14 +++--- client/src/api/socket.js | 3 +- client/src/api/tasks.js | 6 +-- client/src/api/users.js | 22 +++++---- client/src/constants/Config.js | 8 ---- client/src/sagas/core/request.js | 7 ++- client/src/sagas/core/services/users.js | 3 +- client/src/utils/access-token-storage.js | 21 ++++----- package-lock.json | 4 +- server/.env.sample | 7 ++- .../api/controllers/access-tokens/create.js | 2 +- server/api/controllers/users/update-avatar.js | 1 + server/api/controllers/users/update-email.js | 2 +- .../api/controllers/users/update-password.js | 18 ++------ .../api/controllers/users/update-username.js | 2 +- server/api/controllers/users/update.js | 2 +- server/api/helpers/users/update-one.js | 36 +++++++++++++-- server/api/helpers/utils/create-token.js | 29 ++++++++++++ server/api/helpers/utils/sign-token.js | 18 -------- server/api/helpers/utils/verify-token.js | 8 +++- server/api/hooks/current-user/index.js | 46 +++++++++---------- server/api/models/User.js | 3 +- server/config/custom.js | 2 + ...ssword_changed_at_to_user_account_table.js | 2 +- 38 files changed, 213 insertions(+), 171 deletions(-) create mode 100644 server/api/helpers/utils/create-token.js delete mode 100644 server/api/helpers/utils/sign-token.js diff --git a/client/src/api/access-tokens.js b/client/src/api/access-tokens.js index 0e5c988..6cd1063 100755 --- a/client/src/api/access-tokens.js +++ b/client/src/api/access-tokens.js @@ -2,7 +2,7 @@ import http from './http'; /* Actions */ -const createAccessToken = (data) => http.post('/access-tokens', data); +const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers); export default { createAccessToken, diff --git a/client/src/api/activities.js b/client/src/api/activities.js index b99999b..a404eee 100755 --- a/client/src/api/activities.js +++ b/client/src/api/activities.js @@ -9,8 +9,8 @@ export const transformActivity = (activity) => ({ /* Actions */ -const getActivities = (cardId, data) => - socket.get(`/cards/${cardId}/actions`, data).then((body) => ({ +const getActivities = (cardId, data, headers) => + socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({ ...body, items: body.items.map(transformActivity), })); diff --git a/client/src/api/attachments.js b/client/src/api/attachments.js index 8ed6472..f3edba4 100755 --- a/client/src/api/attachments.js +++ b/client/src/api/attachments.js @@ -10,20 +10,20 @@ export const transformAttachment = (attachment) => ({ /* Actions */ -const createAttachment = (cardId, data, requestId) => - http.post(`/cards/${cardId}/attachments?requestId=${requestId}`, data).then((body) => ({ +const createAttachment = (cardId, data, requestId, headers) => + http.post(`/cards/${cardId}/attachments?requestId=${requestId}`, data, headers).then((body) => ({ ...body, item: transformAttachment(body.item), })); -const updateAttachment = (id, data) => - socket.patch(`/attachments/${id}`, data).then((body) => ({ +const updateAttachment = (id, data, headers) => + socket.patch(`/attachments/${id}`, data, headers).then((body) => ({ ...body, item: transformAttachment(body.item), })); -const deleteAttachment = (id) => - socket.delete(`/attachments/${id}`).then((body) => ({ +const deleteAttachment = (id, headers) => + socket.delete(`/attachments/${id}`, undefined, headers).then((body) => ({ ...body, item: transformAttachment(body.item), })); diff --git a/client/src/api/board-memberships.js b/client/src/api/board-memberships.js index 0fa087b..3ef1ce8 100644 --- a/client/src/api/board-memberships.js +++ b/client/src/api/board-memberships.js @@ -2,10 +2,11 @@ import socket from './socket'; /* Actions */ -const createBoardMembership = (boardId, data) => - socket.post(`/boards/${boardId}/memberships`, data); +const createBoardMembership = (boardId, data, headers) => + socket.post(`/boards/${boardId}/memberships`, data, headers); -const deleteBoardMembership = (id) => socket.delete(`/board-memberships/${id}`); +const deleteBoardMembership = (id, headers) => + socket.delete(`/board-memberships/${id}`, undefined, headers); export default { createBoardMembership, diff --git a/client/src/api/boards.js b/client/src/api/boards.js index 4e618ce..b605ed2 100755 --- a/client/src/api/boards.js +++ b/client/src/api/boards.js @@ -4,10 +4,11 @@ import { transformAttachment } from './attachments'; /* Actions */ -const createBoard = (projectId, data) => socket.post(`/projects/${projectId}/boards`, data); +const createBoard = (projectId, data, headers) => + socket.post(`/projects/${projectId}/boards`, data, headers); -const getBoard = (id) => - socket.get(`/boards/${id}`).then((body) => ({ +const getBoard = (id, headers) => + socket.get(`/boards/${id}`, undefined, headers).then((body) => ({ ...body, included: { ...body.included, @@ -16,9 +17,9 @@ const getBoard = (id) => }, })); -const updateBoard = (id, data) => socket.patch(`/boards/${id}`, data); +const updateBoard = (id, data, headers) => socket.patch(`/boards/${id}`, data, headers); -const deleteBoard = (id) => socket.delete(`/boards/${id}`); +const deleteBoard = (id, headers) => socket.delete(`/boards/${id}`, undefined, headers); export default { createBoard, diff --git a/client/src/api/card-labels.js b/client/src/api/card-labels.js index d4fc0fa..92715d6 100644 --- a/client/src/api/card-labels.js +++ b/client/src/api/card-labels.js @@ -2,9 +2,11 @@ import socket from './socket'; /* Actions */ -const createCardLabel = (cardId, data) => socket.post(`/cards/${cardId}/labels`, data); +const createCardLabel = (cardId, data, headers) => + socket.post(`/cards/${cardId}/labels`, data, headers); -const deleteCardLabel = (cardId, labelId) => socket.delete(`/cards/${cardId}/labels/${labelId}`); +const deleteCardLabel = (cardId, labelId, headers) => + socket.delete(`/cards/${cardId}/labels/${labelId}`, undefined, headers); export default { createCardLabel, diff --git a/client/src/api/card-memberships.js b/client/src/api/card-memberships.js index 2333030..2deb9d9 100644 --- a/client/src/api/card-memberships.js +++ b/client/src/api/card-memberships.js @@ -2,10 +2,11 @@ import socket from './socket'; /* Actions */ -const createCardMembership = (cardId, data) => socket.post(`/cards/${cardId}/memberships`, data); +const createCardMembership = (cardId, data, headers) => + socket.post(`/cards/${cardId}/memberships`, data, headers); -const deleteCardMembership = (cardId, userId) => - socket.delete(`/cards/${cardId}/memberships?userId=${userId}`); +const deleteCardMembership = (cardId, userId, headers) => + socket.delete(`/cards/${cardId}/memberships?userId=${userId}`, undefined, headers); export default { createCardMembership, diff --git a/client/src/api/cards.js b/client/src/api/cards.js index b8e2624..c7e3435 100755 --- a/client/src/api/cards.js +++ b/client/src/api/cards.js @@ -35,8 +35,8 @@ export const transformCardData = (data) => ({ /* Actions */ -const getCards = (boardId, data) => - socket.get(`/board/${boardId}/cards`, data).then((body) => ({ +const getCards = (boardId, data, headers) => + socket.get(`/board/${boardId}/cards`, data, headers).then((body) => ({ ...body, items: body.items.map(transformCard), included: { @@ -45,26 +45,26 @@ const getCards = (boardId, data) => }, })); -const createCard = (boardId, data) => - socket.post(`/boards/${boardId}/cards`, transformCardData(data)).then((body) => ({ +const createCard = (boardId, data, headers) => + socket.post(`/boards/${boardId}/cards`, transformCardData(data), headers).then((body) => ({ ...body, item: transformCard(body.item), })); -const getCard = (id) => - socket.get(`/cards/${id}`).then((body) => ({ +const getCard = (id, headers) => + socket.get(`/cards/${id}`, undefined, headers).then((body) => ({ ...body, item: transformCard(body.item), })); -const updateCard = (id, data) => - socket.patch(`/cards/${id}`, transformCardData(data)).then((body) => ({ +const updateCard = (id, data, headers) => + socket.patch(`/cards/${id}`, transformCardData(data), headers).then((body) => ({ ...body, item: transformCard(body.item), })); -const deleteCard = (id) => - socket.delete(`/cards/${id}`).then((body) => ({ +const deleteCard = (id, headers) => + socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({ ...body, item: transformCard(body.item), })); diff --git a/client/src/api/comment-activities.js b/client/src/api/comment-activities.js index 3d37863..814f9de 100755 --- a/client/src/api/comment-activities.js +++ b/client/src/api/comment-activities.js @@ -3,20 +3,20 @@ import { transformActivity } from './activities'; /* Actions */ -const createCommentActivity = (cardId, data) => - socket.post(`/cards/${cardId}/comment-actions`, data).then((body) => ({ +const createCommentActivity = (cardId, data, headers) => + socket.post(`/cards/${cardId}/comment-actions`, data, headers).then((body) => ({ ...body, item: transformActivity(body.item), })); -const updateCommentActivity = (id, data) => - socket.patch(`/comment-actions/${id}`, data).then((body) => ({ +const updateCommentActivity = (id, data, headers) => + socket.patch(`/comment-actions/${id}`, data, headers).then((body) => ({ ...body, item: transformActivity(body.item), })); -const deleteCommentActivity = (id) => - socket.delete(`/comment-actions/${id}`).then((body) => ({ +const deleteCommentActivity = (id, headers) => + socket.delete(`/comment-actions/${id}`, undefined, headers).then((body) => ({ ...body, item: transformActivity(body.item), })); diff --git a/client/src/api/http.js b/client/src/api/http.js index 9776807..646528d 100755 --- a/client/src/api/http.js +++ b/client/src/api/http.js @@ -6,7 +6,7 @@ const http = {}; // TODO: add all methods ['POST'].forEach((method) => { - http[method.toLowerCase()] = (url, data) => { + http[method.toLowerCase()] = (url, data, headers) => { const formData = Object.keys(data).reduce((result, key) => { result.append(key, data[key]); @@ -15,8 +15,8 @@ const http = {}; return fetch(`${Config.SERVER_BASE_URL}/api${url}`, { method, + headers, body: formData, - ...Config.FETCH_OPTIONS, }) .then((response) => response.json().then((body) => ({ diff --git a/client/src/api/labels.js b/client/src/api/labels.js index 80d9201..e1101ef 100755 --- a/client/src/api/labels.js +++ b/client/src/api/labels.js @@ -2,11 +2,12 @@ import socket from './socket'; /* Actions */ -const createLabel = (boardId, data) => socket.post(`/boards/${boardId}/labels`, data); +const createLabel = (boardId, data, headers) => + socket.post(`/boards/${boardId}/labels`, data, headers); -const updateLabel = (id, data) => socket.patch(`/labels/${id}`, data); +const updateLabel = (id, data, headers) => socket.patch(`/labels/${id}`, data, headers); -const deleteLabel = (id) => socket.delete(`/labels/${id}`); +const deleteLabel = (id, headers) => socket.delete(`/labels/${id}`, undefined, headers); export default { createLabel, diff --git a/client/src/api/lists.js b/client/src/api/lists.js index 59722ed..6c1ee6a 100755 --- a/client/src/api/lists.js +++ b/client/src/api/lists.js @@ -2,11 +2,12 @@ import socket from './socket'; /* Actions */ -const createList = (boardId, data) => socket.post(`/boards/${boardId}/lists`, data); +const createList = (boardId, data, headers) => + socket.post(`/boards/${boardId}/lists`, data, headers); -const updateList = (id, data) => socket.patch(`/lists/${id}`, data); +const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers); -const deleteList = (id) => socket.delete(`/lists/${id}`); +const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers); export default { createList, diff --git a/client/src/api/notifications.js b/client/src/api/notifications.js index 3fd664f..f415e24 100755 --- a/client/src/api/notifications.js +++ b/client/src/api/notifications.js @@ -13,8 +13,8 @@ export const transformNotification = (notification) => ({ /* Actions */ -const getNotifications = () => - socket.get('/notifications').then((body) => ({ +const getNotifications = (headers) => + socket.get('/notifications', undefined, headers).then((body) => ({ ...body, items: body.items.map(transformNotification), included: { @@ -24,8 +24,8 @@ const getNotifications = () => }, })); -const getNotification = (id) => - socket.get(`/notifications/${id}`).then((body) => ({ +const getNotification = (id, headers) => + socket.get(`/notifications/${id}`, undefined, headers).then((body) => ({ ...body, item: transformNotification(body.item), included: { @@ -35,8 +35,8 @@ const getNotification = (id) => }, })); -const updateNotifications = (ids, data) => - socket.patch(`/notifications/${ids.join(',')}`, data).then((body) => ({ +const updateNotifications = (ids, data, headers) => + socket.patch(`/notifications/${ids.join(',')}`, data, headers).then((body) => ({ ...body, items: body.items.map(transformNotification), })); diff --git a/client/src/api/project-managers.js b/client/src/api/project-managers.js index 6701a76..a835bf2 100755 --- a/client/src/api/project-managers.js +++ b/client/src/api/project-managers.js @@ -2,10 +2,11 @@ import socket from './socket'; /* Actions */ -const createProjectManager = (projectId, data) => - socket.post(`/projects/${projectId}/managers`, data); +const createProjectManager = (projectId, data, headers) => + socket.post(`/projects/${projectId}/managers`, data, headers); -const deleteProjectManager = (id) => socket.delete(`/project-managers/${id}`); +const deleteProjectManager = (id, headers) => + socket.delete(`/project-managers/${id}`, undefined, headers); export default { createProjectManager, diff --git a/client/src/api/projects.js b/client/src/api/projects.js index 863d3f5..5a22bee 100755 --- a/client/src/api/projects.js +++ b/client/src/api/projects.js @@ -3,18 +3,18 @@ import socket from './socket'; /* Actions */ -const getProjects = () => socket.get('/projects'); +const getProjects = (headers) => socket.get('/projects', undefined, headers); -const createProject = (data) => socket.post('/projects', data); +const createProject = (data, headers) => socket.post('/projects', data, headers); -const getProject = (id) => socket.get(`/projects/${id}`); +const getProject = (id, headers) => socket.get(`/projects/${id}`, undefined, headers); -const updateProject = (id, data) => socket.patch(`/projects/${id}`, data); +const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers); -const updateProjectBackgroundImage = (id, data) => - http.post(`/projects/${id}/background-image`, data); +const updateProjectBackgroundImage = (id, data, headers) => + http.post(`/projects/${id}/background-image`, data, headers); -const deleteProject = (id) => socket.delete(`/projects/${id}`); +const deleteProject = (id, headers) => socket.delete(`/projects/${id}`, undefined, headers); export default { getProjects, diff --git a/client/src/api/socket.js b/client/src/api/socket.js index 8e4aa7a..2902661 100755 --- a/client/src/api/socket.js +++ b/client/src/api/socket.js @@ -16,12 +16,13 @@ const { socket } = io; socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => { - socket[method.toLowerCase()] = (url, data) => + socket[method.toLowerCase()] = (url, data, headers) => new Promise((resolve, reject) => { socket.request( { method, data, + headers, url: `/api${url}`, }, (_, { body, error }) => { diff --git a/client/src/api/tasks.js b/client/src/api/tasks.js index 2b07896..584ec3e 100755 --- a/client/src/api/tasks.js +++ b/client/src/api/tasks.js @@ -2,11 +2,11 @@ import socket from './socket'; /* Actions */ -const createTask = (cardId, data) => socket.post(`/cards/${cardId}/tasks`, data); +const createTask = (cardId, data, headers) => socket.post(`/cards/${cardId}/tasks`, data, headers); -const updateTask = (id, data) => socket.patch(`/tasks/${id}`, data); +const updateTask = (id, data, headers) => socket.patch(`/tasks/${id}`, data, headers); -const deleteTask = (id) => socket.delete(`/tasks/${id}`); +const deleteTask = (id, headers) => socket.delete(`/tasks/${id}`, undefined, headers); export default { createTask, diff --git a/client/src/api/users.js b/client/src/api/users.js index cb286e8..ca77bd9 100755 --- a/client/src/api/users.js +++ b/client/src/api/users.js @@ -3,25 +3,27 @@ import socket from './socket'; /* Actions */ -const getUsers = () => socket.get('/users'); +const getUsers = (headers) => socket.get('/users', undefined, headers); -const createUser = (data) => socket.post('/users', data); +const createUser = (data, headers) => socket.post('/users', data, headers); -const getUser = (id) => socket.get(`/users/${id}`); +const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers); -const getCurrentUser = () => socket.get('/users/me'); +const getCurrentUser = (headers) => socket.get('/users/me', undefined, headers); -const updateUser = (id, data) => socket.patch(`/users/${id}`, data); +const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers); -const updateUserEmail = (id, data) => socket.patch(`/users/${id}/email`, data); +const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers); -const updateUserPassword = (id, data) => socket.patch(`/users/${id}/password`, data); +const updateUserPassword = (id, data, headers) => + socket.patch(`/users/${id}/password`, data, headers); -const updateUserUsername = (id, data) => socket.patch(`/users/${id}/username`, data); +const updateUserUsername = (id, data, headers) => + socket.patch(`/users/${id}/username`, data, headers); -const updateUserAvatar = (id, data) => http.post(`/users/${id}/avatar`, data); +const updateUserAvatar = (id, data, headers) => http.post(`/users/${id}/avatar`, data, headers); -const deleteUser = (id) => socket.delete(`/users/${id}`); +const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers); export default { getUsers, diff --git a/client/src/constants/Config.js b/client/src/constants/Config.js index 96ae584..28fb374 100755 --- a/client/src/constants/Config.js +++ b/client/src/constants/Config.js @@ -2,13 +2,6 @@ const SERVER_BASE_URL = process.env.REACT_APP_SERVER_BASE_URL || (process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1337'); -const FETCH_OPTIONS = - process.env.NODE_ENV === 'production' - ? undefined - : { - credentials: 'include', - }; - const ACCESS_TOKEN_KEY = 'accessToken'; const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion'; const ACCESS_TOKEN_VERSION = '1'; @@ -18,7 +11,6 @@ const ACTIVITIES_LIMIT = 50; export default { SERVER_BASE_URL, - FETCH_OPTIONS, ACCESS_TOKEN_KEY, ACCESS_TOKEN_VERSION_KEY, ACCESS_TOKEN_VERSION, diff --git a/client/src/sagas/core/request.js b/client/src/sagas/core/request.js index 8318452..a918862 100755 --- a/client/src/sagas/core/request.js +++ b/client/src/sagas/core/request.js @@ -1,6 +1,7 @@ import { call, fork, join, put, take } from 'redux-saga/effects'; import actions from '../../actions'; +import { getAccessToken } from '../../utils/access-token-storage'; import ErrorCodes from '../../constants/ErrorCodes'; let lastRequestTask; @@ -12,8 +13,12 @@ function* queueRequest(method, ...args) { } catch {} // eslint-disable-line no-empty } + const accessToken = yield call(getAccessToken); + try { - return yield call(method, ...args); + return yield call(method, ...args, { + Authorization: `Bearer ${accessToken}`, + }); } catch (error) { if (error.code === ErrorCodes.UNAUTHORIZED) { yield put(actions.logout()); // TODO: next url diff --git a/client/src/sagas/core/services/users.js b/client/src/sagas/core/services/users.js index f4ed448..b2a9ea4 100644 --- a/client/src/sagas/core/services/users.js +++ b/client/src/sagas/core/services/users.js @@ -111,6 +111,7 @@ export function* updateUserPassword(id, data) { let user; let accessToken; + try { ({ item: user, accessToken } = yield call(request, api.updateUserPassword, id, data)); } catch (error) { @@ -118,7 +119,7 @@ export function* updateUserPassword(id, data) { return; } - if (accessToken !== undefined) { + if (accessToken) { yield call(setAccessToken, accessToken); } diff --git a/client/src/utils/access-token-storage.js b/client/src/utils/access-token-storage.js index 62fdfde..c735a9c 100755 --- a/client/src/utils/access-token-storage.js +++ b/client/src/utils/access-token-storage.js @@ -2,36 +2,35 @@ import Cookies from 'js-cookie'; import jwtDecode from 'jwt-decode'; import Config from '../constants/Config'; -import socket from '../api/socket'; export const setAccessToken = (accessToken) => { const { exp } = jwtDecode(accessToken); - const expires = exp !== undefined ? new Date(exp * 1000) : 365; + const expires = new Date(exp * 1000); Cookies.set(Config.ACCESS_TOKEN_KEY, accessToken, { expires, secure: window.location.protocol === 'https:', sameSite: 'strict', }); + Cookies.set(Config.ACCESS_TOKEN_VERSION_KEY, Config.ACCESS_TOKEN_VERSION, { expires, }); +}; - socket.headers = { Cookie: document.cookie }; +export const removeAccessToken = () => { + Cookies.remove(Config.ACCESS_TOKEN_KEY); + Cookies.remove(Config.ACCESS_TOKEN_VERSION_KEY); }; export const getAccessToken = () => { - // TODO: remove migration - const accessToken = Cookies.get(Config.ACCESS_TOKEN_KEY); + let accessToken = Cookies.get(Config.ACCESS_TOKEN_KEY); const accessTokenVersion = Cookies.get(Config.ACCESS_TOKEN_VERSION_KEY); + if (accessToken && accessTokenVersion !== Config.ACCESS_TOKEN_VERSION) { - // Add secure and sameSite attributes to the cookie - setAccessToken(accessToken); + removeAccessToken(); + accessToken = undefined; } return accessToken; }; - -export const removeAccessToken = () => { - Cookies.remove(Config.ACCESS_TOKEN_KEY); -}; diff --git a/package-lock.json b/package-lock.json index efc547b..52b132a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "planka", - "version": "1.5.0", + "version": "1.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "planka", - "version": "1.5.0", + "version": "1.5.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/server/.env.sample b/server/.env.sample index 28dd1e1..ac5696c 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -1,6 +1,9 @@ -TZ=UTC BASE_URL=http://localhost:1337 DATABASE_URL=postgresql://postgres@localhost/planka SECRET_KEY=notsecretkey + # In days -ACCESS_TOKEN_EXPIRES=365 +TOKEN_EXPIRES_IN=365 + +# Do not edit this +TZ=UTC diff --git a/server/api/controllers/access-tokens/create.js b/server/api/controllers/access-tokens/create.js index 1ca6eea..5345b48 100755 --- a/server/api/controllers/access-tokens/create.js +++ b/server/api/controllers/access-tokens/create.js @@ -49,7 +49,7 @@ module.exports = { } return { - item: sails.helpers.utils.signToken(user.id), + item: sails.helpers.utils.createToken(user.id), }; }, }; diff --git a/server/api/controllers/users/update-avatar.js b/server/api/controllers/users/update-avatar.js index 455ed17..66635b7 100755 --- a/server/api/controllers/users/update-avatar.js +++ b/server/api/controllers/users/update-avatar.js @@ -54,6 +54,7 @@ module.exports = { { avatarDirname: files[0].extra.dirname, }, + currentUser, this.req, ); diff --git a/server/api/controllers/users/update-email.js b/server/api/controllers/users/update-email.js index f1ccb1b..2d2cd52 100644 --- a/server/api/controllers/users/update-email.js +++ b/server/api/controllers/users/update-email.js @@ -69,7 +69,7 @@ module.exports = { const values = _.pick(inputs, ['email']); user = await sails.helpers.users - .updateOne(user, values, this.req) + .updateOne(user, values, currentUser, this.req) .intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE); if (!user) { diff --git a/server/api/controllers/users/update-password.js b/server/api/controllers/users/update-password.js index 5f90924..f64d01f 100644 --- a/server/api/controllers/users/update-password.js +++ b/server/api/controllers/users/update-password.js @@ -60,26 +60,14 @@ module.exports = { } const values = _.pick(inputs, ['password']); - user = await sails.helpers.users.updateOne(user, values, this.req); + user = await sails.helpers.users.updateOne(user, values, currentUser, this.req); if (!user) { throw Errors.USER_NOT_FOUND; } - // Disconnect all sockets from this user except the current one - const tempRoom = `temp:${user.id}`; - sails.sockets.addRoomMembersToRooms(`user:${user.id}`, tempRoom, () => { - if (currentUser.id === user.id && this.req.isSocket) { - sails.sockets.leave(this.req, tempRoom, () => { - sails.sockets.leaveAll(tempRoom); - }); - } else { - sails.sockets.leaveAll(tempRoom); - } - }); - - if (currentUser.id === user.id) { - const accessToken = sails.helpers.utils.signToken(user.id); + if (user.id === currentUser.id) { + const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt); return { accessToken, diff --git a/server/api/controllers/users/update-username.js b/server/api/controllers/users/update-username.js index 60699b5..f9961b3 100644 --- a/server/api/controllers/users/update-username.js +++ b/server/api/controllers/users/update-username.js @@ -71,7 +71,7 @@ module.exports = { const values = _.pick(inputs, ['username']); user = await sails.helpers.users - .updateOne(user, values, this.req) + .updateOne(user, values, currentUser, this.req) .intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE); if (!user) { diff --git a/server/api/controllers/users/update.js b/server/api/controllers/users/update.js index 10abb6c..0057c91 100755 --- a/server/api/controllers/users/update.js +++ b/server/api/controllers/users/update.js @@ -75,7 +75,7 @@ module.exports = { 'subscribeToOwnCards', ]); - user = await sails.helpers.users.updateOne(user, values, this.req); + user = await sails.helpers.users.updateOne(user, values, currentUser, this.req); if (!user) { throw Errors.USER_NOT_FOUND; diff --git a/server/api/helpers/users/update-one.js b/server/api/helpers/users/update-one.js index b9f49a6..1be079a 100644 --- a/server/api/helpers/users/update-one.js +++ b/server/api/helpers/users/update-one.js @@ -1,6 +1,7 @@ const path = require('path'); const bcrypt = require('bcrypt'); const rimraf = require('rimraf'); +const { v4: uuid } = require('uuid'); module.exports = { inputs: { @@ -35,6 +36,10 @@ module.exports = { }, required: true, }, + user: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -54,10 +59,10 @@ module.exports = { let isOnlyPasswordChange = false; if (!_.isUndefined(inputs.values.password)) { - // eslint-disable-next-line no-param-reassign - inputs.values.password = bcrypt.hashSync(inputs.values.password, 10); - // eslint-disable-next-line no-param-reassign - inputs.values.passwordChangedAt = new Date(); + Object.assign(inputs.values, { + password: bcrypt.hashSync(inputs.values.password, 10), + passwordChangedAt: new Date().toUTCString(), + }); if (Object.keys(inputs.values).length === 1) { isOnlyPasswordChange = true; @@ -105,6 +110,29 @@ module.exports = { } } + if (!_.isUndefined(inputs.values.password)) { + sails.sockets.broadcast( + `user:${user.id}`, + 'userDelete', // TODO: introduce separate event + { + item: user, + }, + inputs.request, + ); + + if (user.id === inputs.user.id && inputs.request && inputs.request.isSocket) { + const tempRoom = uuid(); + + sails.sockets.addRoomMembersToRooms(`user:${user.id}`, tempRoom, () => { + sails.sockets.leave(inputs.request, tempRoom, () => { + sails.sockets.leaveAll(tempRoom); + }); + }); + } else { + sails.sockets.leaveAll(`user:${user.id}`); + } + } + if (!isOnlyPasswordChange) { /* const projectIds = await sails.helpers.users.getManagerProjectIds(user.id); diff --git a/server/api/helpers/utils/create-token.js b/server/api/helpers/utils/create-token.js new file mode 100644 index 0000000..01d6ed6 --- /dev/null +++ b/server/api/helpers/utils/create-token.js @@ -0,0 +1,29 @@ +const jwt = require('jsonwebtoken'); + +module.exports = { + sync: true, + + inputs: { + subject: { + type: 'json', + required: true, + }, + issuedAt: { + type: 'ref', + }, + }, + + fn(inputs) { + const { issuedAt = new Date() } = inputs; + const iat = Math.floor(issuedAt / 1000); + + return jwt.sign( + { + iat, + sub: inputs.subject, + exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60, + }, + sails.config.session.secret, + ); + }, +}; diff --git a/server/api/helpers/utils/sign-token.js b/server/api/helpers/utils/sign-token.js deleted file mode 100644 index 11a2519..0000000 --- a/server/api/helpers/utils/sign-token.js +++ /dev/null @@ -1,18 +0,0 @@ -const jwt = require('jsonwebtoken'); - -module.exports = { - sync: true, - - inputs: { - payload: { - type: 'json', - required: true, - }, - }, - - fn(inputs) { - return jwt.sign({ sub: inputs.payload }, sails.config.session.secret, { - expiresIn: `${process.env.ACCESS_TOKEN_EXPIRES}d`, - }); - }, -}; diff --git a/server/api/helpers/utils/verify-token.js b/server/api/helpers/utils/verify-token.js index a7d71bb..585287c 100644 --- a/server/api/helpers/utils/verify-token.js +++ b/server/api/helpers/utils/verify-token.js @@ -15,10 +15,16 @@ module.exports = { }, fn(inputs) { + let payload; try { - return jwt.verify(inputs.token, sails.config.session.secret); + payload = jwt.verify(inputs.token, sails.config.session.secret); } catch (error) { throw 'invalidToken'; } + + return { + subject: payload.sub, + issuedAt: new Date(payload.iat * 1000), + }; }, }; diff --git a/server/api/hooks/current-user/index.js b/server/api/hooks/current-user/index.js index 138d254..ee37403 100644 --- a/server/api/hooks/current-user/index.js +++ b/server/api/hooks/current-user/index.js @@ -10,28 +10,20 @@ module.exports = function defineCurrentUserHook(sails) { const TOKEN_PATTERN = /^Bearer /; const getUser = async (accessToken) => { - let id; - let iat; - let decodedToken; - + let payload; try { - decodedToken = sails.helpers.utils.verifyToken(accessToken); + payload = sails.helpers.utils.verifyToken(accessToken); } catch (error) { return null; } - if (_.isString(decodedToken)) { - id = decodedToken; - iat = 1; - } else { - id = decodedToken.sub; - iat = decodedToken.iat; + const user = await sails.helpers.users.getOne(payload.subject); + + if (user && user.passwordChangedAt > payload.issuedAt) { + return null; } - return sails.helpers.users.getOne({ - id, - passwordChangedAt: { '<=': new Date((iat + 1) * 1000) }, - }); + return user; }; return { @@ -45,21 +37,25 @@ module.exports = function defineCurrentUserHook(sails) { routes: { before: { - '/*': { + '/api/*': { async fn(req, res, next) { - let accessToken; - if (req.headers.authorization) { - if (TOKEN_PATTERN.test(req.headers.authorization)) { - accessToken = req.headers.authorization.replace(TOKEN_PATTERN, ''); - } - } else if (req.cookies.accessToken) { - accessToken = req.cookies.accessToken; - } + const { authorization: authorizationHeader } = req.headers; + + if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) { + const accessToken = authorizationHeader.replace(TOKEN_PATTERN, ''); - if (accessToken) { req.currentUser = await getUser(accessToken); } + return next(); + }, + }, + '/attachments/*': { + async fn(req, res, next) { + if (req.cookies.accessToken) { + req.currentUser = await getUser(req.cookies.accessToken); + } + return next(); }, }, diff --git a/server/api/models/User.js b/server/api/models/User.js index fea4f83..4d03e0e 100755 --- a/server/api/models/User.js +++ b/server/api/models/User.js @@ -70,7 +70,6 @@ module.exports = { passwordChangedAt: { type: 'ref', columnName: 'password_changed_at', - defaultsTo: new Date(0).toUTCString(), }, // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ @@ -107,7 +106,7 @@ module.exports = { customToJSON() { return { - ..._.omit(this, ['password', 'avatarDirname']), + ..._.omit(this, ['password', 'avatarDirname', 'passwordChangedAt']), avatarUrl: this.avatarDirname && `${sails.config.custom.userAvatarsUrl}/${this.avatarDirname}/square-100.jpg`, diff --git a/server/config/custom.js b/server/config/custom.js index 8d47ae7..9b3d86e 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -20,6 +20,8 @@ module.exports.custom = { baseUrl: process.env.BASE_URL, + tokenExpiresIn: process.env.TOKEN_EXPIRES_IN, + userAvatarsPath: path.join(sails.config.paths.public, 'user-avatars'), userAvatarsUrl: `${process.env.BASE_URL}/user-avatars`, diff --git a/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js b/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js index 50a1ef3..5d63696 100644 --- a/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js +++ b/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js @@ -2,7 +2,7 @@ module.exports.up = async (knex) => knex.schema.table('user_account', (table) => { /* Columns */ - table.timestamp('password_changed_at', true).defaultsTo(new Date(0).toUTCString()); + table.timestamp('password_changed_at', true); }); module.exports.down = async (knex) =>