diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js index 5b59eb9ac..1445f316a 100644 --- a/src/controller/registry-org.controller/error.js +++ b/src/controller/registry-org.controller/error.js @@ -98,6 +98,27 @@ class RegistryOrgControllerError extends idrErr.IDRError { err.message = 'Parameters were invalid: conversation must be an object with a body.' return err } + + conversationDne (shortname, index) { + const err = {} + err.error = 'CONVERSATION_DNE' + err.message = `The conversation at index ${index} does not exist for the ${shortname} organization.` + return err + } + + notAllowedToEditConversation () { + const err = {} + err.error = 'NOT_ALLOWED_TO_EDIT_CONVERSATION' + err.message = 'You must be the original author or Secretariat to edit this conversation.' + return err + } + + notAllowedToChangeConversationVisibility () { + const err = {} + err.error = 'NOT_ALLOWED_TO_CHANGE_CONVERSATION_VISIBILITY' + err.message = 'Only the Secretariat is allowed to change the visibility of a conversation.' + return err + } } module.exports = { diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 7ed9c3f1e..551a0a814 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -594,6 +594,89 @@ async function createUserByOrg (req, res, next) { } } +/** + * Updates the conversation at the provided index for the given organization. + * + * @async + * @function editConversationForOrg + * @param {object} req - The Express request object, containing the organization shortname in `req.ctx.params.shortname` and conversation updates in `req.ctx.body`. + * @param {object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Promise} - A promise that resolves when the response is sent. + * @description User must be the original author of the conversation or the Secretariat role. + * The original author is allowed to update the conversation message body. + * Secretariat is allowed to update the conversation message body and visibility. + * Called by PUT /api/registryOrg/:shortname/conversation/:index + */ +async function editConversationForOrg (req, res, next) { + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() + const conversationRepo = req.ctx.repositories.getConversationRepository() + const requesterUsername = req.ctx.user + const orgShortName = req.ctx.params.shortname + const index = req.params.index + const incomingParameters = req.ctx.body + let returnValue + + const session = await mongoose.startSession({ causalConsistency: false }) + try { + // Check if org exists + const orgUUID = await orgRepo.getOrgUUID(orgShortName, {}, false) + if (!orgUUID) { + logger.info({ uuid: req.ctx.uuid, message: 'The conversation could not be edited because ' + orgShortName + ' organization does not exist.' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + try { + session.startTransaction() + // Fetch conversation + const conversation = conversationRepo.findByTargetUUIDAndIndex(orgUUID, index, { session }) + if (!conversation) { + logger.info({ uuid: req.ctx.uuid, message: `The conversation at index ${index} does not exist for the ${orgShortName} organization.` }) + return res.status(404).json(error.conversationDne(orgShortName, index)) + } + + // Check if user has permissions to edit conversation + const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org, { session }) + const userUUID = await userRepo.getUserUUID(requesterUsername, orgShortName, { session }) + if (conversation.author_id !== userUUID && !isSecretariat) { + logger.info({ uuid: req.ctx.uuid, message: 'The user does not have permission to edit this conversation.' }) + return res.status(403).json(error.notAllowedToEditConversation()) + } + + // Check if user has permission to change visibility of conversation + if (incomingParameters.visibility && !isSecretariat) { + logger.info({ uuid: req.ctx.uuid, message: 'Only the Secretariat is allowed to change the visibility of a conversation.' }) + return res.status(403).json(error.notAllowedToChangeConversationVisibility()) + } + + // Make the edit + returnValue = await conversationRepo.editConversation(conversation.UUID, incomingParameters, userUUID, { session }) + } catch (error) { + await session.abortTransaction() + throw error + } finally { + await session.endSession() + } + + const responseMessage = { + message: 'The conversation was successfully updated.', + updated: returnValue + } + + const payload = { + action: 'update_org_conversation', + change: `Conversation at index ${index} for org ${orgShortName} was successfully updated.`, + req_UUID: req.ctx.uuid, + org_UUID: orgUUID + } + logger.info(JSON.stringify(payload)) + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + module.exports = { ALL_ORGS: getAllOrgs, SINGLE_ORG: getOrg, @@ -601,5 +684,6 @@ module.exports = { UPDATE_ORG: updateOrg, DELETE_ORG: deleteOrg, USER_ALL: getUsers, - USER_CREATE_SINGLE: createUserByOrg + USER_CREATE_SINGLE: createUserByOrg, + EDIT_CONVERSATION: editConversationForOrg } diff --git a/src/model/conversation.js b/src/model/conversation.js index ef7c2767b..fe304bf8f 100644 --- a/src/model/conversation.js +++ b/src/model/conversation.js @@ -12,7 +12,9 @@ const schema = { author_role: String, visibility: String, body: String, - posted_at: Date + posted_at: Date, + edited_at: Date, + editor_id: String } const ConversationSchema = new mongoose.Schema(schema, { collection: 'Conversation', timestamps: { createdAt: 'posted_at', updatedAt: 'last_updated' } }) diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 9d7fe0849..2e23a2c1c 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -36,10 +36,27 @@ class ConversationRepository extends BaseRepository { } async getAllByTargetUUID (targetUUID, isSecretariat, options = {}) { - const conversations = await ConversationModel.find({ target_uuid: targetUUID }, null, options) + const conversations = await ConversationModel.find({ target_uuid: targetUUID }, null, { + ...options, + sort: { + posted_at: 1, + UUID: 1 + } + }) return conversations.map(convo => convo.toObject()).filter(conv => isSecretariat || conv.visibility === 'public') } + async findByTargetUUIDAndIndex (targetUUID, index, options = {}) { + const conversation = await ConversationModel.find({ target_uuid: targetUUID }, null, { + ...options, + sort: { + posted_at: 1, + UUID: 1 + } + }).skip(index).limit(1) + return conversation.toObject() + } + async createConversation (targetUUID, body, user, isSecretariat, options = {}) { const { getUserFullName } = require('../utils/utils') const newUUID = uuid.v4() @@ -57,6 +74,8 @@ class ConversationRepository extends BaseRepository { author_id: user.UUID, author_name: getUserFullName(user), author_role: isSecretariat ? 'Secretariat' : 'Partner', + editor_id: null, + edited_at: null, visibility: !isSecretariat ? 'public' : (['public', 'private'].includes(body.visibility?.toLowerCase()) ? body.visibility.toLowerCase() : 'private'), body: body.body } @@ -64,6 +83,20 @@ class ConversationRepository extends BaseRepository { const result = await newConversation.save(options) return result.toObject() } + + async editConversation (UUID, incomingParameters, userUUID, options = {}) { + const conversation = this.findOneByUUID(UUID, options) + if (incomingParameters?.body) { + conversation.body = incomingParameters.body + } + if (incomingParameters?.visibility) { + conversation.visibility = incomingParameters.visibility + } + conversation.editor_id = userUUID + conversation.edited_at = Date.now() + const result = await conversation.save(options) + return result.toObject() + } } module.exports = ConversationRepository