diff --git a/app.js b/app.js index 548f9092..17128c61 100644 --- a/app.js +++ b/app.js @@ -12,9 +12,11 @@ import clientRouter from './routes/client.js' import _gog_fragmentsRouter from './routes/_gog_fragments_from_manuscript.js'; import _gog_glossesRouter from './routes/_gog_glosses_from_manuscript.js'; import rest from './rest.js' +import csrf from 'csurf' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) +const csrfProtection = csrf({ cookie: true }) const app = express() @@ -60,7 +62,7 @@ app.use(express.json()) app.use(express.text()) app.use(express.urlencoded({ extended: true })) app.use(cookieParser()) - +app.use(csrfProtection) //Publicly available scripts, CSS, and HTML pages. app.use(express.static(path.join(__dirname, 'public'))) diff --git a/controllers/patchBase.js b/controllers/patchBase.js new file mode 100644 index 00000000..78b0c186 --- /dev/null +++ b/controllers/patchBase.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node + +/** + * Base PATCH controller for RERUM operations + * Provides shared logic for patchSet, patchUnset, and patchUpdate operations + * @author Claude Sonnet 4, cubap, thehabes + */ + +import { newID, isValidID, db } from '../database/client.js' +import { isDeleted } from '../predicates.js' +import { configureRerumOptions } from '../versioning.js' +import { configureWebAnnoHeadersFor } from '../headers.js' +import config from '../config/index.js' +import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' + +/** + * Base function for PATCH operations that handles common logic + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next function + * @param {Function} processObject - Function that processes the object (set/unset/update specific logic) + * @param {string} operationName - Name of the operation for logging + * @returns {Promise} + */ +const basePatchOperation = async function (req, res, next, processObject, operationName = "PATCH") { + let err = { message: `` } + res.set("Content-Type", "application/json; charset=utf-8") + let objectReceived = JSON.parse(JSON.stringify(req.body)) + let patchedObject = {} + let generatorAgent = getAgentClaim(req, next) + const receivedID = objectReceived["@id"] ?? objectReceived.id + + if (receivedID) { + let id = parseDocumentID(receivedID) + let originalObject + try { + originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) + } catch (error) { + next(createExpressError(error)) + return + } + + if (null === originalObject) { + //This object is not in RERUM, they want to import it. Do that automatically. + //updateExternalObject(objectReceived) + err = Object.assign(err, { + message: `This object is not from RERUM and will need imported. This is not automated yet. You can make a new object with create. ${err.message}`, + status: 501 + }) + } + else if (isDeleted(originalObject)) { + err = Object.assign(err, { + message: `The object you are trying to update is deleted. ${err.message}`, + status: 403 + }) + } + else { + // Call the specific processing function + const result = processObject(originalObject, objectReceived, patchedObject) + + if (result.noChanges) { + // No changes were made, return original object + res.set(configureWebAnnoHeadersFor(originalObject)) + originalObject = idNegotiation(originalObject) + originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) + res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) + res.status(200) + res.json(originalObject) + return + } + + // Use the processed object + patchedObject = result.patchedObject + + // Create new version + const id = ObjectID() + let context = patchedObject["@context"] ? { "@context": patchedObject["@context"] } : {} + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } + delete patchedObject["__rerum"] + delete patchedObject["_id"] + delete patchedObject["@id"] + delete patchedObject["@context"] + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) + + console.log(operationName) + try { + let result = await db.insertOne(newObject) + if (alterHistoryNext(originalObject, newObject["@id"])) { + //Success, the original object has been updated. + res.set(configureWebAnnoHeadersFor(newObject)) + newObject = idNegotiation(newObject) + newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) + res.status(200) + res.json(newObject) + return + } + err = Object.assign(err, { + message: `Unable to alter the history next of the originating object. The history tree may be broken. See ${originalObject["@id"]}. ${err.message}`, + status: 500 + }) + } + catch (error) { + //WriteError or WriteConcernError + next(createExpressError(error)) + return + } + } + } + else { + //The http module will not detect this as a 400 on its own + err = Object.assign(err, { + message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, + status: 400 + }) + } + next(createExpressError(err)) +} + +export { basePatchOperation } \ No newline at end of file diff --git a/controllers/patchSet.js b/controllers/patchSet.js index c85128a9..eda238f7 100644 --- a/controllers/patchSet.js +++ b/controllers/patchSet.js @@ -6,122 +6,34 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/client.js' -import { isDeleted } from '../predicates.js' -import { configureRerumOptions } from '../versioning.js' -import { configureWebAnnoHeadersFor } from '../headers.js' -import config from '../config/index.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' +import { basePatchOperation } from './patchBase.js' +import { _contextid } from './utils.js' -/** - * Update some existing object in MongoDB by adding the keys from the JSON object in the request body. - * Note that if a key on the request object matches a key on the object in MongoDB, that key will be ignored. - * Order the properties to preference @context and @id. Put __rerum and _id last. - * This cannot change or unset existing keys. - * Track History - * Respond RESTfully - * */ -const patchSet = async function (req, res, next) { - let err = { message: `` } - res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) - let originalContext - let patchedObject = {} - let generatorAgent = getAgentClaim(req, next) - const receivedID = objectReceived["@id"] ?? objectReceived.id - if (receivedID) { - let id = parseDocumentID(receivedID) - let originalObject - try { - originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) - } catch (error) { - next(createExpressError(error)) - return - } - if (null === originalObject) { - //This object is not in RERUM, they want to import it. Do that automatically. - //updateExternalObject(objectReceived) - err = Object.assign(err, { - message: `This object is not from RERUM and will need imported. This is not automated yet. You can make a new object with create. ${err.message}`, - status: 501 - }) - } - else if (isDeleted(originalObject)) { - err = Object.assign(err, { - message: `The object you are trying to update is deleted. ${err.message}`, - status: 403 - }) - } - else { - patchedObject = JSON.parse(JSON.stringify(originalObject)) - if(_contextid(originalObject["@context"])) { - // If the original object has a context that needs id protected, make sure you don't set it. - delete objectReceived.id - delete originalObject.id - delete patchedObject.id - } - //A set only adds new keys. If the original object had the key, it is ignored here. - delete objectReceived._id - for (let k in objectReceived) { - if (originalObject.hasOwnProperty(k)) { - //Note the possibility of notifying the user that these keys were not processed. - delete objectReceived[k] - } - else { - patchedObject[k] = objectReceived[k] - } - } - if (Object.keys(objectReceived).length === 0) { - //Then you aren't actually changing anything...there are no new properties - //Just hand back the object. The resulting of setting nothing is the object from the request body. - res.set(configureWebAnnoHeadersFor(originalObject)) - originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) - res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) - res.status(200) - res.json(originalObject) - return - } - const id = ObjectID() - let context = patchedObject["@context"] ? { "@context": patchedObject["@context"] } : {} - let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } - delete patchedObject["__rerum"] - delete patchedObject["_id"] - delete patchedObject["@id"] - delete patchedObject["@context"] - let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) - try { - let result = await db.insertOne(newObject) - if (alterHistoryNext(originalObject, newObject["@id"])) { - //Success, the original object has been updated. - res.set(configureWebAnnoHeadersFor(newObject)) - newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) - res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) - res.status(200) - res.json(newObject) - return - } - err = Object.assign(err, { - message: `Unable to alter the history next of the originating object. The history tree may be broken. See ${originalObject["@id"]}. ${err.message}`, - status: 500 - }) - } - catch (error) { - //WriteError or WriteConcernError - next(createExpressError(error)) - return - } - } +const processPatchSet = (originalObject, objectReceived, patchedObject) => { + patchedObject = JSON.parse(JSON.stringify(originalObject)) + + if(_contextid(originalObject['@context'])) { + delete objectReceived.id + delete originalObject.id + delete patchedObject.id } - else { - //The http module will not detect this as a 400 on its own - err = Object.assign(err, { - message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, - status: 400 - }) + + delete objectReceived._id + + for (let k in objectReceived) { + if (originalObject.hasOwnProperty(k)) { + delete objectReceived[k] + } else { + patchedObject[k] = objectReceived[k] + } } - next(createExpressError(err)) + + const noChanges = Object.keys(objectReceived).length === 0 + return { patchedObject, noChanges } +} + +const patchSet = async (req, res, next) => { + return basePatchOperation(req, res, next, processPatchSet, 'PATCH SET') } export { patchSet } diff --git a/controllers/patchUnset.js b/controllers/patchUnset.js index 71dd3723..42cceff9 100644 --- a/controllers/patchUnset.js +++ b/controllers/patchUnset.js @@ -6,127 +6,32 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/client.js' -import { isDeleted } from '../predicates.js' -import { configureRerumOptions } from '../versioning.js' -import { configureWebAnnoHeadersFor } from '../headers.js' -import config from '../config/index.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' +import { basePatchOperation } from './patchBase.js' +import { _contextid } from './utils.js' -/** - * Update some existing object in MongoDB by removing the keys noted in the JSON object in the request body. - * Note that if a key on the request object does not match a key on the object in MongoDB, that key will be ignored. - * Order the properties to preference @context and @id. Put __rerum and _id last. - * This cannot change existing keys or set new keys. - * Track History - * Respond RESTfully - * */ -const patchUnset = async function (req, res, next) { - let err = { message: `` } - res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) - let patchedObject = {} - let generatorAgent = getAgentClaim(req, next) - const receivedID = objectReceived["@id"] ?? objectReceived.id - if (receivedID) { - let id = parseDocumentID(receivedID) - let originalObject - try { - originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) - } catch (error) { - next(createExpressError(error)) - return - } - if (null === originalObject) { - //This object is not in RERUM, they want to import it. Do that automatically. - //updateExternalObject(objectReceived) - err = Object.assign(err, { - message: `This object is not from RERUM and will need imported. This is not automated yet. You can make a new object with create. ${err.message}`, - status: 501 - }) - } - else if (isDeleted(originalObject)) { - err = Object.assign(err, { - message: `The object you are trying to update is deleted. ${err.message}`, - status: 403 - }) - } - else { - patchedObject = JSON.parse(JSON.stringify(originalObject)) - delete objectReceived._id //can't unset this - delete objectReceived.__rerum //can't unset this - delete objectReceived["@id"] //can't unset this - // id is also protected in this case, so it can't be unset. - if(_contextid(originalObject["@context"])) delete objectReceived.id - - /** - * unset does not alter an existing key. It removes an existing key. - * The request payload had {key:null} to flag keys to be removed. - * Everything else is ignored. - */ - for (let k in objectReceived) { - if (originalObject.hasOwnProperty(k) && objectReceived[k] === null) { - delete patchedObject[k] - } - else { - //Note the possibility of notifying the user that these keys were not processed. - delete objectReceived[k] - } - } - if (Object.keys(objectReceived).length === 0) { - //Then you aren't actually changing anything...no properties in the request body were removed from the original object. - //Just hand back the object. The resulting of unsetting nothing is the object. - res.set(configureWebAnnoHeadersFor(originalObject)) - originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) - res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) - res.status(200) - res.json(originalObject) - return - } - const id = ObjectID() - let context = patchedObject["@context"] ? { "@context": patchedObject["@context"] } : {} - let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } - delete patchedObject["__rerum"] - delete patchedObject["_id"] - delete patchedObject["@id"] - // id is also protected in this case, so it can't be set. - if(_contextid(patchedObject["@context"])) delete patchedObject.id - delete patchedObject["@context"] - let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) - console.log("PATCH UNSET") - try { - let result = await db.insertOne(newObject) - if (alterHistoryNext(originalObject, newObject["@id"])) { - //Success, the original object has been updated. - res.set(configureWebAnnoHeadersFor(newObject)) - newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) - res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) - res.status(200) - res.json(newObject) - return - } - err = Object.assign(err, { - message: `Unable to alter the history next of the originating object. The history tree may be broken. See ${originalObject["@id"]}. ${err.message}`, - status: 500 - }) - } - catch (error) { - //WriteError or WriteConcernError - next(createExpressError(error)) - return - } +const processPatchUnset = (originalObject, objectReceived, patchedObject) => { + patchedObject = JSON.parse(JSON.stringify(originalObject)) + + delete objectReceived._id + delete objectReceived.__rerum + delete objectReceived['@id'] + if (_contextid(originalObject['@context'])) delete objectReceived.id + + for (let k in objectReceived) { + if (originalObject.hasOwnProperty(k) && objectReceived[k] === null) { + delete patchedObject[k] + delete objectReceived[k] + } else { + delete objectReceived[k] } } - else { - //The http module will not detect this as a 400 on its own - err = Object.assign(err, { - message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, - status: 400 - }) - } - next(createExpressError(err)) + + const noChanges = Object.keys(objectReceived).length === 0 + return { patchedObject, noChanges } +} + +const patchUnset = async (req, res, next) => { + return basePatchOperation(req, res, next, processPatchUnset, 'PATCH UNSET') } export { patchUnset } diff --git a/controllers/patchUpdate.js b/controllers/patchUpdate.js index 03874912..4fe85aed 100644 --- a/controllers/patchUpdate.js +++ b/controllers/patchUpdate.js @@ -6,126 +6,39 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/client.js' -import { isDeleted } from '../predicates.js' -import { configureRerumOptions } from '../versioning.js' -import { configureWebAnnoHeadersFor } from '../headers.js' -import config from '../config/index.js' -import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' +import { basePatchOperation } from './patchBase.js' +import { _contextid } from './utils.js' -/** - * Update some existing object in MongoDB by changing the keys from the JSON object in the request body. - * Keys in the request body that do not exist in the original object will be ignored. - * Order the properties to preference @context and @id. Put __rerum and _id last. - * Track History - * Respond RESTfully - * */ -const patchUpdate = async function (req, res, next) { - let err = { message: `` } - res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) - let patchedObject = {} - let generatorAgent = getAgentClaim(req, next) - const receivedID = objectReceived["@id"] ?? objectReceived.id - if (receivedID) { - let id = parseDocumentID(receivedID) - let originalObject - try { - originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) - } catch (error) { - next(createExpressError(error)) - return - } - if (null === originalObject) { - //This object is not in RERUM, they want to import it. Do that automatically. - //updateExternalObject(objectReceived) - err = Object.assign(err, { - message: `This object is not from RERUM and will need imported. This is not automated yet. You can make a new object with create. ${err.message}`, - status: 501 - }) - } - else if (isDeleted(originalObject)) { - err = Object.assign(err, { - message: `The object you are trying to update is deleted. ${err.message}`, - status: 403 - }) - } - else { - patchedObject = JSON.parse(JSON.stringify(originalObject)) - delete objectReceived.__rerum //can't patch this - delete objectReceived._id //can't patch this - delete objectReceived["@id"] //can't patch this - // id is also protected in this case, so it can't be set. - if(_contextid(objectReceived["@context"])) delete objectReceived.id - //A patch only alters existing keys. Remove non-existent keys from the object received in the request body. - for (let k in objectReceived) { - if (originalObject.hasOwnProperty(k)) { - if (objectReceived[k] === null) { - delete patchedObject[k] - } - else { - patchedObject[k] = objectReceived[k] - } - } - else { - //Note the possibility of notifying the user that these keys were not processed. - delete objectReceived[k] - } - } - if (Object.keys(objectReceived).length === 0) { - //Then you aren't actually changing anything...only @id came through - //Just hand back the object. The resulting of patching nothing is the object unchanged. - res.set(configureWebAnnoHeadersFor(originalObject)) - originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) - res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) - res.status(200) - res.json(originalObject) - return - } - const id = ObjectID() - let context = patchedObject["@context"] ? { "@context": patchedObject["@context"] } : {} - let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } - delete patchedObject["__rerum"] - delete patchedObject["_id"] - delete patchedObject["@id"] - // id is also protected in this case, so it can't be set. - if(_contextid(patchedObject["@context"])) delete patchedObject.id - delete patchedObject["@context"] - let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) - console.log("PATCH UPDATE") - try { - let result = await db.insertOne(newObject) - if (alterHistoryNext(originalObject, newObject["@id"])) { - //Success, the original object has been updated. - res.set(configureWebAnnoHeadersFor(newObject)) - newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) - res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) - res.status(200) - res.json(newObject) - return - } - err = Object.assign(err, { - message: `Unable to alter the history next of the originating object. The history tree may be broken. See ${originalObject["@id"]}. ${err.message}`, - status: 500 - }) - } - catch (error) { - //WriteError or WriteConcernError - next(createExpressError(error)) - return +const processPatchUpdate = (originalObject, objectReceived, patchedObject) => { + patchedObject = JSON.parse(JSON.stringify(originalObject)) + + delete objectReceived.__rerum + delete objectReceived._id + delete objectReceived['@id'] + + if (_contextid(objectReceived['@context'])) { + delete objectReceived.id + } + + for (let k in objectReceived) { + if (originalObject.hasOwnProperty(k)) { + if (objectReceived[k] === null) { + delete patchedObject[k] + } else { + patchedObject[k] = objectReceived[k] } + delete objectReceived[k] + } else { + delete objectReceived[k] } } - else { - //The http module will not detect this as a 400 on its own - err = Object.assign(err, { - message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, - status: 400 - }) - } - next(createExpressError(err)) + + const noChanges = Object.keys(objectReceived).length === 0 + return { patchedObject, noChanges } +} + +const patchUpdate = async (req, res, next) => { + return basePatchOperation(req, res, next, processPatchUpdate, 'PATCH UPDATE') } export { patchUpdate } diff --git a/db-controller.js b/db-controller.js index 07aa6f65..7eda2b04 100644 --- a/db-controller.js +++ b/db-controller.js @@ -11,7 +11,10 @@ import { index, idNegotiation, generateSlugId, remove } from './controllers/util import { create, query, id } from './controllers/crud.js' import { searchAsWords, searchAsPhrase } from './controllers/search.js' import { deleteObj } from './controllers/delete.js' -import { putUpdate, patchUpdate, patchSet, patchUnset, overwrite } from './controllers/update.js' +import { putUpdate, overwrite } from './controllers/update.js' +import { patchUpdate } from './controllers/patchUpdate.js' +import { patchSet } from './controllers/patchSet.js' +import { patchUnset } from './controllers/patchUnset.js' import { bulkCreate, bulkUpdate } from './controllers/bulk.js' import { since, history, idHeadRequest, queryHeadRequest, sinceHeadRequest, historyHeadRequest } from './controllers/history.js' import { release } from './controllers/release.js' diff --git a/package-lock.json b/package-lock.json index c5f495e3..82522bff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,30 @@ { "name": "rerum_server_nodejs", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rerum_server_nodejs", - "version": "0.0.0", - "license": "UNLICENSED", + "version": "1.0.0", + "license": "MIT", "dependencies": { "cookie-parser": "~1.4.7", "cors": "^2.8.5", + "csurf": "^1.11.0", "debug": "~4.4.3", "dotenv": "~17.2.3", "express": "^5.2.1", "express-oauth2-jwt-bearer": "~1.7.1", + "express-rate-limit": "^8.3.2", "express-urlrewrite": "~2.0.3", + "lusca": "^1.7.0", "mongodb": "^7.0.0", "morgan": "~1.10.1" }, + "bin": { + "rerum_server_nodejs": "bin/rerum_v1.js" + }, "devDependencies": { "@jest/globals": "^30.2.0", "jest": "^30.2.0", @@ -1755,9 +1761,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2203,6 +2209,94 @@ "node": ">= 8" } }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "license": "MIT", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2579,6 +2673,24 @@ "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0" } }, + "node_modules/express-rate-limit": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express-urlrewrite": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-2.0.3.tgz", @@ -3061,6 +3173,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3743,9 +3864,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3942,6 +4063,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lusca": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lusca/-/lusca-1.7.0.tgz", + "integrity": "sha512-msnrplCfY7zaqlZBDEloCIKld+RUeMZVeWzSPaGUKeRXFlruNSdKg2XxCyR+zj6BqzcXhXlRnvcvx6rAGgsvMA==", + "dependencies": { + "tsscmp": "^1.0.5" + }, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -4093,13 +4225,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4520,9 +4652,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4623,9 +4755,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -4637,6 +4769,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4701,6 +4842,12 @@ "node": ">=8" } }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -4718,9 +4865,9 @@ } }, "node_modules/router/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -5237,9 +5384,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5270,9 +5417,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5331,6 +5478,15 @@ "license": "0BSD", "optional": true }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5368,6 +5524,18 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undici-types": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", diff --git a/package.json b/package.json index 057182f6..32813f55 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "license": "MIT", "author": "Research Computing Group (https://slu.edu)", "repository": { - "type": "git", - "url": "git+https://github.com/CenterForDigitalHumanities/rerum_server_nodejs.git" + "type": "git", + "url": "git+https://github.com/CenterForDigitalHumanities/rerum_server_nodejs.git" }, "bugs": { "url": "https://github.com/CenterForDigitalHumanities/rerum_server_nodejs/issues" @@ -42,11 +42,14 @@ "dependencies": { "cookie-parser": "~1.4.7", "cors": "^2.8.5", + "csurf": "^1.11.0", "debug": "~4.4.3", "dotenv": "~17.2.3", "express": "^5.2.1", "express-oauth2-jwt-bearer": "~1.7.1", + "express-rate-limit": "^8.3.2", "express-urlrewrite": "~2.0.3", + "lusca": "^1.7.0", "mongodb": "^7.0.0", "morgan": "~1.10.1" }, diff --git a/routes/__tests__/patch.test.js b/routes/__tests__/patch.test.js index a4d9ebc1..a892900e 100644 --- a/routes/__tests__/patch.test.js +++ b/routes/__tests__/patch.test.js @@ -12,12 +12,22 @@ const addAuth = (req, res, next) => { next() } +// Rate limiting for tests - allows unlimited requests in test environment +import rateLimit from 'express-rate-limit' +const limiter = rateLimit({ + windowMs: 1000, // 1 second for tests + max: 1000, // allow many requests in test environment + message: 'Too many requests from this IP, please try again later.', + standardHeaders: true, + legacyHeaders: false, +}) + const routeTester = new express() routeTester.use(express.json()) routeTester.use(express.urlencoded({ extended: false })) // Mount our own /patch route without auth that will use controller.patch -routeTester.use("/patch", [addAuth, controller.patchUpdate]) +routeTester.use("/patch", limiter, addAuth, controller.patchUpdate) const unique = new Date(Date.now()).toISOString().replace("Z", "") it("'/patch' route functions", async () => { diff --git a/routes/__tests__/set.test.js b/routes/__tests__/set.test.js index 1559356c..4b3c6892 100644 --- a/routes/__tests__/set.test.js +++ b/routes/__tests__/set.test.js @@ -13,12 +13,22 @@ const addAuth = (req, res, next) => { next() } +// Rate limiting for tests - allows unlimited requests in test environment +import rateLimit from 'express-rate-limit' +const limiter = rateLimit({ + windowMs: 1000, // 1 second for tests + max: 1000, // allow many requests in test environment + message: 'Too many requests from this IP, please try again later.', + standardHeaders: true, + legacyHeaders: false, +}) + const routeTester = new express() routeTester.use(express.json()) routeTester.use(express.urlencoded({ extended: false })) -// Mount our own /create route without auth that will use controller.create -routeTester.use("/set", [addAuth, controller.patchSet]) +// Mount our own /set route without auth that will use controller.patchSet +routeTester.use("/set", limiter, addAuth, controller.patchSet) const unique = new Date(Date.now()).toISOString().replace("Z", "") it("'/set' route functions", async () => { @@ -28,15 +38,15 @@ it("'/set' route functions", async () => { .set('Content-Type', 'application/json; charset=utf-8') .then(resp => resp) .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.headers["location"]).not.toBe(`${process.env.RERUM_ID_PREFIX}11111`) - expect(response.statusCode).toBe(200) - expect(response.body._id).toBeUndefined() - expect(response.body["test_set"]).toBe(unique) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() + expect(response.header.location).toBe(response.body["@id"]) + expect(response.headers["location"]).not.toBe(`${process.env.RERUM_ID_PREFIX}11111`) + expect(response.statusCode).toBe(200) + expect(response.body._id).toBeUndefined() + expect(response.body["test_set"]).toBe(unique) + expect(response.headers["content-length"]).toBeTruthy() + expect(response.headers["content-type"]).toBeTruthy() + expect(response.headers["date"]).toBeTruthy() + expect(response.headers["etag"]).toBeTruthy() + expect(response.headers["allow"]).toBeTruthy() + expect(response.headers["link"]).toBeTruthy() }) diff --git a/routes/__tests__/unset.test.js b/routes/__tests__/unset.test.js index e3c8c97c..3731c0a6 100644 --- a/routes/__tests__/unset.test.js +++ b/routes/__tests__/unset.test.js @@ -13,12 +13,22 @@ const addAuth = (req, res, next) => { next() } +// Rate limiting for tests - allows unlimited requests in test environment +import rateLimit from 'express-rate-limit' +const limiter = rateLimit({ + windowMs: 1000, // 1 second for tests + max: 1000, // allow many requests in test environment + message: 'Too many requests from this IP, please try again later.', + standardHeaders: true, + legacyHeaders: false, +}) + const routeTester = new express() routeTester.use(express.json()) routeTester.use(express.urlencoded({ extended: false })) -// Mount our own /create route without auth that will use controller.create -routeTester.use("/unset", [addAuth, controller.patchUnset]) +// Mount our own /unset route without auth that will use controller.patchUnset +routeTester.use("/unset", limiter, addAuth, controller.patchUnset) it("'/unset' route functions", async () => { const response = await request(routeTester) @@ -27,15 +37,15 @@ it("'/unset' route functions", async () => { .set('Content-Type', 'application/json; charset=utf-8') .then(resp => resp) .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.statusCode).toBe(200) - expect(response.body._id).toBeUndefined() - expect(response.body.hasOwnProperty("test_obj")).toBe(false) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() + expect(response.header.location).toBe(response.body["@id"]) + expect(response.statusCode).toBe(200) + expect(response.body._id).toBeUndefined() + expect(response.body.hasOwnProperty("test_obj")).toBe(false) + expect(response.headers["content-length"]).toBeTruthy() + expect(response.headers["content-type"]).toBeTruthy() + expect(response.headers["date"]).toBeTruthy() + expect(response.headers["etag"]).toBeTruthy() + expect(response.headers["allow"]).toBeTruthy() + expect(response.headers["link"]).toBeTruthy() }) diff --git a/routes/patchSet.js b/routes/patchSet.js index ff67ec1a..a9bacaa4 100644 --- a/routes/patchSet.js +++ b/routes/patchSet.js @@ -4,10 +4,18 @@ const router = express.Router() import controller from '../db-controller.js' import auth from '../auth/index.js' import rest from '../rest.js' +import rateLimit from 'express-rate-limit' + +const patchSetLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 patch-set requests per windowMs + standardHeaders: true, + legacyHeaders: false, +}) router.route('/') - .patch(auth.checkJwt, controller.patchSet) - .post(auth.checkJwt, (req, res, next) => { + .patch(auth.checkJwt, patchSetLimiter, controller.patchSet) + .post(auth.checkJwt, patchSetLimiter, (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..850a77b7 100644 --- a/routes/patchUnset.js +++ b/routes/patchUnset.js @@ -4,10 +4,18 @@ const router = express.Router() import controller from '../db-controller.js' import auth from '../auth/index.js' import rest from '../rest.js' +import rateLimit from 'express-rate-limit' + +const patchUnsetLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 patch-unset requests per windowMs + standardHeaders: true, + legacyHeaders: false, +}) router.route('/') - .patch(auth.checkJwt, controller.patchUnset) - .post(auth.checkJwt, (req, res, next) => { + .patch(auth.checkJwt, patchUnsetLimiter, controller.patchUnset) + .post(auth.checkJwt, patchUnsetLimiter, (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..c785421e 100644 --- a/routes/patchUpdate.js +++ b/routes/patchUpdate.js @@ -5,10 +5,18 @@ const router = express.Router() import controller from '../db-controller.js' import rest from '../rest.js' import auth from '../auth/index.js' +import rateLimit from 'express-rate-limit' + +const patchUpdateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 patch-update requests per windowMs + standardHeaders: true, + legacyHeaders: false, +}) router.route('/') - .patch(auth.checkJwt, controller.patchUpdate) - .post(auth.checkJwt, (req, res, next) => { + .patch(auth.checkJwt, patchUpdateLimiter, controller.patchUpdate) + .post(auth.checkJwt, patchUpdateLimiter, (req, res, next) => { if (rest.checkPatchOverrideSupport(req, res)) { controller.patchUpdate(req, res, next) }