mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
feat(profile): return 400 for invalid _id format
Add validation for _id field in profile API: - POST: validates each document's _id before storage - PUT: validates _id format before update - DELETE: validates _id parameter before removal Accepts: undefined, null, or 24-character hex string Rejects: UUIDs, short strings, numbers, objects with 400 Bad Request This prevents 500 errors from BSONError when clients send UUID-style _ids (e.g., NightscoutKit). Tests added for all validation cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
var consts = require('../../constants');
|
var consts = require('../../constants');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate MongoDB ObjectId format.
|
||||||
|
* Accepts: undefined, null, or 24-character hex string.
|
||||||
|
* Rejects: anything else (UUIDs, short strings, numbers, objects).
|
||||||
|
* @param {*} id - The _id value to validate
|
||||||
|
* @returns {boolean} - true if valid or empty, false if invalid format
|
||||||
|
*/
|
||||||
|
function isValidObjectId(id) {
|
||||||
|
if (id === undefined || id === null) return true; // Will auto-generate
|
||||||
|
if (typeof id !== 'string') return false;
|
||||||
|
return /^[a-fA-F0-9]{24}$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate _id field for each document in an array.
|
||||||
|
* @param {Array} docs - Array of documents to validate
|
||||||
|
* @returns {Object|null} - null if all valid, or {index, id} of first invalid
|
||||||
|
*/
|
||||||
|
function findInvalidId(docs) {
|
||||||
|
for (var i = 0; i < docs.length; i++) {
|
||||||
|
if (!isValidObjectId(docs[i]._id)) {
|
||||||
|
return { index: i, id: docs[i]._id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function configure (app, wares, ctx) {
|
function configure (app, wares, ctx) {
|
||||||
var express = require('express'),
|
var express = require('express'),
|
||||||
api = express.Router( );
|
api = express.Router( );
|
||||||
@@ -69,6 +96,13 @@ function configure (app, wares, ctx) {
|
|||||||
data = [data];
|
data = [data];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate _id fields before storage (return 400 on invalid)
|
||||||
|
var invalid = findInvalidId(data);
|
||||||
|
if (invalid) {
|
||||||
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
|
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
||||||
|
}
|
||||||
|
|
||||||
// Purify each profile
|
// Purify each profile
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
ctx.purifier.purifyObject(data[i]);
|
ctx.purifier.purifyObject(data[i]);
|
||||||
@@ -89,6 +123,13 @@ function configure (app, wares, ctx) {
|
|||||||
// update record
|
// update record
|
||||||
api.put('/profile/', ctx.authorization.isPermitted('api:profile:update'), function(req, res) {
|
api.put('/profile/', ctx.authorization.isPermitted('api:profile:update'), function(req, res) {
|
||||||
var data = req.body;
|
var data = req.body;
|
||||||
|
|
||||||
|
// Validate _id if provided (required for PUT, must be valid format)
|
||||||
|
if (!isValidObjectId(data._id)) {
|
||||||
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(data._id));
|
||||||
|
}
|
||||||
|
|
||||||
ctx.profile.save(data, function (err, created) {
|
ctx.profile.save(data, function (err, created) {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
|
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
|
||||||
@@ -103,6 +144,12 @@ function configure (app, wares, ctx) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
api.delete('/profile/:_id', ctx.authorization.isPermitted('api:profile:delete'), function(req, res) {
|
api.delete('/profile/:_id', ctx.authorization.isPermitted('api:profile:delete'), function(req, res) {
|
||||||
|
// Validate _id parameter
|
||||||
|
if (!isValidObjectId(req.params._id)) {
|
||||||
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params._id));
|
||||||
|
}
|
||||||
|
|
||||||
ctx.profile.remove(req.params._id, function ( ) {
|
ctx.profile.remove(req.params._id, function ( ) {
|
||||||
res.json({ });
|
res.json({ });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,4 +147,146 @@ describe('Profiles API', function ( ) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// _id validation tests (prevent 500 errors from invalid ObjectId)
|
||||||
|
describe('_id validation', function() {
|
||||||
|
|
||||||
|
it('should return 400 for POST with invalid UUID _id', function(done) {
|
||||||
|
var profile_with_uuid = {
|
||||||
|
"_id": "my-uuid-12345",
|
||||||
|
"defaultProfile": "Default",
|
||||||
|
"store": { "Default": { "dia": 3 } },
|
||||||
|
"startDate": "2024-10-19T23:00:00.000Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/profile/')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.send(profile_with_uuid)
|
||||||
|
.expect(400)
|
||||||
|
.expect(function(response) {
|
||||||
|
response.body.should.have.property('status', 400);
|
||||||
|
response.body.should.have.property('message');
|
||||||
|
response.body.message.should.match(/Invalid _id format/i);
|
||||||
|
})
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for POST with short _id', function(done) {
|
||||||
|
var profile_short_id = {
|
||||||
|
"_id": "abc",
|
||||||
|
"defaultProfile": "Default",
|
||||||
|
"store": { "Default": { "dia": 3 } },
|
||||||
|
"startDate": "2024-10-19T23:00:00.000Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/profile/')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.send(profile_short_id)
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for PUT with invalid _id', function(done) {
|
||||||
|
var profile_invalid = {
|
||||||
|
"_id": "not-a-valid-object-id",
|
||||||
|
"defaultProfile": "Default",
|
||||||
|
"store": { "Default": { "dia": 3 } },
|
||||||
|
"startDate": "2024-10-19T23:00:00.000Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
request(self.app)
|
||||||
|
.put('/api/profile/')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.send(profile_invalid)
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for DELETE with invalid _id', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.delete('/api/profile/invalid-uuid-here')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept POST with valid 24-hex _id', function(done) {
|
||||||
|
var profile_valid_id = {
|
||||||
|
"_id": "507f1f77bcf86cd799439011",
|
||||||
|
"defaultProfile": "Default",
|
||||||
|
"store": { "Default": { "dia": 3 } },
|
||||||
|
"startDate": "2024-10-19T23:00:00.000Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/profile/')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.send(profile_valid_id)
|
||||||
|
.expect(200)
|
||||||
|
.expect(function(response) {
|
||||||
|
response.body.should.be.an.Array();
|
||||||
|
response.body.length.should.equal(1);
|
||||||
|
response.body[0]._id.should.equal('507f1f77bcf86cd799439011');
|
||||||
|
})
|
||||||
|
.end(function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
// Clean up: delete the profile we just created
|
||||||
|
request(self.app)
|
||||||
|
.delete('/api/profile/507f1f77bcf86cd799439011')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept POST without _id (auto-generate)', function(done) {
|
||||||
|
var profile_no_id = {
|
||||||
|
"defaultProfile": "Default",
|
||||||
|
"store": { "Default": { "dia": 3 } },
|
||||||
|
"startDate": "2024-10-20T23:00:00.000Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/profile/')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.send(profile_no_id)
|
||||||
|
.expect(200)
|
||||||
|
.expect(function(response) {
|
||||||
|
response.body.should.be.an.Array();
|
||||||
|
response.body.length.should.equal(1);
|
||||||
|
response.body[0].should.have.property('_id');
|
||||||
|
// Verify auto-generated _id is valid format
|
||||||
|
response.body[0]._id.toString().should.match(/^[a-fA-F0-9]{24}$/);
|
||||||
|
})
|
||||||
|
.end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
// Clean up
|
||||||
|
var createdId = res.body[0]._id;
|
||||||
|
request(self.app)
|
||||||
|
.delete('/api/profile/' + createdId)
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for array POST with one invalid _id', function(done) {
|
||||||
|
var profiles_mixed = [
|
||||||
|
{ "defaultProfile": "Default1", "store": { "Default": { "dia": 3 } }, "startDate": "2024-10-19T23:00:00.000Z" },
|
||||||
|
{ "_id": "bad-uuid", "defaultProfile": "Default2", "store": { "Default": { "dia": 3 } }, "startDate": "2024-10-20T23:00:00.000Z" }
|
||||||
|
];
|
||||||
|
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/profile/')
|
||||||
|
.set('api-secret', known || '')
|
||||||
|
.send(profiles_mixed)
|
||||||
|
.expect(400)
|
||||||
|
.expect(function(response) {
|
||||||
|
response.body.message.should.match(/Invalid _id format/i);
|
||||||
|
})
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user