diff --git a/services/web/app/src/Features/Project/ProjectEditorHandler.js b/services/web/app/src/Features/Project/ProjectEditorHandler.js index a0d82a3bc95..eb9ea958bef 100644 --- a/services/web/app/src/Features/Project/ProjectEditorHandler.js +++ b/services/web/app/src/Features/Project/ProjectEditorHandler.js @@ -8,7 +8,7 @@ function mergeDeletedDocs(a, b) { } module.exports = ProjectEditorHandler = { - trackChangesAvailable: false, + trackChangesAvailable: true, buildProjectModelView(project, members, invites, deletedDocsFromDocstore) { let owner, ownerFeatures @@ -58,7 +58,7 @@ module.exports = ProjectEditorHandler = { references: false, referencesSearch: false, mendeley: false, - trackChanges: false, + trackChanges: true, trackChangesVisible: ProjectEditorHandler.trackChangesAvailable, symbolPalette: false, }) diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 48574e17c61..033eaf56170 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -971,6 +971,7 @@ module.exports = { 'server-ce-scripts', 'user-activate', 'saml-bypass', + 'track-changes', ], viewIncludes: {}, diff --git a/services/web/modules/track-changes/app/src/TrackChangesController.js b/services/web/modules/track-changes/app/src/TrackChangesController.js new file mode 100644 index 00000000000..3c8093bed5c --- /dev/null +++ b/services/web/modules/track-changes/app/src/TrackChangesController.js @@ -0,0 +1,308 @@ +const ChatApiHandler = require('../../../../app/src/Features/Chat/ChatApiHandler') +const ChatManager = require('../../../../app/src/Features/Chat/ChatManager') +const EditorRealTimeController = require('../../../../app/src/Features/Editor/EditorRealTimeController') +const SessionManager = require('../../../../app/src/Features/Authentication/SessionManager') +const UserInfoManager = require('../../../../app/src/Features/User/UserInfoManager') +const DocstoreManager = require('../../../../app/src/Features/Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler') +const CollaboratorsGetter = require('../../../../app/src/Features/Collaborators/CollaboratorsGetter') +const { Project } = require('../../../../app/src/models/Project') +const pLimit = require('p-limit') + +async function _updateTCState (projectId, state, callback) { + await Project.updateOne({_id: projectId}, {track_changes: state}).exec() + callback() +} +function _transformId(doc) { + if (doc._id) { + doc.id = doc._id; + delete doc._id; + } + return doc; +} + +const TrackChangesController = { + trackChanges(req, res, next) { + const { project_id } = req.params + let state = req.body.on || req.body.on_for + if ( req.body.on_for_guests && !req.body.on ) state.__guests__ = true + + return _updateTCState(project_id, state, + function (err, message) { + if (err != null) { + return next(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'toggle-track-changes', + state + ) + return res.sendStatus(204) + } + ) + }, + acceptChanges(req, res, next) { + const { project_id, doc_id } = req.params + const change_ids = req.body.change_ids + return DocumentUpdaterHandler.acceptChanges( + project_id, + doc_id, + change_ids, + function (err, message) { + if (err != null) { + return next(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'accept-changes', + doc_id, + change_ids, + ) + return res.sendStatus(204) + } + ) + }, + async getAllRanges(req, res, next) { + const { project_id } = req.params + // FIXME: ranges are from mongodb, probably already outdated + const ranges = await DocstoreManager.promises.getAllRanges(project_id) +// frontend expects 'id', not '_id' + return res.json(ranges.map(_transformId)) + }, + async getChangesUsers(req, res, next) { + const { project_id } = req.params + const memberIds = await CollaboratorsGetter.promises.getMemberIds(project_id) + // FIXME: Does not work properly if the user is no longer a member of the project + // memberIds from DocstoreManager.getAllRanges(project_id) is not a remedy + // because ranges are not updated in real-time + const limit = pLimit(3) + const users = await Promise.all( + memberIds.map(memberId => + limit(async () => { + const user = await UserInfoManager.promises.getPersonalInfo(memberId) + return user + }) + ) + ) + users.push({_id: null}) // An anonymous user won't cause any harm +// frontend expects 'id', not '_id' + return res.json(users.map(_transformId)) + }, + getThreads(req, res, next) { + const { project_id } = req.params + return ChatApiHandler.getThreads( + project_id, + function (err, messages) { + if (err != null) { + return next(err) + } + return ChatManager.injectUserInfoIntoThreads( + messages, + function (err) { + if (err != null) { + return next(err) + } + return res.json(messages) + } + ) + } + ) + }, + sendComment(req, res, next) { + const { project_id, thread_id } = req.params + const { content } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + if (user_id == null) { + const err = new Error('no logged-in user') + return next(err) + } + return ChatApiHandler.sendComment( + project_id, + thread_id, + user_id, + content, + function (err, message) { + if (err != null) { + return next(err) + } + return UserInfoManager.getPersonalInfo( + user_id, + function (err, user) { + if (err != null) { + return next(err) + } + message.user = user + EditorRealTimeController.emitToRoom( + project_id, + 'new-comment', + thread_id, message + ) + return res.sendStatus(204) + } + ) + } + ) + }, + editMessage(req, res, next) { + const { project_id, thread_id, message_id } = req.params + const { content } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + if (user_id == null) { + const err = new Error('no logged-in user') + return next(err) + } + return ChatApiHandler.editMessage( + project_id, + thread_id, + message_id, + user_id, + content, + function (err, message) { + if (err != null) { + return next(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'edit-message', + thread_id, + message_id, + content + ) + return res.sendStatus(204) + } + ) + }, + deleteMessage(req, res, next) { + const { project_id, thread_id, message_id } = req.params + return ChatApiHandler.deleteMessage( + project_id, + thread_id, + message_id, + function (err, message) { + if (err != null) { + return next(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'delete-message', + thread_id, + message_id + ) + return res.sendStatus(204) + } + ) + }, + resolveThread(req, res, next) { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (user_id == null) { + const err = new Error('no logged-in user') + return next(err) + } + DocumentUpdaterHandler.resolveThread( + project_id, + doc_id, + thread_id, + user_id, + function (err, message) { + if (err != null) { + return next(err) + } + } + ) + return ChatApiHandler.resolveThread( + project_id, + thread_id, + user_id, + function (err, message) { + if (err != null) { + return next(err) + } + return UserInfoManager.getPersonalInfo( + user_id, + function (err, user) { + if (err != null) { + return next(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'resolve-thread', + thread_id, + user + ) + return res.sendStatus(204) + } + ) + } + ) + }, + reopenThread(req, res, next) { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (user_id == null) { + const err = new Error('no logged-in user') + return next(err) + } + DocumentUpdaterHandler.reopenThread( + project_id, + doc_id, + thread_id, + user_id, + function (err, message) { + if (err != null) { + return next(err) + } + } + ) + return ChatApiHandler.reopenThread( + project_id, + thread_id, + function (err, message) { + if (err != null) { + return next(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'reopen-thread', + thread_id + ) + return res.sendStatus(204) + } + ) + }, + deleteThread(req, res, next) { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (user_id == null) { + const err = new Error('no logged-in user') + return next(err) + } + return DocumentUpdaterHandler.deleteThread( + project_id, + doc_id, + thread_id, + user_id, + function (err, message) { + if (err != null) { + return next(err) + } + ChatApiHandler.deleteThread( + project_id, + thread_id, + function (err, message) { + if (err != null) { + return next(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'delete-thread', + thread_id + ) + return res.sendStatus(204) + } + ) + } + ) + }, +} +module.exports = TrackChangesController \ No newline at end of file diff --git a/services/web/modules/track-changes/app/src/TrackChangesRouter.js b/services/web/modules/track-changes/app/src/TrackChangesRouter.js new file mode 100644 index 00000000000..ada573153cd --- /dev/null +++ b/services/web/modules/track-changes/app/src/TrackChangesRouter.js @@ -0,0 +1,72 @@ +const logger = require('@overleaf/logger') +const AuthorizationMiddleware = require('../../../../app/src/Features/Authorization/AuthorizationMiddleware') +const TrackChangesController = require('./TrackChangesController') + +module.exports = { + apply(webRouter) { + logger.debug({}, 'Init track-changes router') + + webRouter.post('/project/:project_id/track_changes', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.trackChanges + ) + webRouter.post('/project/:project_id/doc/:doc_id/changes/accept', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.acceptChanges + ) + webRouter.get('/project/:project_id/ranges', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.getAllRanges + ) + webRouter.get('/project/:project_id/changes/users', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.getChangesUsers + ) + webRouter.get( + '/project/:project_id/threads', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.getThreads + ) + webRouter.post( + '/project/:project_id/thread/:thread_id/messages', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.sendComment + ) + webRouter.post( + '/project/:project_id/thread/:thread_id/messages/:message_id/edit', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.editMessage + ) + webRouter.delete( + '/project/:project_id/thread/:thread_id/messages/:message_id', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.deleteMessage + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/thread/:thread_id/resolve', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.resolveThread + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/thread/:thread_id/reopen', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.reopenThread + ) + webRouter.delete( + '/project/:project_id/doc/:doc_id/thread/:thread_id', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.deleteThread + ) + }, +} \ No newline at end of file diff --git a/services/web/modules/track-changes/index.js b/services/web/modules/track-changes/index.js new file mode 100644 index 00000000000..025126c4363 --- /dev/null +++ b/services/web/modules/track-changes/index.js @@ -0,0 +1,2 @@ +const TrackChangesRouter = require('./app/src/TrackChangesRouter') +module.exports = { router : TrackChangesRouter } \ No newline at end of file