diff --git a/services/web/app/src/Features/Project/ProjectEditorHandler.js b/services/web/app/src/Features/Project/ProjectEditorHandler.js index 3d3d300e66..f01f9afe12 100644 --- a/services/web/app/src/Features/Project/ProjectEditorHandler.js +++ b/services/web/app/src/Features/Project/ProjectEditorHandler.js @@ -4,7 +4,7 @@ const Path = require('path') const Features = require('../../infrastructure/Features') module.exports = ProjectEditorHandler = { - trackChangesAvailable: false, + trackChangesAvailable: true, buildProjectModelView( project, diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index bd0730d5d0..d3899cab72 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1030,6 +1030,7 @@ module.exports = { 'launchpad', 'server-ce-scripts', 'user-activate', + 'track-changes', ], viewIncludes: {}, diff --git a/services/web/modules/track-changes/app/src/TrackChangesController.mjs b/services/web/modules/track-changes/app/src/TrackChangesController.mjs new file mode 100644 index 0000000000..931ce06bfa --- /dev/null +++ b/services/web/modules/track-changes/app/src/TrackChangesController.mjs @@ -0,0 +1,172 @@ +import ChatApiHandler from '../../../../app/src/Features/Chat/ChatApiHandler.js' +import ChatManager from '../../../../app/src/Features/Chat/ChatManager.js' +import EditorRealTimeController from '../../../../app/src/Features/Editor/EditorRealTimeController.js' +import SessionManager from '../../../../app/src/Features/Authentication/SessionManager.js' +import UserInfoManager from '../../../../app/src/Features/User/UserInfoManager.js' +import UserInfoController from '../../../../app/src/Features/User/UserInfoController.js' +import DocstoreManager from '../../../../app/src/Features/Docstore/DocstoreManager.js' +import DocumentUpdaterHandler from '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js' +import CollaboratorsGetter from '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js' +import { Project } from '../../../../app/src/models/Project.js' +import pLimit from 'p-limit' + +function _transformId(doc) { + if (doc._id) { + doc.id = doc._id + delete doc._id + } + return doc +} + +const TrackChangesController = { + async trackChanges(req, res, next) { + try { + 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 + await Project.updateOne({_id: project_id}, {track_changes: state}).exec() //do not wait? + EditorRealTimeController.emitToRoom(project_id, 'toggle-track-changes', state) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async acceptChanges(req, res, next) { + try { + const { project_id, doc_id } = req.params + const change_ids = req.body.change_ids + EditorRealTimeController.emitToRoom(project_id, 'accept-changes', doc_id, change_ids) + await DocumentUpdaterHandler.promises.acceptChanges(project_id, doc_id, change_ids) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async getAllRanges(req, res, next) { + try { + const { project_id } = req.params + await DocumentUpdaterHandler.promises.flushProjectToMongo(project_id) + const ranges = await DocstoreManager.promises.getAllRanges(project_id) + res.json(ranges.map(_transformId)) + } catch (err) { + next(err) + } + }, + async getChangesUsers(req, res, next) { +// This route was previously used by the frontend to retrieve names of users who made changes or comments. +// review-panel-new no longer needs this for comments, but still relies on it for changes - +// although the frontend knows the names of the current owner and members, it depends on the data +// provided here to assign names to authors who have left the project but still have unaccepted changes. + try { + const { project_id } = req.params + const memberIds = await DocstoreManager.promises.getTrackedChangesUserIds(project_id) + const limit = pLimit(3) + const users = await Promise.all( + memberIds.map(memberId => + limit(async () => { + const user = await UserInfoManager.promises.getPersonalInfo(memberId) + return UserInfoController.formatPersonalInfo(user) + }) + ) + ) + res.json(users) + } catch (err) { + next(err) + } + }, + async getThreads(req, res, next) { + try { + const { project_id } = req.params + const messages = await ChatApiHandler.promises.getThreads(project_id) + await ChatManager.promises.injectUserInfoIntoThreads(messages) + res.json(messages) + } catch (err) { + next(err) + } + }, + async sendComment(req, res, next) { + try { + const { project_id, thread_id } = req.params + const { content } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + const message = await ChatApiHandler.promises.sendComment(project_id, thread_id, user_id, content) + const user = await UserInfoManager.promises.getPersonalInfo(user_id) + message.user = UserInfoController.formatPersonalInfo(user) + EditorRealTimeController.emitToRoom(project_id, 'new-comment', thread_id, message) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async editMessage(req, res, next) { + try { + const { project_id, thread_id, message_id } = req.params + const { content } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + await ChatApiHandler.promises.editMessage(project_id, thread_id, message_id, user_id, content) + EditorRealTimeController.emitToRoom(project_id, 'edit-message', thread_id, message_id, content) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async deleteMessage(req, res, next) { + try { + const { project_id, thread_id, message_id } = req.params + await ChatApiHandler.promises.deleteMessage(project_id, thread_id, message_id) + EditorRealTimeController.emitToRoom(project_id, 'delete-message', thread_id, message_id) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async resolveThread(req, res, next) { + try { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + const user = await UserInfoManager.promises.getPersonalInfo(user_id) + await ChatApiHandler.promises.resolveThread(project_id, thread_id, user_id) + EditorRealTimeController.emitToRoom( + project_id, + 'resolve-thread', + thread_id, + UserInfoController.formatPersonalInfo(user) + ) + await DocumentUpdaterHandler.promises.resolveThread(project_id, doc_id, thread_id, user_id) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async reopenThread(req, res, next) { + try { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + await ChatApiHandler.promises.reopenThread(project_id, thread_id) + EditorRealTimeController.emitToRoom(project_id, 'reopen-thread', thread_id) + await DocumentUpdaterHandler.promises.reopenThread(project_id, doc_id, thread_id, user_id) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async deleteThread(req, res, next) { + try { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + await ChatApiHandler.promises.deleteThread(project_id, thread_id) + EditorRealTimeController.emitToRoom(project_id, 'delete-thread', thread_id) + await DocumentUpdaterHandler.promises.deleteThread(project_id, doc_id, thread_id, user_id) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, +} + +export default TrackChangesController diff --git a/services/web/modules/track-changes/app/src/TrackChangesRouter.mjs b/services/web/modules/track-changes/app/src/TrackChangesRouter.mjs new file mode 100644 index 0000000000..21d73f2ad4 --- /dev/null +++ b/services/web/modules/track-changes/app/src/TrackChangesRouter.mjs @@ -0,0 +1,76 @@ +import logger from '@overleaf/logger' +import AuthorizationMiddleware from '../../../../app/src/Features/Authorization/AuthorizationMiddleware.js' +import TrackChangesController from './TrackChangesController.mjs' + +export default { + 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 + ) + } +} diff --git a/services/web/modules/track-changes/index.mjs b/services/web/modules/track-changes/index.mjs new file mode 100644 index 0000000000..1ddf76966f --- /dev/null +++ b/services/web/modules/track-changes/index.mjs @@ -0,0 +1,3 @@ +import TrackChangesRouter from './app/src/TrackChangesRouter.mjs' +const TrackChangesModule = { router: TrackChangesRouter } +export default TrackChangesModule