diff --git a/app.js b/app.js index fa6e7900..89953b47 100644 --- a/app.js +++ b/app.js @@ -57,9 +57,8 @@ app.use( }) ) app.use(logger('dev')) -app.use(express.json()) +app.use(express.json({ type: ["application/json", "application/ld+json"] })) app.use(express.text()) -app.use(express.urlencoded({ extended: true })) app.use(cookieParser()) //Publicly available scripts, CSS, and HTML pages. diff --git a/controllers/bulk.js b/controllers/bulk.js index 35e7fcb5..5e019a27 100644 --- a/controllers/bulk.js +++ b/controllers/bulk.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' /** * Create many objects at once with the power of MongoDB bulkWrite() operations. @@ -22,13 +22,13 @@ const bulkCreate = async function (req, res, next) { if (!Array.isArray(documents)) { err.message = "The request body must be an array of objects." err.status = 400 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } if (documents.length === 0) { err.message = "No action on an empty array." err.status = 400 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } const gatekeep = documents.filter(d=> { @@ -46,7 +46,7 @@ const bulkCreate = async function (req, res, next) { if (gatekeep.length > 0) { err.message = "All objects in the body of a `/bulkCreate` must be JSON and must not contain a declared identifier property." err.status = 400 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } @@ -55,7 +55,7 @@ const bulkCreate = async function (req, res, next) { // if(slug){ // const slugError = await exports.generateSlugId(slug) // if(slugError){ - // next(createExpressError(slugError)) + // next(utils.createExpressError(slugError)) // return // } // else{ @@ -92,7 +92,7 @@ const bulkCreate = async function (req, res, next) { } catch (error) { //MongoServerError from the client has the following properties: index, code, keyPattern, keyValue - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -111,13 +111,13 @@ const bulkUpdate = async function (req, res, next) { if (!Array.isArray(documents)) { err.message = "The request body must be an array of objects." err.status = 400 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } if (documents.length === 0) { err.message = "No action on an empty array." err.status = 400 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } const gatekeep = documents.filter(d => { @@ -136,7 +136,7 @@ const bulkUpdate = async function (req, res, next) { if (gatekeep.length > 0) { err.message = "All objects in the body of a `/bulkUpdate` must be JSON and must contain a declared identifier property." err.status = 400 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } // unordered bulkWrite() operations have better performance metrics. @@ -154,7 +154,7 @@ const bulkUpdate = async function (req, res, next) { try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === originalObject) continue @@ -196,7 +196,7 @@ const bulkUpdate = async function (req, res, next) { } catch (error) { //MongoServerError from the client has the following properties: index, code, keyPattern, keyValue - next(createExpressError(error)) + next(utils.createExpressError(error)) } } diff --git a/controllers/crud.js b/controllers/crud.js index 7702de58..178f455c 100644 --- a/controllers/crud.js +++ b/controllers/crud.js @@ -6,7 +6,7 @@ */ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, idNegotiation, generateSlugId, ObjectID, createExpressError, getAgentClaim, parseDocumentID } from './utils.js' +import { _contextid, idNegotiation, generateSlugId, ObjectID, getAgentClaim, parseDocumentID } from './utils.js' /** * Create a new Linked Open Data object in RERUM v1. @@ -19,7 +19,7 @@ const create = async function (req, res, next) { if(req.get("Slug")){ let slug_json = await generateSlugId(req.get("Slug"), next) if(slug_json.code){ - next(createExpressError(slug_json)) + next(utils.createExpressError(slug_json)) return } else{ @@ -43,7 +43,6 @@ const create = async function (req, res, next) { delete provided["@context"] let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, provided, rerumProp, { "_id": id }) - console.log("CREATE") try { let result = await db.insertOne(newObject) res.set(utils.configureWebAnnoHeadersFor(newObject)) @@ -55,7 +54,7 @@ const create = async function (req, res, next) { } catch (error) { //MongoServerError from the client has the following properties: index, code, keyPattern, keyValue - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -75,7 +74,7 @@ const query = async function (req, res, next) { message: "Detected empty JSON object. You must provide at least one property in the /query request body JSON.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } try { @@ -84,7 +83,7 @@ const query = async function (req, res, next) { res.set(utils.configureLDHeadersFor(matches)) res.json(matches) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -116,9 +115,9 @@ const id = async function (req, res, next) { "message": `No RERUM object with id '${id}'`, "status": 404 } - next(createExpressError(err)) + next(utils.createExpressError(err)) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) } } diff --git a/controllers/delete.js b/controllers/delete.js index 12aec2ac..8002f1cf 100644 --- a/controllers/delete.js +++ b/controllers/delete.js @@ -6,7 +6,7 @@ */ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { createExpressError, getAgentClaim, parseDocumentID, getAllVersions, getAllDescendants } from './utils.js' +import { getAgentClaim, parseDocumentID, getAllVersions, getAllDescendants } from './utils.js' /** * Mark an object as deleted in the database. @@ -26,7 +26,7 @@ const deleteObj = async function(req, res, next) { try { id = req.params["_id"] ?? parseDocumentID(JSON.parse(JSON.stringify(req.body))["@id"]) ?? parseDocumentID(JSON.parse(JSON.stringify(req.body))["id"]) } catch(error){ - next(createExpressError(error)) + next(utils.createExpressError(error)) return } let agentRequestingDelete = getAgentClaim(req, next) @@ -34,7 +34,7 @@ const deleteObj = async function(req, res, next) { try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null !== originalObject) { @@ -58,7 +58,7 @@ const deleteObj = async function(req, res, next) { }) } if (err.status) { - next(createExpressError(err)) + next(utils.createExpressError(err)) return } let preserveID = safe_original["@id"] @@ -76,14 +76,14 @@ const deleteObj = async function(req, res, next) { try { result = await db.replaceOne({ "_id": originalObject["_id"] }, deletedObject) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (result.modifiedCount === 0) { //result didn't error out, the action was not performed. Sometimes, this is a neutral thing. Sometimes it is indicative of an error. err.message = "The original object was not replaced with the deleted object in the database." err.status = 500 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } //204 to say it is deleted and there is nothing in the body @@ -94,12 +94,12 @@ const deleteObj = async function(req, res, next) { //Not sure we can get here, as healHistoryTree might throw and error. err.message = "The history tree for the object being deleted could not be mended." err.status = 500 - next(createExpressError(err)) + next(utils.createExpressError(err)) return } err.message = "No object with this id could be found in RERUM. Cannot delete." err.status = 404 - next(createExpressError(err)) + next(utils.createExpressError(err)) } /** diff --git a/controllers/gog.js b/controllers/gog.js index 67dd04de..7bc72d17 100644 --- a/controllers/gog.js +++ b/controllers/gog.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' /** * THIS IS SPECIFICALLY FOR 'Gallery of Glosses' @@ -44,7 +44,7 @@ const _gog_fragments_from_manuscript = async function (req, res, next) { }) } if (err.status) { - next(createExpressError(err)) + next(utils.createExpressError(err)) return } try { @@ -138,7 +138,7 @@ const _gog_fragments_from_manuscript = async function (req, res, next) { } catch (error) { console.error(error) - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -162,7 +162,7 @@ const _gog_glosses_from_manuscript = async function (req, res, next) { const skip = parseInt(req.query.skip ?? 0) let err = { message: `` } // This request can only be made my Gallery of Glosses production apps. - if (!agentID === "61043ad4ffce846a83e700dd") { + if (agentID !== "61043ad4ffce846a83e700dd") { err = Object.assign(err, { message: `Only the Gallery of Glosses can make this request.`, status: 403 @@ -176,7 +176,7 @@ const _gog_glosses_from_manuscript = async function (req, res, next) { }) } if (err.status) { - next(createExpressError(err)) + next(utils.createExpressError(err)) return } try { @@ -300,7 +300,7 @@ const _gog_glosses_from_manuscript = async function (req, res, next) { } catch (error) { console.error(error) - next(createExpressError(error)) + next(utils.createExpressError(error)) } } diff --git a/controllers/history.js b/controllers/history.js index f0ad0031..651568d8 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, getAllVersions, getAllAncestors, getAllDescendants } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, getAllVersions, getAllAncestors, getAllDescendants } from './utils.js' /** * Public facing servlet to gather for all versions downstream from a provided `key object`. @@ -23,7 +23,7 @@ const since = async function (req, res, next) { try { obj = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === obj) { @@ -31,7 +31,7 @@ const since = async function (req, res, next) { message: `Cannot produce a history. There is no object in the database with id '${id}'. Check the URL.`, status: 404 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } let all = await getAllVersions(obj) @@ -60,7 +60,7 @@ const history = async function (req, res, next) { try { obj = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === obj) { @@ -68,7 +68,7 @@ const history = async function (req, res, next) { message: `Cannot produce a history. There is no object in the database with id '${id}'. Check the URL.`, status: 404 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } let all = await getAllVersions(obj) @@ -103,9 +103,9 @@ const idHeadRequest = async function (req, res, next) { "message": `No RERUM object with id '${id}'`, "status": 404 } - next(createExpressError(err)) + next(utils.createExpressError(err)) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -128,9 +128,9 @@ const queryHeadRequest = async function (req, res, next) { "message": `There are no objects in the database matching the query. Check the request body.`, "status": 404 } - next(createExpressError(err)) + next(utils.createExpressError(err)) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -145,7 +145,7 @@ const sinceHeadRequest = async function (req, res, next) { try { obj = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === obj) { @@ -153,7 +153,7 @@ const sinceHeadRequest = async function (req, res, next) { message: `Cannot produce a history. There is no object in the database with id '${id}'. Check the URL.`, status: 404 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } let all = await getAllVersions(obj) @@ -183,7 +183,7 @@ const historyHeadRequest = async function (req, res, next) { try { obj = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === obj) { @@ -191,7 +191,7 @@ const historyHeadRequest = async function (req, res, next) { message: "Cannot produce a history. There is no object in the database with this id. Check the URL.", status: 404 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } let all = await getAllVersions(obj) diff --git a/controllers/overwrite.js b/controllers/overwrite.js index 284fac89..e496e670 100644 --- a/controllers/overwrite.js +++ b/controllers/overwrite.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' /** * Replace some existing object in MongoDB with the JSON object in the request body. @@ -23,13 +23,12 @@ const overwrite = async function (req, res, next) { let agentRequestingOverwrite = getAgentClaim(req, next) const receivedID = objectReceived["@id"] ?? objectReceived.id if (receivedID) { - console.log("OVERWRITE") let id = parseDocumentID(receivedID) let originalObject try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === originalObject) { @@ -85,7 +84,7 @@ const overwrite = async function (req, res, next) { try { result = await db.replaceOne({ "_id": id }, newObject) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (result.modifiedCount == 0) { @@ -109,7 +108,7 @@ const overwrite = async function (req, res, next) { status: 400 }) } - next(createExpressError(err)) + next(utils.createExpressError(err)) } export { overwrite } diff --git a/controllers/patchSet.js b/controllers/patchSet.js index 85e97af8..1d1f0932 100644 --- a/controllers/patchSet.js +++ b/controllers/patchSet.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** * Update some existing object in MongoDB by adding the keys from the JSON object in the request body. @@ -32,7 +32,7 @@ const patchSet = async function (req, res, next) { try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === originalObject) { @@ -106,7 +106,7 @@ const patchSet = async function (req, res, next) { } catch (error) { //WriteError or WriteConcernError - next(createExpressError(error)) + next(utils.createExpressError(error)) return } } @@ -118,7 +118,7 @@ const patchSet = async function (req, res, next) { status: 400 }) } - next(createExpressError(err)) + next(utils.createExpressError(err)) } export { patchSet } diff --git a/controllers/patchUnset.js b/controllers/patchUnset.js index c4cf53d7..5d8f05f3 100644 --- a/controllers/patchUnset.js +++ b/controllers/patchUnset.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** * Update some existing object in MongoDB by removing the keys noted in the JSON object in the request body. @@ -31,7 +31,7 @@ const patchUnset = async function (req, res, next) { try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === originalObject) { @@ -91,7 +91,6 @@ const patchUnset = async function (req, res, next) { if(_contextid(patchedObject["@context"])) delete patchedObject.id delete patchedObject["@context"] let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) - console.log("PATCH UNSET") try { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { @@ -111,7 +110,7 @@ const patchUnset = async function (req, res, next) { } catch (error) { //WriteError or WriteConcernError - next(createExpressError(error)) + next(utils.createExpressError(error)) return } } @@ -123,7 +122,7 @@ const patchUnset = async function (req, res, next) { status: 400 }) } - next(createExpressError(err)) + next(utils.createExpressError(err)) } export { patchUnset } diff --git a/controllers/patchUpdate.js b/controllers/patchUpdate.js index c7271bbb..942c61f9 100644 --- a/controllers/patchUpdate.js +++ b/controllers/patchUpdate.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** * Update some existing object in MongoDB by changing the keys from the JSON object in the request body. @@ -30,7 +30,7 @@ const patchUpdate = async function (req, res, next) { try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === originalObject) { @@ -90,7 +90,6 @@ const patchUpdate = async function (req, res, next) { if(_contextid(patchedObject["@context"])) delete patchedObject.id delete patchedObject["@context"] let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) - console.log("PATCH UPDATE") try { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { @@ -110,7 +109,7 @@ const patchUpdate = async function (req, res, next) { } catch (error) { //WriteError or WriteConcernError - next(createExpressError(error)) + next(utils.createExpressError(error)) return } } @@ -122,7 +121,7 @@ const patchUpdate = async function (req, res, next) { status: 400 }) } - next(createExpressError(err)) + next(utils.createExpressError(err)) } export { patchUpdate } diff --git a/controllers/putUpdate.js b/controllers/putUpdate.js index 177507ac..3778dcf8 100644 --- a/controllers/putUpdate.js +++ b/controllers/putUpdate.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** * Replace some existing object in MongoDB with the JSON object in the request body. @@ -35,7 +35,7 @@ const putUpdate = async function (req, res, next) { try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (null === originalObject) { @@ -63,7 +63,6 @@ const putUpdate = async function (req, res, next) { delete objectReceived["@context"] let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) - console.log("UPDATE") try { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { @@ -83,7 +82,7 @@ const putUpdate = async function (req, res, next) { } catch (error) { //WriteError or WriteConcernError - next(createExpressError(error)) + next(utils.createExpressError(error)) return } } @@ -95,7 +94,7 @@ const putUpdate = async function (req, res, next) { status: 400 }) } - next(createExpressError(err)) + next(utils.createExpressError(err)) } /** @@ -122,7 +121,6 @@ async function _import(req, res, next) { delete objectReceived["@context"] let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) - console.log("IMPORT") try { let result = await db.insertOne(newObject) res.set(utils.configureWebAnnoHeadersFor(newObject)) @@ -134,7 +132,7 @@ async function _import(req, res, next) { } catch (error) { //MongoServerError from the client has the following properties: index, code, keyPattern, keyValue - next(createExpressError(error)) + next(utils.createExpressError(error)) } } diff --git a/controllers/release.js b/controllers/release.js index 62f26f04..66fe534e 100644 --- a/controllers/release.js +++ b/controllers/release.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, generateSlugId, establishReleasesTree, healReleasesTree } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, generateSlugId, establishReleasesTree, healReleasesTree } from './utils.js' /** * Public facing servlet to release an existing RERUM object. This will not @@ -29,7 +29,7 @@ const release = async function (req, res, next) { if(req.get("Slug")){ let slug_json = await generateSlugId(req.get("Slug"), next) if(slug_json.code){ - next(createExpressError(slug_json)) + next(utils.createExpressError(slug_json)) return } else{ @@ -42,7 +42,7 @@ const release = async function (req, res, next) { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } let safe_original = JSON.parse(JSON.stringify(originalObject)) @@ -68,10 +68,9 @@ const release = async function (req, res, next) { }) } if (err.status) { - next(createExpressError(err)) + next(utils.createExpressError(err)) return } - console.log("RELEASE") if (null !== originalObject){ safe_original["__rerum"].isReleased = new Date(Date.now()).toISOString().replace("Z", "") safe_original["__rerum"].releases.replaces = previousReleasedID @@ -103,7 +102,7 @@ const release = async function (req, res, next) { result = await db.replaceOne({ "_id": id }, releasedObject) } catch (error) { - next(createExpressError(error)) + next(utils.createExpressError(error)) return } if (result.modifiedCount == 0) { @@ -125,7 +124,7 @@ const release = async function (req, res, next) { message: "You must provide the id of an object to release. Use /release/id-here or release?_id=id-here.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } } diff --git a/controllers/search.js b/controllers/search.js index 5a688abf..071a1f58 100644 --- a/controllers/search.js +++ b/controllers/search.js @@ -6,7 +6,7 @@ */ import { db } from '../database/index.js' import utils from '../utils.js' -import { idNegotiation, createExpressError } from './utils.js' +import { idNegotiation } from './utils.js' /** * Merges and deduplicates results from multiple MongoDB Atlas Search index queries. @@ -269,7 +269,7 @@ const searchAsWords = async function (req, res, next) { message: "You did not provide text to search for in the search request.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } const limit = parseInt(req.query.limit ?? 100) @@ -287,7 +287,7 @@ const searchAsWords = async function (req, res, next) { res.json(results) } catch (error) { console.error(error) - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -357,7 +357,7 @@ const searchAsPhrase = async function (req, res, next) { message: "You did not provide text to search for in the search request.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } const limit = parseInt(req.query.limit ?? 100) @@ -375,7 +375,7 @@ const searchAsPhrase = async function (req, res, next) { res.json(results) } catch (error) { console.error(error) - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -437,7 +437,7 @@ const searchFuzzily = async function (req, res, next) { message: "You did not provide text to search for in the search request.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } const limit = parseInt(req.query.limit ?? 100) @@ -455,7 +455,7 @@ const searchFuzzily = async function (req, res, next) { res.json(results) } catch (error) { console.error(error) - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -525,7 +525,7 @@ const searchWildly = async function (req, res, next) { message: "You did not provide text to search for in the search request.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } // Require wildcards in the search text @@ -534,7 +534,7 @@ const searchWildly = async function (req, res, next) { message: "Wildcards must be used in wildcard search. Use '*' to match any characters or '?' to match a single character.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } const limit = parseInt(req.query.limit ?? 100) @@ -552,7 +552,7 @@ const searchWildly = async function (req, res, next) { res.json(results) } catch (error) { console.error(error) - next(createExpressError(error)) + next(utils.createExpressError(error)) } } @@ -629,7 +629,7 @@ const searchAlikes = async function (req, res, next) { message: "You must provide a JSON document in the request body to find similar documents.", status: 400 } - next(createExpressError(err)) + next(utils.createExpressError(err)) return } const limit = parseInt(req.query.limit ?? 100) @@ -686,7 +686,7 @@ const searchAlikes = async function (req, res, next) { res.json(results) } catch (error) { console.error(error) - next(createExpressError(error)) + next(utils.createExpressError(error)) } } diff --git a/controllers/utils.js b/controllers/utils.js index 9da47cea..602d372e 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -66,7 +66,7 @@ const idNegotiation = function (resBody) { * Check if an object with the proposed custom _id already exists. * If so, this is a 409 conflict. It will be detected downstream if we continue one by returning the proposed Slug. * We can avoid the 409 conflict downstream and return a newly minted ObjectID.toHextString() - * We error out right here with next(createExpressError({"code" : 11000})) + * We error out right here with next(utils.createExpressError({"code" : 11000})) * @param slug_id A proposed _id. * */ @@ -99,25 +99,6 @@ const index = function (req, res, next) { }) } -function createExpressError(err) { - let error = {} - if (err.code) { - switch (err.code) { - case 11000: - //Duplicate _id key error, specific to SLUG support. This is a Conflict. - error.statusMessage = `The id provided already exists. Please use a different _id or Slug.` - error.statusCode = 409 - break - default: - error.statusMessage = "There was a mongo error that prevented this request from completing successfully." - error.statusCode = 500 - } - } - error.statusCode = err.statusCode ?? err.status ?? 500 - error.statusMessage = err.statusMessage ?? err.message ?? "Detected Error" - return error -} - /** * An internal helper for removing a document from the database using a known _id or __rerums.slug. * This is not exposed over the http request and response. @@ -156,7 +137,7 @@ function getAgentClaim(req, next) { "message": "Could not get agent from req.user. Have you registered with RERUM?", "status": 403 } - next(createExpressError(err)) + return next(utils.createExpressError(err)) } function parseDocumentID(atID){ @@ -469,7 +450,6 @@ export { generateSlugId, index, ObjectID, - createExpressError, remove, getAgentClaim, parseDocumentID, diff --git a/rest.js b/rest.js index 00e61700..abc0d18c 100644 --- a/rest.js +++ b/rest.js @@ -1,11 +1,13 @@ #!/usr/bin/env node +import utils from './utils.js' + /** * This module is used for any REST support functionality. It is used as middleware and so * has access to the http module request and response objects, as well as next() - * It is in charge of responding to the client. - * - * @author thehabes + * It is in charge of responding to the client. + * + * @author thehabes */ /** @@ -24,12 +26,104 @@ const checkPatchOverrideSupport = function (req, res) { return undefined !== override && override === "PATCH" } +/** + * Detects multiple MIME types smuggled into a single Content-Type header. + * The following are the cases that should result in a 415 (not a 500) + + - application/json text/plain + - application/json, text/plain + - text/plain; application/json + - text/plain; a=b, application/json + - application/json; a=b; text/plain; + - application/json; a=b text/plain; + - application/json; charset=utf-8, text/plain + - application/json; + + * @param {string} contentType - Lowercased Content-Type header value + * @returns {boolean} True if multiple MIME types are detected + */ +const hasMultipleContentTypes = (contentType) => { + const segments = contentType.split(";") + const mimeSegment = segments[0].trim() + // No commas or spaces allowed in MIME types + if (mimeSegment.includes(",") || mimeSegment.includes(" ")) return true + // Parameter values are tokens (no spaces/commas) or quoted strings per RFC 2045. + // Commas or spaces outside quotes indicate a smuggled MIME type. + return segments.slice(1).some(segment => { + const trimmed = segment.trim() + if (!trimmed.includes("=")) return true + const withoutQuoted = trimmed.replace(/"[^"]*"/g, "") + if (withoutQuoted.includes(",") || withoutQuoted.includes(" ")) return true + return false + }) +} + +/** + * Middleware to verify Content-Type headers for endpoints receiving JSON bodies. + * Responds with a 415 Invalid Media Type for Content-Type headers that are not for JSON bodies. + * + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + */ +const verifyJsonContentType = function (req, res, next) { + const contentType = (req.get("Content-Type") ?? "").toLowerCase() + const mimeType = contentType.split(";")[0].trim() + if (!mimeType) { + return next(utils.createExpressError({ + statusCode: 415, + statusMessage: `Missing or empty Content-Type header.` + })) + } + if (hasMultipleContentTypes(contentType)) { + return next(utils.createExpressError({ + statusCode: 415, + statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.` + })) + } + if (mimeType === "application/json" || mimeType === "application/ld+json") return next() + return next(utils.createExpressError({ + statusCode: 415, + statusMessage: `Unsupported Content-Type: ${contentType}. This endpoint requires application/json or application/ld+json.` + })) +} + +/** + * Middleware to verify Content-Type headers for endpoints receiving either JSON or textual bodies. + * Responds with a 415 Invalid Media Type for Content-Type headers that are neither for textual bodies nor JSON bodies. + * + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + */ +const verifyEitherContentType = function (req, res, next) { + const contentType = (req.get("Content-Type") ?? "").toLowerCase() + const mimeType = contentType.split(";")[0].trim() + if (!mimeType) { + return next(utils.createExpressError({ + statusCode: 415, + statusMessage: `Missing or empty Content-Type header.` + })) + } + if (hasMultipleContentTypes(contentType)) { + return next(utils.createExpressError({ + statusCode: 415, + statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.` + })) + } + if (mimeType === "text/plain" || mimeType === "application/json" || mimeType === "application/ld+json") return next() + return next(utils.createExpressError({ + statusCode: 415, + statusMessage: `Unsupported Content-Type: ${contentType}. This endpoint requires application/json, application/ld+json, or text/plain.` + })) +} + /** * Throughout the routes are certain warning, error, and hard fail scenarios. * REST is all about communication. The response code and the textual body are particular. * RERUM is all about being clear. It will build custom responses sometimes for certain scenarios, will remaining RESTful. * - * You have likely reached this with a next(createExpressError(err)) call. End here and send the error. + * You have likely reached this with a next(utils.createExpressError(err)) call. End here and send the error. */ const messenger = function (err, req, res, next) { if (res.headersSent) { @@ -63,7 +157,7 @@ If the body is JSON, make sure it is valid JSON.` if (token) { error.message += ` The token provided is Unauthorized. Please check that it is your token and that it is not expired. -Token: ${token} ` +Token: ${token.slice(0, 15)}... ` } else { error.message += ` @@ -76,11 +170,11 @@ like "Authorization: Bearer ". Make sure you have registered at ${process if (token) { error.message += ` You are Forbidden from performing this action. Check your privileges. -Token: ${token}` +Token: ${token.slice(0, 15)}...` } else { //If there was no Token, this would be a 401. If you made it here, you didn't REST. - err.message += ` + error.message += ` You are Forbidden from performing this action. The request does not contain an "Authorization" header. Make sure you have registered at ${process.env.RERUM_PREFIX}. ` } @@ -95,6 +189,9 @@ The requested web page or resource could not be found.` case 409: // These are all handled in db-controller.js already. break + case 415: + // Unsupported Media Type. The Content-Type header is not acceptable for this endpoint. + break case 501: // Not implemented. Handled upstream. break @@ -113,4 +210,4 @@ It may not have completed at all, and most likely did not complete successfully. res.status(error.status).send(error.message) } -export default { checkPatchOverrideSupport, messenger } +export default { checkPatchOverrideSupport, verifyJsonContentType, verifyEitherContentType, messenger } diff --git a/routes/__tests__/bulkCreate.test.js b/routes/__tests__/bulkCreate.test.js index 917cc7e6..05dbf29f 100644 --- a/routes/__tests__/bulkCreate.test.js +++ b/routes/__tests__/bulkCreate.test.js @@ -12,8 +12,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /bulkCreate route without auth that will use controller.bulkCreate routeTester.use("/bulkCreate", [addAuth, controller.bulkCreate]) diff --git a/routes/__tests__/bulkUpdate.test.js b/routes/__tests__/bulkUpdate.test.js index e857e5a0..b9b619d6 100644 --- a/routes/__tests__/bulkUpdate.test.js +++ b/routes/__tests__/bulkUpdate.test.js @@ -12,8 +12,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /bulkCreate route without auth that will use controller.bulkCreate routeTester.use("/bulkUpdate", [addAuth, controller.bulkUpdate]) diff --git a/routes/__tests__/contentType.test.js b/routes/__tests__/contentType.test.js new file mode 100644 index 00000000..c0989b4f --- /dev/null +++ b/routes/__tests__/contentType.test.js @@ -0,0 +1,279 @@ +/** + * Tests for the Content-Type validation middlewares verifyJsonContentType and verifyEitherContentType. + * The following are examples of good Content-Type headers that should not result in a 415 + + - application/ld+json + - text/plain; a="b,c" + - application/json; a="b,c"; xy=z + * + * The following are the cases that should result in a 415 (not a 500) + + - application/json text/plain + - application/json, text/plain + - text/plain; application/json + - text/plain; a=b, application/json + - application/json; a=b; text/plain; + - application/json; a=b text/plain; + - application/json; charset=utf-8, text/plain + - application/json; + + * If a request contains more than one Content-Type header, that should also result in a 415. + * + * @author thehabes + */ + +import express from "express" +import request from "supertest" +import rest from '../../rest.js' + +// Set up a minimal Express app mirroring the real app's body parsers +const routeTester = express() +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) +routeTester.use(express.text()) + +// JSON-only endpoints (like /api/create, /api/query, /api/update, etc.) +routeTester.post("/json-endpoint", rest.verifyJsonContentType, (req, res) => { + res.status(200).json({ received: req.body }) +}) + +// Either JSON or text endpoint (like /api/search) +routeTester.post("/json-or-text-endpoint", rest.verifyEitherContentType, (req, res) => { + res.status(200).json({ received: req.body }) +}) + +// Error handler matching the app's pattern +routeTester.use(rest.messenger) + +describe("verifyJsonContentType middleware", () => { + + it("accepts application/json", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json") + .send({ test: "data" }) + expect(response.statusCode).toBe(200) + }) + + it("accepts application/ld+json", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/ld+json") + // Must stringify manually; supertest's .send(object) would override Content-Type to application/json + .send(JSON.stringify({ "@context": "http://example.org", test: "ld" })) + expect(response.statusCode).toBe(200) + }) + + it("returns 415 for trailing semicolon without parameter", async () => { + // A trailing semicolon is malformed per RFC 7231 and express.json() won't parse it + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json;") + .send('{"test":"trailing-semicolon"}') + expect(response.statusCode).toBe(415) + }) + + it("accepts application/json with charset parameter", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json; charset=utf-8") + .send({ test: "charset" }) + expect(response.statusCode).toBe(200) + }) + + it("accepts application/ld+json with charset parameter", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/ld+json; charset=utf-8") + // Must stringify manually; supertest's .send(object) would override Content-Type to application/json + .send(JSON.stringify({ "@context": "http://example.org", test: "ld-charset" })) + expect(response.statusCode).toBe(200) + }) + + it("accepts application/json with quoted comma in parameter", async () => { + // Exercises the hasMultipleContentTypes quoted-string bypass: a="b,c" contains a comma + // but it is inside quotes, so it should not be treated as a smuggled MIME type. + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", 'application/json; a="b,c"; xy=z') + .send({ test: "quoted-param" }) + expect(response.statusCode).toBe(200) + }) + + it("accepts Content-Type with unusual casing", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "Application/JSON") + .send({ test: "casing" }) + expect(response.statusCode).toBe(200) + }) + + it("returns 415 for missing Content-Type", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .unset("Content-Type") + .send(Buffer.from('{"test":"data"}')) + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for text/plain", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "text/plain") + .send("some plain text") + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for space-separated multiple Content-Type values", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for comma-separated multiple Content-Type values", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json, text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for comma-injected Content-Type parameter", async () => { + // Even though the MIME type portion is valid, the comma in the full header + // is rejected to prevent Content-Type smuggling via parameter injection. + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json; charset=utf-8, text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for semicolon-smuggled MIME type", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json; text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for semicolon-smuggled MIME type with valid parameter", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json; charset=utf-8; text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for space-smuggled MIME type after valid parameter", async () => { + const response = await request(routeTester) + .post("/json-endpoint") + .set("Content-Type", "application/json; a=b; c=d text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) +}) + +describe("verifyEitherContentType middleware", () => { + + it("accepts application/json", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/json") + .send({ searchText: "hello" }) + expect(response.statusCode).toBe(200) + }) + + it("accepts application/ld+json", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/ld+json") + // Must stringify manually; supertest's .send(object) would override Content-Type to application/json + .send(JSON.stringify({ "@context": "http://example.org" })) + expect(response.statusCode).toBe(200) + }) + + it("accepts text/plain", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "text/plain") + .send("search terms") + expect(response.statusCode).toBe(200) + }) + + it("accepts text/plain with quoted comma in parameter", async () => { + // Exercises the hasMultipleContentTypes quoted-string bypass: a="b,c" contains a comma + // but it is inside quotes, so it should not be treated as a smuggled MIME type. + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", 'text/plain; a="b,c"') + .send("search terms") + expect(response.statusCode).toBe(200) + }) + + it("returns 415 for missing Content-Type", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .unset("Content-Type") + .send(Buffer.from("hello")) + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for application/xml", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/xml") + .send("") + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for space-separated multiple Content-Type values", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/json text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for comma-separated multiple Content-Type values", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/json, text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for comma-injected Content-Type parameter", async () => { + // Even though the MIME type portion is valid, the comma in the full header + // is rejected to prevent Content-Type smuggling via parameter injection. + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/json; charset=utf-8, text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for semicolon-smuggled MIME type", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/json; text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for semicolon-smuggled MIME type with valid parameter", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/json; charset=utf-8; text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) + + it("returns 415 for space-smuggled MIME type after valid parameter", async () => { + const response = await request(routeTester) + .post("/json-or-text-endpoint") + .set("Content-Type", "application/json; a=b; c=d text/plain") + .send('{"test":"data"}') + expect(response.statusCode).toBe(415) + }) +}) diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 788247f9..3c922587 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -13,8 +13,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /create route without auth that will use controller.create routeTester.use("/create", [addAuth, controller.create]) diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index ac012840..964655a3 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -12,8 +12,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // FIXME here we need to create something to delete in order to test this route. routeTester.use("/create", [addAuth, controller.create]) diff --git a/routes/__tests__/history.test.js b/routes/__tests__/history.test.js index c4c87b22..ffcff3e3 100644 --- a/routes/__tests__/history.test.js +++ b/routes/__tests__/history.test.js @@ -1,4 +1,5 @@ import { jest } from "@jest/globals" +jest.setTimeout(10000) // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -6,8 +7,7 @@ import request from "supertest" import controller from '../../db-controller.js' const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /history route without auth that will use controller.history routeTester.use("/history/:_id", controller.history) diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index 7300f21b..b5957744 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -6,8 +6,7 @@ import request from "supertest" import controller from '../../db-controller.js' const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /id route without auth that will use controller.id routeTester.use("/id/:_id", controller.id) diff --git a/routes/__tests__/overwrite-optimistic-locking.test.txt b/routes/__tests__/overwrite-optimistic-locking.test.txt index 3ef6486e..91d4f771 100644 --- a/routes/__tests__/overwrite-optimistic-locking.test.txt +++ b/routes/__tests__/overwrite-optimistic-locking.test.txt @@ -25,8 +25,7 @@ const addAuth = (req, res, next) => { // Create a test Express app const routeTester = express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our routes routeTester.use('/overwrite', [addAuth, controller.overwrite]) diff --git a/routes/__tests__/patch.test.js b/routes/__tests__/patch.test.js index a4d9ebc1..319ef743 100644 --- a/routes/__tests__/patch.test.js +++ b/routes/__tests__/patch.test.js @@ -13,8 +13,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /patch route without auth that will use controller.patch routeTester.use("/patch", [addAuth, controller.patchUpdate]) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index b593b6f9..8b494836 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -6,8 +6,7 @@ import request from "supertest" import controller from '../../db-controller.js' const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /query route without auth that will use controller.query routeTester.use("/query", controller.query) diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index eb6c6e3a..477b2b6a 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -12,8 +12,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // FIXME here we need to create something to release in order to test this route. routeTester.use("/create", [addAuth, controller.create]) diff --git a/routes/__tests__/set.test.js b/routes/__tests__/set.test.js index 1559356c..5c4af116 100644 --- a/routes/__tests__/set.test.js +++ b/routes/__tests__/set.test.js @@ -14,8 +14,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /create route without auth that will use controller.create routeTester.use("/set", [addAuth, controller.patchSet]) diff --git a/routes/__tests__/since.test.js b/routes/__tests__/since.test.js index 13f9579f..c8b59213 100644 --- a/routes/__tests__/since.test.js +++ b/routes/__tests__/since.test.js @@ -1,4 +1,5 @@ import { jest } from "@jest/globals" +jest.setTimeout(10000) // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -6,8 +7,7 @@ import request from "supertest" import controller from '../../db-controller.js' const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /create route without auth that will use controller.history routeTester.use("/since/:_id", controller.since) diff --git a/routes/__tests__/unset.test.js b/routes/__tests__/unset.test.js index e3c8c97c..456da795 100644 --- a/routes/__tests__/unset.test.js +++ b/routes/__tests__/unset.test.js @@ -14,8 +14,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /create route without auth that will use controller.create routeTester.use("/unset", [addAuth, controller.patchUnset]) diff --git a/routes/__tests__/update.test.js b/routes/__tests__/update.test.js index df5e21a3..67ae5318 100644 --- a/routes/__tests__/update.test.js +++ b/routes/__tests__/update.test.js @@ -13,8 +13,7 @@ const addAuth = (req, res, next) => { } const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) +routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) // Mount our own /create route without auth that will use controller.create routeTester.use("/update", [addAuth, controller.putUpdate]) diff --git a/routes/api-routes.js b/routes/api-routes.js index e5cdc743..abee718e 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -46,7 +46,7 @@ import sinceRouter from './since.js'; import historyRouter from './history.js'; router.use(staticRouter) -router.use('/id',idRouter) +router.use('/id', idRouter) router.use('/api', compatabilityRouter) router.use('/api/query', queryRouter) router.use('/api/search', searchRouter) diff --git a/routes/bulkCreate.js b/routes/bulkCreate.js index 8eb2fc90..b4cb49f4 100644 --- a/routes/bulkCreate.js +++ b/routes/bulkCreate.js @@ -5,9 +5,10 @@ const router = express.Router() //This controller will handle all MongoDB interactions. import controller from '../db-controller.js' import auth from '../auth/index.js' +import rest from '../rest.js' router.route('/') - .post(auth.checkJwt, controller.bulkCreate) + .post(auth.checkJwt, rest.verifyJsonContentType, controller.bulkCreate) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use POST.' res.status(405) diff --git a/routes/bulkUpdate.js b/routes/bulkUpdate.js index f7fad3fa..293cd113 100644 --- a/routes/bulkUpdate.js +++ b/routes/bulkUpdate.js @@ -5,9 +5,10 @@ const router = express.Router() //This controller will handle all MongoDB interactions. import controller from '../db-controller.js' import auth from '../auth/index.js' +import rest from '../rest.js' router.route('/') - .put(auth.checkJwt, controller.bulkUpdate) + .put(auth.checkJwt, rest.verifyJsonContentType, controller.bulkUpdate) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use PUT.' res.status(405) diff --git a/routes/create.js b/routes/create.js index 97b86975..e015d129 100644 --- a/routes/create.js +++ b/routes/create.js @@ -4,9 +4,10 @@ const router = express.Router() //This controller will handle all MongoDB interactions. import controller from '../db-controller.js' import auth from '../auth/index.js' +import rest from '../rest.js' router.route('/') - .post(auth.checkJwt, controller.create) + .post(auth.checkJwt, rest.verifyJsonContentType, controller.create) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use POST.' res.status(405) diff --git a/routes/overwrite.js b/routes/overwrite.js index 08b54fd7..edb8ed06 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -4,9 +4,10 @@ const router = express.Router() //This controller will handle all MongoDB interactions. import controller from '../db-controller.js' import auth from '../auth/index.js' +import rest from '../rest.js' router.route('/') - .put(auth.checkJwt, controller.overwrite) + .put(auth.checkJwt, rest.verifyJsonContentType, controller.overwrite) .all((req, res, next) => { res.statusMessage = 'Improper request method for overwriting, please use PUT to overwrite this object.' res.status(405) diff --git a/routes/patchSet.js b/routes/patchSet.js index ff67ec1a..2cf4cb52 100644 --- a/routes/patchSet.js +++ b/routes/patchSet.js @@ -6,8 +6,8 @@ import auth from '../auth/index.js' import rest from '../rest.js' router.route('/') - .patch(auth.checkJwt, controller.patchSet) - .post(auth.checkJwt, (req, res, next) => { + .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchSet) + .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { if (rest.checkPatchOverrideSupport(req, res)) { controller.patchSet(req, res, next) } diff --git a/routes/patchUnset.js b/routes/patchUnset.js index 6bdf0b65..4aa5ee04 100644 --- a/routes/patchUnset.js +++ b/routes/patchUnset.js @@ -6,8 +6,8 @@ import auth from '../auth/index.js' import rest from '../rest.js' router.route('/') - .patch(auth.checkJwt, controller.patchUnset) - .post(auth.checkJwt, (req, res, next) => { + .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUnset) + .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { if (rest.checkPatchOverrideSupport(req, res)) { controller.patchUnset(req, res, next) } diff --git a/routes/patchUpdate.js b/routes/patchUpdate.js index 5df088bf..2d13564c 100644 --- a/routes/patchUpdate.js +++ b/routes/patchUpdate.js @@ -7,8 +7,8 @@ import rest from '../rest.js' import auth from '../auth/index.js' router.route('/') - .patch(auth.checkJwt, controller.patchUpdate) - .post(auth.checkJwt, (req, res, next) => { + .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUpdate) + .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { if (rest.checkPatchOverrideSupport(req, res)) { controller.patchUpdate(req, res, next) } diff --git a/routes/putUpdate.js b/routes/putUpdate.js index d9397122..88cc93f4 100644 --- a/routes/putUpdate.js +++ b/routes/putUpdate.js @@ -4,9 +4,10 @@ const router = express.Router() //This controller will handle all MongoDB interactions. import controller from '../db-controller.js' import auth from '../auth/index.js' +import rest from '../rest.js' router.route('/') - .put(auth.checkJwt, controller.putUpdate) + .put(auth.checkJwt, rest.verifyJsonContentType, controller.putUpdate) .all((req, res, next) => { res.statusMessage = 'Improper request method for updating, please use PUT to update this object.' res.status(405) diff --git a/routes/query.js b/routes/query.js index 61c33c9b..5be0c5ba 100644 --- a/routes/query.js +++ b/routes/query.js @@ -2,9 +2,10 @@ import express from 'express' const router = express.Router() //This controller will handle all MongoDB interactions. import controller from '../db-controller.js' +import rest from '../rest.js' router.route('/') - .post(controller.query) + .post(rest.verifyJsonContentType, controller.query) .head(controller.queryHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method for requesting objects with matching properties. Please use POST.' diff --git a/routes/search.js b/routes/search.js index 2053bf5a..9b9948ca 100644 --- a/routes/search.js +++ b/routes/search.js @@ -1,9 +1,10 @@ import express from 'express' const router = express.Router() import controller from '../db-controller.js' +import rest from '../rest.js' router.route('/') - .post(controller.searchAsWords) + .post(rest.verifyEitherContentType, controller.searchAsWords) .all((req, res, next) => { res.statusMessage = 'Improper request method for search. Please use POST.' res.status(405) @@ -11,7 +12,7 @@ router.route('/') }) router.route('/phrase') - .post(controller.searchAsPhrase) + .post(rest.verifyEitherContentType, controller.searchAsPhrase) .all((req, res, next) => { res.statusMessage = 'Improper request method for search. Please use POST.' res.status(405) diff --git a/routes/static.js b/routes/static.js index 7189dc57..853edbe6 100644 --- a/routes/static.js +++ b/routes/static.js @@ -14,7 +14,6 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) // public also available at `/v1` -router.use(express.urlencoded({ extended: false })) router.use(express.static(path.join(__dirname, '../public'))) // Set default API response diff --git a/utils.js b/utils.js index be1f9c24..e007dc72 100644 --- a/utils.js +++ b/utils.js @@ -239,8 +239,28 @@ const configureLastModifiedHeader = function(obj){ return {"Last-Modified":new Date(date).toUTCString()} } +/** + * Create a standardized Express error object from an error or error-like input. + * Handles MongoDB duplicate key errors (code 11000) as 409 Conflict. + * + * @param {Object} err - An error or object with statusCode/status and statusMessage/message properties + * @return {Object} A normalized error object with statusCode and statusMessage + */ +function createExpressError(err) { + let error = { + statusCode: err.statusCode ?? err.status ?? 500, + statusMessage: err.statusMessage ?? err.message ?? "There was an error that prevented this request from completing successfully." + } + if (err.code === 11000) { + error.statusMessage = `The id provided already exists. Please use a different _id or Slug.` + error.statusCode = 409 + } + return error +} + export default { configureRerumOptions, + createExpressError, isDeleted, isReleased, isGenerator, @@ -249,4 +269,4 @@ export default { isContainerType, isLD, configureLastModifiedHeader -} \ No newline at end of file +}