Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/controller/conversation.controller/conversation.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const mongoose = require('mongoose')
const logger = require('../../middleware/logger')
const getConstants = require('../../../src/constants').getConstants
const CONSTANTS = getConstants()
const errors = require('./error')
const error = new errors.ConversationControllerError()

async function getAllConversations (req, res, next) {
const repo = req.ctx.repositories.getConversationRepository()
Expand Down Expand Up @@ -42,8 +44,8 @@ async function createConversationForTargetUUID (req, res, next) {

const user = await userRepo.findOneByUsernameAndOrgShortname(requesterUsername, requesterOrg, { session })

if (!body.body) {
return res.status(400).json({ message: 'Missing required field body' })
if (typeof body !== 'object' || !body.body || !repo.validateConversation(body)) {
return res.status(400).json(error.invalidConversationObject())
}

const result = await repo.createConversation(targetUUID, body, user, true, { session })
Expand Down Expand Up @@ -73,8 +75,56 @@ async function createConversationForTargetUUID (req, res, next) {
}
}

async function updateConversationByUUID (req, res, next) {
const session = await mongoose.startSession()

try {
session.startTransaction()

const repo = req.ctx.repositories.getConversationRepository()
const conversationUUID = req.params.uuid
const body = req.body

// Check if conversation exists
const conversation = await repo.findOneByUUID(conversationUUID, { session })
if (!conversation) {
logger.info({ uuid: req.ctx.uuid, message: `No conversation found with UUID ${conversationUUID}` })
return res.status(404).json(error.conversationDne(conversationUUID))
}

// Validate body
if (typeof body !== 'object' || !(body.body || body.visibility) || !repo.validateConversation(body)) {
logger.info({ uuid: req.ctx.uuid, message: 'The conversation could not be edited because the request body was invalid.' })
return res.status(400).json(error.invalidConversationEditObject())
}

const result = await repo.editConversation(conversationUUID, body, { session })
await session.commitTransaction()
return res.status(200).json(result)
} catch (err) {
if (session && session.inTransaction()) {
await session.abortTransaction()
}
next(err)
} finally {
if (session && session.id) {
// Check if session is still valid before trying to end
try {
await session.endSession()
} catch (sessionEndError) {
logger.error({
uuid: req.ctx.uuid,
message: 'Error ending session in finally block',
error: sessionEndError
})
}
}
}
}

module.exports = {
getAllConversations,
getConversationsForTargetUUID,
createConversationForTargetUUID
createConversationForTargetUUID,
updateConversationByUUID
}
49 changes: 49 additions & 0 deletions src/controller/conversation.controller/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const idrErr = require('../../utils/error')

class ConversationControllerError extends idrErr.IDRError {
conversationDne (uuid) {
const err = {}
err.error = 'CONVERSATION_DNE'
err.message = `The conversation with UUID ${uuid} does not exist.`
return err
}

conversationIndexDne (shortname, index) {
const err = {}
err.error = 'CONVERSATION_INDEX_DNE'
err.message = `No conversation exists at index ${index} 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
}

invalidConversationObject () {
const err = {}
err.error = 'BAD_INPUT'
err.message = "Parameters were invalid: conversation object must include property 'body' (string) and optionally 'visibility' ('public' or 'private')."
return err
}

invalidConversationEditObject () {
const err = {}
err.error = 'BAD_INPUT'
err.message = "Parameters were invalid: conversation object must include at least one of the following properties: 'body' (string) or 'visibility' ('public' or 'private')."
return err
}
}

module.exports = {
ConversationControllerError
}
95 changes: 95 additions & 0 deletions src/controller/conversation.controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,99 @@ router.post('/conversation/target/:uuid',
controller.createConversationForTargetUUID
)

// Update conversation - SEC only
router.put('/conversation/:uuid',
/*
#swagger.tags = ['Conversation']
#swagger.operationId = 'updateConversationByUUID'
#swagger.summary = "Updates a conversation by UUID (accessible to Secretariat only)"
#swagger.description = "
<h2>Access Control</h2>
<p>User must belong to an organization with the <b>Secretariat</b> role</p>
<h2>Expected Behavior</h2>
<p><b>Secretariat:</b> Updates the conversation with the specified UUID</p>"
#swagger.parameters['uuid'] = { description: 'The UUID of the conversation to update' }
#swagger.parameters['$ref'] = [
'#/components/parameters/apiEntityHeader',
'#/components/parameters/apiUserHeader',
'#/components/parameters/apiSecretHeader'
]
#swagger.requestBody = {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
body: {
type: 'string',
description: 'The updated content of the conversation message'
},
visibility: {
type: 'string',
enum: ['private', 'public'],
description: 'The updated visibility of the conversation message'
}
}
}
}
}
}
#swagger.responses[200] = {
description: 'Returns the updated conversation',
content: {
"application/json": {
schema: {
$ref: '../schemas/conversation/conversation.json'
}
}
}
}
#swagger.responses[400] = {
description: 'Bad Request',
content: {
"application/json": {
schema: { $ref: '../schemas/errors/bad-request.json' }
}
}
}
#swagger.responses[401] = {
description: 'Not Authenticated',
content: {
"application/json": {
schema: { $ref: '../schemas/errors/generic.json' }
}
}
}
#swagger.responses[403] = {
description: 'Forbidden',
content: {
"application/json": {
schema: { $ref: '../schemas/errors/generic.json' }
}
}
}
#swagger.responses[404] = {
description: 'Not Found',
content: {
"application/json": {
schema: { $ref: '../schemas/errors/generic.json' }
}
}
}
#swagger.responses[500] = {
description: 'Internal Server Error',
content: {
"application/json": {
schema: { $ref: '../schemas/errors/generic.json' }
}
}
}
*/
mw.validateUser,
Comment thread
cberger8 marked this conversation as resolved.
Dismissed
mw.onlySecretariat,
param(['uuid']).isUUID(4),
controller.updateConversationByUUID
)

module.exports = router
28 changes: 0 additions & 28 deletions src/controller/registry-org.controller/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,34 +91,6 @@ class RegistryOrgControllerError extends idrErr.IDRError {
err.message = 'The requested user can not be created and added to the organization because the organization has hit its limit of 100 users. Contact the Secretariat.'
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
}

invalidConversationObject () {
const err = {}
err.error = 'BAD_INPUT'
err.message = 'Parameters were invalid: conversation must be an object with a body.'
return err
}
}

module.exports = {
Expand Down
36 changes: 29 additions & 7 deletions src/controller/registry-org.controller/registry-org.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const { getConstants } = require('../../constants')
const _ = require('lodash')
const errors = require('./error')
const error = new errors.RegistryOrgControllerError()
const conversationErrors = require('../conversation.controller/error')
const convoError = new conversationErrors.ConversationControllerError()
const validateUUID = require('uuid').validate

/**
Expand Down Expand Up @@ -241,10 +243,6 @@ async function updateOrg (req, res, next) {
let updatedOrg
let jointApprovalRequired

if (conversation && (typeof conversation !== 'object' || !conversation.body)) {
return res.status(400).json(error.invalidConversationObject())
}

try {
session.startTransaction()
const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session })
Expand Down Expand Up @@ -280,13 +278,27 @@ async function updateOrg (req, res, next) {
}
}

// Validate org
const result = repo.validateOrg(body, { session })
if (!result.isValid) {
logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' }))
await session.abortTransaction()
return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors })
}

// Validate conversation (if it exists)
if (conversation) {
if (
typeof conversation !== 'object' ||
!conversation.body ||
!conversationRepo.validateConversation(conversation)
) {
logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'Invalid conversation object.' }))
await session.abortTransaction()
return res.status(400).json(convoError.invalidConversationObject())
}
}

// Check for duplicate short_name
if (body?.short_name !== shortName && await repo.orgExists(body?.short_name, { session })) {
logger.info({
Expand Down Expand Up @@ -634,21 +646,31 @@ async function editConversationForOrg (req, res, next) {
const conversation = await 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))
return res.status(404).json(convoError.conversationIndexDne(orgShortName, index))
}

// Validate body
if (
typeof incomingParameters !== 'object' ||
!(incomingParameters.body || incomingParameters.visibility) ||
!conversationRepo.validateConversation(incomingParameters)
) {
logger.info({ uuid: req.ctx.uuid, message: 'The conversation could not be edited because the request body was invalid.' })
return res.status(400).json(convoError.invalidConversationObject())
}

// Check if user has permissions to edit conversation
const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org, { session })
const userUUID = await userRepo.getUserUUID(requesterUsername, req.ctx.org, { 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())
return res.status(403).json(convoError.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())
return res.status(403).json(convoError.notAllowedToChangeConversationVisibility())
}

// Make the edit
Expand Down
7 changes: 7 additions & 0 deletions src/repositories/conversationRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ class ConversationRepository extends BaseRepository {
return conversation[0]
}

validateConversation (conversation) {
if ((conversation.body && typeof conversation.body !== 'string') || (conversation.visibility && !['public', 'private'].includes(conversation.visibility))) {
return false
}
return true
}

async createConversation (targetUUID, body, user, isSecretariat, options = {}) {
const { getUserFullName } = require('../utils/utils')
const newUUID = uuid.v4()
Expand Down
Loading
Loading