diff --git a/app/api/auth/privateInstanceMiddleware.js b/app/api/auth/privateInstanceMiddleware.js index 2879ad3996..071ae297cb 100644 --- a/app/api/auth/privateInstanceMiddleware.js +++ b/app/api/auth/privateInstanceMiddleware.js @@ -1,9 +1,9 @@ import settings from '../settings'; -const allowedRoutes = ['login', 'setpassword/']; +const allowedRoutes = ['login', 'setpassword/', 'unlockaccount/']; const allowedRoutesMatch = new RegExp(allowedRoutes.join('|')); -const allowedApiCalls = ['/api/recoverpassword', '/api/resetpassword']; +const allowedApiCalls = ['/api/recoverpassword', '/api/resetpassword', '/api/unlockaccount']; const allowedApiMatch = new RegExp(allowedApiCalls.join('|')); const forbiddenRoutes = ['/api/', '/uploaded_documents/']; @@ -16,18 +16,13 @@ export default function (req, res, next) { return settings.get() .then((result) => { - if (result.private && req.url.match(forbiddenRoutesMatch)) { - if (req.url.match(allowedApiMatch)) { - next(); + if (result.private && !req.url.match(allowedApiMatch)) { + if (req.url.match(forbiddenRoutesMatch)) { + res.status(401); + res.json({ error: 'Unauthorized' }); return; } - res.status(401); - res.json({ error: 'Unauthorized' }); - return; - } - - if (result.private) { res.redirect('/login'); return; } diff --git a/app/api/auth/specs/privateInstanceMiddleware.spec.js b/app/api/auth/specs/privateInstanceMiddleware.spec.js index 45509f4bcf..1f09b6f868 100644 --- a/app/api/auth/specs/privateInstanceMiddleware.spec.js +++ b/app/api/auth/specs/privateInstanceMiddleware.spec.js @@ -70,6 +70,11 @@ describe('privateInstanceMiddleware', () => { req.url = 'host:port/api/resetpassword'; expectNextPromise(done); }); + + it('should allow the unlockaccount endpoint', (done) => { + req.url = 'host:port/api/unlockaccount'; + expectNextPromise(done); + }); }); describe('Other private-related calls', () => { @@ -119,5 +124,10 @@ describe('privateInstanceMiddleware', () => { req.url = 'url/setpassword/somehash'; expectNext(); }); + + it('should call next when instance is private and the url matches unlockaccount', () => { + req.url = 'url/unlockaccount/someAccount'; + expectNext(); + }); }); }); diff --git a/app/api/documents/specs/__snapshots__/routes.spec.js.snap b/app/api/documents/specs/__snapshots__/routes.spec.js.snap index 09cd0ea3a5..358d2cdf2f 100644 --- a/app/api/documents/specs/__snapshots__/routes.spec.js.snap +++ b/app/api/documents/specs/__snapshots__/routes.spec.js.snap @@ -116,6 +116,12 @@ Object { "items": Array [ Object { "children": Object { + "_id": Object { + "invalids": Array [ + "", + ], + "type": "string", + }, "filename": Object { "invalids": Array [ "", @@ -143,9 +149,10 @@ Object { }, "timestamp": Object { "invalids": Array [ - "", + Infinity, + -Infinity, ], - "type": "string", + "type": "number", }, }, "type": "object", diff --git a/app/api/entities/endpointSchema.js b/app/api/entities/endpointSchema.js index aaf09c3c58..3eddd0fc04 100644 --- a/app/api/entities/endpointSchema.js +++ b/app/api/entities/endpointSchema.js @@ -62,11 +62,12 @@ const saveSchema = Joi.object().keys({ }) })), attachments: Joi.array().items(Joi.object().keys({ + _id: Joi.string(), originalname: Joi.string(), filename: Joi.string(), mimetype: Joi.string(), size: Joi.number(), - timestamp: Joi.string(), + timestamp: Joi.number() })), creationDate: Joi.number(), processed: Joi.boolean(), diff --git a/app/api/entities/specs/__snapshots__/routes.spec.js.snap b/app/api/entities/specs/__snapshots__/routes.spec.js.snap index c3d8b4d777..eb045f74e1 100644 --- a/app/api/entities/specs/__snapshots__/routes.spec.js.snap +++ b/app/api/entities/specs/__snapshots__/routes.spec.js.snap @@ -432,6 +432,12 @@ Object { "items": Array [ Object { "children": Object { + "_id": Object { + "invalids": Array [ + "", + ], + "type": "string", + }, "filename": Object { "invalids": Array [ "", @@ -459,9 +465,10 @@ Object { }, "timestamp": Object { "invalids": Array [ - "", + Infinity, + -Infinity, ], - "type": "string", + "type": "number", }, }, "type": "object", diff --git a/app/api/migrations/migrations/12-add-RTL-to-settings-languages/index.js b/app/api/migrations/migrations/12-add-RTL-to-settings-languages/index.js index 81202f2fec..d0ee62fdc1 100644 --- a/app/api/migrations/migrations/12-add-RTL-to-settings-languages/index.js +++ b/app/api/migrations/migrations/12-add-RTL-to-settings-languages/index.js @@ -1,7 +1,3 @@ -export const rtlLanguagesList = [ - 'ar', 'dv', 'ha', 'he', 'ks', 'ku', 'ps', 'fa', 'ur', 'yi' -]; - export default { delta: 12, @@ -9,6 +5,8 @@ export default { description: 'Adds the missing RTL value for existing instances with RTL languages', + rtlLanguagesList: ['ar', 'dv', 'ha', 'he', 'ks', 'ku', 'ps', 'fa', 'ur', 'yi'], + async up(db) { process.stdout.write(`${this.name}...\r\n`); @@ -18,7 +16,7 @@ export default { languages = languages.map((l) => { const migratedLanguage = l; - if (rtlLanguagesList.includes(l.key)) { + if (this.rtlLanguagesList.includes(l.key)) { migratedLanguage.rtl = true; } diff --git a/app/api/migrations/migrations/12-add-RTL-to-settings-languages/specs/12-add-RTL-to-settings-languages.spec.js b/app/api/migrations/migrations/12-add-RTL-to-settings-languages/specs/12-add-RTL-to-settings-languages.spec.js index d512107b6d..07f9af7924 100644 --- a/app/api/migrations/migrations/12-add-RTL-to-settings-languages/specs/12-add-RTL-to-settings-languages.spec.js +++ b/app/api/migrations/migrations/12-add-RTL-to-settings-languages/specs/12-add-RTL-to-settings-languages.spec.js @@ -1,6 +1,6 @@ import { catchErrors } from 'api/utils/jasmineHelpers'; import testingDB from 'api/utils/testing_db'; -import migration, { rtlLanguagesList } from '../index.js'; +import migration from '../index.js'; import fixtures from './fixtures.js'; describe('migration add-RTL-to-settings-languages', () => { @@ -27,7 +27,7 @@ describe('migration add-RTL-to-settings-languages', () => { expect(rtlLanguages.length).toBe(10); rtlLanguages.forEach((language) => { - expect(rtlLanguagesList).toContain(language.key); + expect(migration.rtlLanguagesList).toContain(language.key); }); }); diff --git a/app/api/socketio/middleware.js b/app/api/socketio/middleware.js index fa2e4469a5..d7a11e0f11 100644 --- a/app/api/socketio/middleware.js +++ b/app/api/socketio/middleware.js @@ -9,7 +9,7 @@ export default (server, app) => { }); app.use((req, res, next) => { - req.io.getCurrentSessionSockets = () => { + req.getCurrentSessionSockets = () => { const sessionSockets = { sockets: [], emit(...args) { diff --git a/app/api/socketio/specs/middleware.spec.js b/app/api/socketio/specs/middleware.spec.js index 0dd6c972b3..1161b788c3 100644 --- a/app/api/socketio/specs/middleware.spec.js +++ b/app/api/socketio/specs/middleware.spec.js @@ -52,18 +52,29 @@ describe('socketio middleware', () => { }); it('should find and return an array of sockets belonging to the current cookie', () => { - let result = req.io.getCurrentSessionSockets(); + let result = req.getCurrentSessionSockets(); expect(result.sockets[0]).toBe(socket1); expect(result.sockets[1]).toBe(socket3); req.session.id = 'sessionId2'; executeMiddleware(req, res, next); - result = req.io.getCurrentSessionSockets(); + result = req.getCurrentSessionSockets(); expect(result.sockets[0]).toBe(socket2); }); + it('should isolate sockets for each requests when multiple requests are issued', () => { + const req1 = { ...req }; + const req2 = { ...req, session: { id: 'sessionId2' } }; + executeMiddleware(req1, res, next); + executeMiddleware(req2, res, next); + const req1Result = req1.getCurrentSessionSockets(); + const req2Result = req2.getCurrentSessionSockets(); + expect(req1Result.sockets).toEqual([socket1, socket3]); + expect(req2Result.sockets).toEqual([socket2]); + }); + it('should include in the result an "emit" function that emits to all the found sockets the sent message', () => { - const result = req.io.getCurrentSessionSockets(); + const result = req.getCurrentSessionSockets(); const data = { data: 'data' }; result.emit('Message', data); diff --git a/app/api/upload/routes.js b/app/api/upload/routes.js index a65a03a7b2..4bb3d24bd4 100644 --- a/app/api/upload/routes.js +++ b/app/api/upload/routes.js @@ -65,7 +65,7 @@ export default (app) => { const file = req.files[0].destination + req.files[0].filename; - const sessionSockets = req.io.getCurrentSessionSockets(); + const sessionSockets = req.getCurrentSessionSockets(); sessionSockets.emit('conversionStart', req.body.document); debugLog.debug(`Starting conversion of: ${req.files[0].originalname}`); return Promise.all([ @@ -106,7 +106,7 @@ export default (app) => { .then(() => { debugLog.debug('Saving documents'); return entities.saveMultiple(docs.map(doc => ({ ...doc, file: { ...doc.file, timestamp: Date.now() } }))).then(() => { - const sessionSockets = req.io.getCurrentSessionSockets(); + const sessionSockets = req.getCurrentSessionSockets(); sessionSockets.emit('documentProcessed', req.body.document); }); }); @@ -120,7 +120,7 @@ export default (app) => { entities.saveMultiple(docs.map(doc => ({ ...doc, processed: false }))); }); - const sessionSockets = req.io.getCurrentSessionSockets(); + const sessionSockets = req.getCurrentSessionSockets(); sessionSockets.emit('conversionFailed', req.body.document); }); diff --git a/app/api/upload/specs/routes.spec.js b/app/api/upload/specs/routes.spec.js index c61eac36ba..870fda903f 100644 --- a/app/api/upload/specs/routes.spec.js +++ b/app/api/upload/specs/routes.spec.js @@ -70,7 +70,6 @@ describe('upload routes', () => { spyOn(search, 'delete').and.returnValue(Promise.resolve()); spyOn(entities, 'indexEntities').and.returnValue(Promise.resolve()); iosocket = jasmine.createSpyObj('socket', ['emit']); - const io = { getCurrentSessionSockets: () => ({ sockets: [iosocket], emit: iosocket.emit }) }; routes = instrumentRoutes(uploadRoutes); file = { fieldname: 'file', @@ -82,7 +81,15 @@ describe('upload routes', () => { path: `${__dirname}/uploads/f2082bf51b6ef839690485d7153e847a.pdf`, size: 171411271 }; - req = { language: 'es', user: 'admin', headers: {}, body: { document: 'sharedId1' }, files: [file], io }; + req = { + language: 'es', + user: 'admin', + headers: {}, + body: { document: 'sharedId1' }, + files: [file], + io: {}, + getCurrentSessionSockets: () => ({ sockets: [iosocket], emit: iosocket.emit }) + }; db.clearAllAndLoad(fixtures).then(done).catch(catchErrors(done)); spyOn(errorLog, 'error'); //just to avoid annoying console output diff --git a/app/api/users/specs/users.spec.js b/app/api/users/specs/users.spec.js index 2b3c5caa84..74d324f7ff 100644 --- a/app/api/users/specs/users.spec.js +++ b/app/api/users/specs/users.spec.js @@ -32,6 +32,16 @@ describe('Users', () => { }) .catch(catchErrors(done))); + it('should not save a null password on update', async () => { + const user = { _id: recoveryUserId, role: 'admin' }; + + const [userInDb] = await users.get(recoveryUserId, '+password'); + await users.save(user, { _id: userId, role: 'admin' }); + const [updatedUser] = await users.get(recoveryUserId, '+password'); + + expect(updatedUser.password.toString()).toBe(userInDb.password.toString()); + }); + describe('when you try to change role', () => { it('should be an admin', (done) => { currentUser = { _id: userId, role: 'editor' }; diff --git a/app/api/users/users.js b/app/api/users/users.js index b2a1d4b57e..9ba54464f4 100644 --- a/app/api/users/users.js +++ b/app/api/users/users.js @@ -69,21 +69,20 @@ const sendAccountLockedEmail = (user, domain) => { }; export default { - save(user, currentUser) { - return model.get({ _id: user._id }) - .then(async ([userInTheDatabase]) => { - if (user._id === currentUser._id.toString() && user.role !== currentUser.role) { - return Promise.reject(createError('Can not change your own role', 403)); - } + async save(user, currentUser) { + const [userInTheDatabase] = await model.get({ _id: user._id }, '+password'); - if (user.hasOwnProperty('role') && user.role !== userInTheDatabase.role && currentUser.role !== 'admin') { - return Promise.reject(createError('Unauthorized', 403)); - } + if (user._id === currentUser._id.toString() && user.role !== currentUser.role) { + return Promise.reject(createError('Can not change your own role', 403)); + } + + if (user.hasOwnProperty('role') && user.role !== userInTheDatabase.role && currentUser.role !== 'admin') { + return Promise.reject(createError('Unauthorized', 403)); + } - return model.save({ - ...user, - password: user.password ? await encryptPassword(user.password) : undefined, - }); + return model.save({ + ...user, + password: user.password ? await encryptPassword(user.password) : userInTheDatabase.password, }); }, diff --git a/app/react/App/scss/elements/_controls.scss b/app/react/App/scss/elements/_controls.scss index afc5ae3bdc..927cb15d4c 100644 --- a/app/react/App/scss/elements/_controls.scss +++ b/app/react/App/scss/elements/_controls.scss @@ -228,7 +228,7 @@ input[type="checkbox"][disabled] ~ label { cursor: not-allowed; } -.icon-selector { +.icon-selector, .clear-field-button { button { background: none; border: none; diff --git a/app/react/Forms/components/Geolocation.js b/app/react/Forms/components/Geolocation.js index 720780a0e9..56bae5d6f5 100644 --- a/app/react/Forms/components/Geolocation.js +++ b/app/react/Forms/components/Geolocation.js @@ -1,6 +1,12 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Map from 'app/Map/Map'; +import { Translate } from 'app/I18N'; + +function isCoordValid(coord) { + // eslint-disable-next-line no-restricted-globals + return typeof coord === 'number' && !isNaN(coord); +} export default class Geolocation extends Component { constructor(props) { @@ -8,9 +14,14 @@ export default class Geolocation extends Component { this.latChange = this.latChange.bind(this); this.lonChange = this.lonChange.bind(this); this.mapClick = this.mapClick.bind(this); + this.clearCoordinates = this.clearCoordinates.bind(this); } onChange(value) { + if (!isCoordValid(value.lat) && !isCoordValid(value.lon)) { + this.props.onChange(); + return; + } this.props.onChange(value); } @@ -28,6 +39,10 @@ export default class Geolocation extends Component { this.onChange({ lat: parseFloat(event.lngLat[1]), lon: parseFloat(event.lngLat[0]) }); } + clearCoordinates() { + this.props.onChange(); + } + render() { const markers = []; const { value } = this.props; @@ -39,7 +54,7 @@ export default class Geolocation extends Component { lon = value.lon; } - if (lat && lon) { + if (isCoordValid(lat) && isCoordValid(lon)) { markers.push({ latitude: parseFloat(value.lat), longitude: parseFloat(value.lon) }); } return ( @@ -55,6 +70,13 @@ export default class Geolocation extends Component { + { (isCoordValid(lat) || isCoordValid(lon)) && ( +