From b86b88ab895953011becdab7a310ed579db785ba Mon Sep 17 00:00:00 2001 From: Alberto Casado Torres Date: Fri, 10 May 2024 09:32:58 +0200 Subject: [PATCH 1/2] 6754 canonical hot fix (#6760) * added filename to canonical links * version bump --- app/react/Viewer/PDFView.js | 9 +++++++-- app/react/Viewer/specs/PDFView.spec.js | 11 +++++++---- .../Viewer/specs/__snapshots__/PDFView.spec.js.snap | 12 ++++++------ package.json | 2 +- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/react/Viewer/PDFView.js b/app/react/Viewer/PDFView.js index a7c52122b0..cd8774d351 100644 --- a/app/react/Viewer/PDFView.js +++ b/app/react/Viewer/PDFView.js @@ -102,12 +102,17 @@ class PDFViewComponent extends Component { render() { const query = searchParamsFromSearchParams(this.props.searchParams); const page = Number(query.page || 1); - const { pathname } = this.props.location; + const { filename } = defaultDoc(this.props.entity); + const sharedId = this.props.entity.get('sharedId'); const raw = query.raw === 'true' || !isClient; return ( <> - {raw && } + + {raw && ( + + )} + { }; props = { - entity: fromJS({ defaultDoc: { _id: 'documentId' } }), + entity: fromJS({ + sharedId: 'a2b4c3', + defaultDoc: { _id: 'documentId', sharedId: 'sharedId', filename: '1234.pdf' }, + }), routes: [], }; spyOn(routeActions, 'requestViewerState'); @@ -144,10 +147,10 @@ describe('PDFView', () => { page = 1; pathname = 'pathname'; render(); - expect( - component.find({ link: [{ href: 'pathname?page=1', rel: 'canonical' }] }) - ).toMatchSnapshot(); + + expect(component.find({ link: [{ rel: 'canonical' }] })).toMatchSnapshot(); }); + describe('when page is undefined', () => { it('should render link canonical with a default page', () => { raw = 'true'; diff --git a/app/react/Viewer/specs/__snapshots__/PDFView.spec.js.snap b/app/react/Viewer/specs/__snapshots__/PDFView.spec.js.snap index 3d5a6e4ad2..d4cbcd643a 100644 --- a/app/react/Viewer/specs/__snapshots__/PDFView.spec.js.snap +++ b/app/react/Viewer/specs/__snapshots__/PDFView.spec.js.snap @@ -8,7 +8,7 @@ Array [ link={ Array [ Object { - "href": "pathname?page=1", + "href": "/entity/a2b4c3?page=1&file=1234.pdf", "rel": "canonical", }, ] @@ -20,7 +20,7 @@ Array [ link={ Array [ Object { - "href": "pathname?page=1", + "href": "/entity/a2b4c3?page=1&file=1234.pdf", "rel": "canonical", }, ] @@ -33,7 +33,7 @@ Array [ link={ Array [ Object { - "href": "pathname?page=1", + "href": "/entity/a2b4c3?page=1&file=1234.pdf", "rel": "canonical", }, ] @@ -50,7 +50,7 @@ Array [ link={ Array [ Object { - "href": "pathname?page=1", + "href": "/entity/a2b4c3?page=1&file=1234.pdf", "rel": "canonical", }, ] @@ -62,7 +62,7 @@ Array [ link={ Array [ Object { - "href": "pathname?page=1", + "href": "/entity/a2b4c3?page=1&file=1234.pdf", "rel": "canonical", }, ] @@ -75,7 +75,7 @@ Array [ link={ Array [ Object { - "href": "pathname?page=1", + "href": "/entity/a2b4c3?page=1&file=1234.pdf", "rel": "canonical", }, ] diff --git a/package.json b/package.json index 193e0926b1..c132d1d3f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.165.1", + "version": "1.165.2", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react" From 710822d7e2d58108f06f9709adaf184a1e89e296 Mon Sep 17 00:00:00 2001 From: Federico Nocetti Date: Thu, 23 May 2024 11:51:00 -0300 Subject: [PATCH 2/2] Improve codes (#6813) * Change key generation * increment recovery ttl and add disclaimer * Upgrade version --- app/api/users/generateUnlockCode.ts | 3 +++ app/api/users/passwordRecoveriesModel.js | 3 +++ app/api/users/specs/users.spec.js | 11 ++++++----- app/api/users/users.js | 11 +++++------ package.json | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 app/api/users/generateUnlockCode.ts diff --git a/app/api/users/generateUnlockCode.ts b/app/api/users/generateUnlockCode.ts new file mode 100644 index 0000000000..ba3b669be2 --- /dev/null +++ b/app/api/users/generateUnlockCode.ts @@ -0,0 +1,3 @@ +import crypto from 'crypto'; + +export const generateUnlockCode = () => crypto.randomBytes(32).toString('hex'); diff --git a/app/api/users/passwordRecoveriesModel.js b/app/api/users/passwordRecoveriesModel.js index 9123aaaa7a..a92ae1f841 100644 --- a/app/api/users/passwordRecoveriesModel.js +++ b/app/api/users/passwordRecoveriesModel.js @@ -2,9 +2,12 @@ import mongoose from 'mongoose'; import { instanceModel } from 'api/odm'; +const ONE_DAY = 60 * 60 * 24; + const schema = new mongoose.Schema({ key: String, user: { type: mongoose.Schema.Types.ObjectId, ref: 'users' }, + expiresAt: { type: Date, expires: ONE_DAY, default: Date.now() }, }); export default instanceModel('passwordrecoveries', schema); diff --git a/app/api/users/specs/users.spec.js b/app/api/users/specs/users.spec.js index ebae63a4d1..15a8e84ee0 100644 --- a/app/api/users/specs/users.spec.js +++ b/app/api/users/specs/users.spec.js @@ -2,7 +2,6 @@ /* eslint-disable max-statements */ import { createError } from 'api/utils'; -import SHA256 from 'crypto-js/sha256'; import crypto from 'crypto'; import mailer from 'api/utils/mailer'; import db from 'api/utils/testing_db'; @@ -25,6 +24,7 @@ import fixtures, { import users from '../users.js'; import passwordRecoveriesModel from '../passwordRecoveriesModel'; import usersModel from '../usersModel'; +import * as unlockCode from '../generateUnlockCode'; describe('Users', () => { beforeEach(async () => { @@ -510,10 +510,11 @@ describe('Users', () => { jest.restoreAllMocks(); jest.spyOn(mailer, 'send').mockImplementation(async () => Promise.resolve('OK')); jest.spyOn(Date, 'now').mockReturnValue(1000); + jest.spyOn(unlockCode, 'generateUnlockCode').mockReturnValue('ABCDEF1234'); }); it('should find the matching email create a recover password doc in the database and send an email', async () => { - const key = SHA256(`test@email.com${1000}`).toString(); + const key = unlockCode.generateUnlockCode(); const settings = await settingsModel.get(); const response = await users.recoverPassword('test@email.com', 'domain'); expect(response).toBe('OK'); @@ -524,13 +525,13 @@ describe('Users', () => { from: emailSender, to: 'test@email.com', subject: 'Password set', - text: `To set your password click on the following link:\ndomain/setpassword/${key}`, + text: `To set your password click on the following link:\ndomain/setpassword/${key}\nThis link will be valid for 24 hours.`, }; expect(mailer.send).toHaveBeenCalledWith(expectedMailOptions); }); it('should personalize the mail if recover password process is part of a newly created user', async () => { - const key = SHA256(`peter@parker.com${1000}`).toString(); + const key = unlockCode.generateUnlockCode(); const settings = await settingsModel.get(); const newUser = await users.newUser( @@ -590,7 +591,7 @@ describe('Users', () => { describe('when the user does not exist with that email', () => { it('should not create the entry in the database, should not send a mail, and return an error.', async () => { jest.spyOn(Date, 'now').mockReturnValue(1000); - const key = SHA256(`false@email.com${1000}`).toString(); + const key = unlockCode.generateUnlockCode(); let response; try { response = await users.recoverPassword('false@email.com'); diff --git a/app/api/users/users.js b/app/api/users/users.js index c0074007ed..2e8dff14a4 100644 --- a/app/api/users/users.js +++ b/app/api/users/users.js @@ -1,5 +1,4 @@ import SHA256 from 'crypto-js/sha256'; -import crypto from 'crypto'; import { createError } from 'api/utils'; import random from 'shared/uniqueID'; @@ -15,16 +14,15 @@ import mailer from '../utils/mailer'; import model from './usersModel'; import passwordRecoveriesModel from './passwordRecoveriesModel'; import settings from '../settings/settings'; +import { generateUnlockCode } from './generateUnlockCode'; const MAX_FAILED_LOGIN_ATTEMPTS = 6; -const generateUnlockCode = () => crypto.randomBytes(32).toString('hex'); - function conformRecoverText(options, _settings, domain, key, user) { const response = {}; if (!options.newUser) { response.subject = 'Password set'; - response.text = `To set your password click on the following link:\n${domain}/setpassword/${key}`; + response.text = `To set your password click on the following link:\n${domain}/setpassword/${key}\nThis link will be valid for 24 hours.`; } if (options.newUser) { @@ -34,7 +32,8 @@ function conformRecoverText(options, _settings, domain, key, user) { `The administrators of ${siteName} have created an account for you under the user name:\n` + `${user.username}\n\n` + 'To complete this process, please create a strong password by clicking on the following link:\n' + - `${domain}/setpassword/${key}?createAccount=true\n\n` + + `${domain}/setpassword/${key}?createAccount=true\n` + + 'This link will be valid for 24 hours.\n\n' + 'For more information about the Uwazi platform, visit https://www.uwazi.io.\n\nThank you!\nUwazi team'; const htmlLink = `${domain}/setpassword/${key}?createAccount=true`; @@ -288,7 +287,7 @@ export default { }, recoverPassword(email, domain, options = {}) { - const key = SHA256(email + Date.now()).toString(); + const key = generateUnlockCode(); return Promise.all([model.get({ email }), settings.get()]).then(([_user, _settings]) => { const user = _user[0]; if (user) { diff --git a/package.json b/package.json index 8e66504764..a17be271d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.167.0", + "version": "1.167.1", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react"