mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
feat(api): add _id validation to activity and food APIs
Add validation for _id field in activity and food APIs: - activity: POST, PUT, DELETE now validate _id format - food: POST, PUT, DELETE now validate _id format Accepts: undefined, null, or 24-character hex string Rejects: UUIDs, short strings, numbers, objects with 400 Bad Request Previously: - activity: 500 crash on invalid _id in save/remove - food: silently replaced invalid _id with new ObjectId (data loss) Tests added covering all validation cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,30 @@ var _isArray = require('lodash/isArray');
|
|||||||
var consts = require('../../constants');
|
var consts = require('../../constants');
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate MongoDB ObjectId format.
|
||||||
|
* Accepts: undefined, null, or 24-character hex string.
|
||||||
|
* Rejects: anything else (UUIDs, short strings, numbers, objects).
|
||||||
|
*/
|
||||||
|
function isValidObjectId(id) {
|
||||||
|
if (id === undefined || id === null) return true;
|
||||||
|
if (typeof id !== 'string') return false;
|
||||||
|
return /^[a-fA-F0-9]{24}$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate _id field for each document in an array.
|
||||||
|
* @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();
|
||||||
@@ -73,6 +97,13 @@ function configure(app, wares, ctx) {
|
|||||||
activity = [activity];
|
activity = [activity];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate _id fields before storage (return 400 on invalid)
|
||||||
|
var invalid = findInvalidId(activity);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
ctx.activity.create(activity, function(err, created) {
|
ctx.activity.create(activity, function(err, created) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log('Error adding activity data', err);
|
console.log('Error adding activity data', err);
|
||||||
@@ -87,6 +118,11 @@ function configure(app, wares, ctx) {
|
|||||||
api.post('/activity/', ctx.authorization.isPermitted('api:activity:create'), post_response);
|
api.post('/activity/', ctx.authorization.isPermitted('api:activity:create'), post_response);
|
||||||
|
|
||||||
api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) {
|
api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity: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.activity.remove(req.params._id, function() {
|
ctx.activity.remove(req.params._id, function() {
|
||||||
res.json({});
|
res.json({});
|
||||||
});
|
});
|
||||||
@@ -95,6 +131,13 @@ function configure(app, wares, ctx) {
|
|||||||
// update record
|
// update record
|
||||||
api.put('/activity/', ctx.authorization.isPermitted('api:activity:update'), function(req, res) {
|
api.put('/activity/', ctx.authorization.isPermitted('api:activity:update'), function(req, res) {
|
||||||
var data = req.body;
|
var data = req.body;
|
||||||
|
|
||||||
|
// Validate _id if provided
|
||||||
|
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.activity.save(data, function(err, created) {
|
ctx.activity.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);
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
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).
|
||||||
|
*/
|
||||||
|
function isValidObjectId(id) {
|
||||||
|
if (id === undefined || id === null) return true;
|
||||||
|
if (typeof id !== 'string') return false;
|
||||||
|
return /^[a-fA-F0-9]{24}$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
function configure (app, wares, ctx) {
|
function configure (app, wares, ctx) {
|
||||||
var express = require('express'),
|
var express = require('express'),
|
||||||
api = express.Router( );
|
api = express.Router( );
|
||||||
@@ -43,6 +54,13 @@ function configure (app, wares, ctx) {
|
|||||||
// create new record
|
// create new record
|
||||||
api.post('/food/', ctx.authorization.isPermitted('api:food:create'), function(req, res) {
|
api.post('/food/', ctx.authorization.isPermitted('api:food:create'), function(req, res) {
|
||||||
var data = req.body;
|
var data = req.body;
|
||||||
|
|
||||||
|
// Validate _id if provided
|
||||||
|
if (!isValidObjectId(data._id)) {
|
||||||
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
|
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(data._id));
|
||||||
|
}
|
||||||
|
|
||||||
ctx.food.create(data, function (err, created) {
|
ctx.food.create(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);
|
||||||
@@ -58,6 +76,13 @@ function configure (app, wares, ctx) {
|
|||||||
// update record
|
// update record
|
||||||
api.put('/food/', ctx.authorization.isPermitted('api:food:update'), function(req, res) {
|
api.put('/food/', ctx.authorization.isPermitted('api:food:update'), function(req, res) {
|
||||||
var data = req.body;
|
var data = req.body;
|
||||||
|
|
||||||
|
// Validate _id if provided
|
||||||
|
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.food.save(data, function (err, created) {
|
ctx.food.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);
|
||||||
@@ -71,6 +96,11 @@ function configure (app, wares, ctx) {
|
|||||||
});
|
});
|
||||||
// delete record
|
// delete record
|
||||||
api.delete('/food/:_id', ctx.authorization.isPermitted('api:food:delete'), function(req, res) {
|
api.delete('/food/:_id', ctx.authorization.isPermitted('api:food: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.food.remove(req.params._id, function ( ) {
|
ctx.food.remove(req.params._id, function ( ) {
|
||||||
res.json({ });
|
res.json({ });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var request = require('supertest');
|
||||||
|
var should = require('should');
|
||||||
|
var language = require('../lib/language')();
|
||||||
|
|
||||||
|
describe('_id Validation API Tests', function() {
|
||||||
|
this.timeout(10000);
|
||||||
|
var self = this;
|
||||||
|
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
||||||
|
|
||||||
|
var api = require('../lib/api/');
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
process.env.API_SECRET = 'this is my long pass phrase';
|
||||||
|
self.env = require('../lib/server/env')();
|
||||||
|
self.env.settings.authDefaultRoles = 'readable';
|
||||||
|
self.env.settings.enable = ['careportal', 'api'];
|
||||||
|
this.wares = require('../lib/middleware/')(self.env);
|
||||||
|
self.app = require('express')();
|
||||||
|
self.app.enable('api');
|
||||||
|
self.app.enable('careportal');
|
||||||
|
require('../lib/server/bootevent')(self.env, language).boot(function booted(ctx) {
|
||||||
|
self.ctx = ctx;
|
||||||
|
self.ctx.ddata = require('../lib/data/ddata')();
|
||||||
|
self.app.use('/api', api(self.env, ctx));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Activity API _id validation', function() {
|
||||||
|
it('should return 400 for POST with invalid UUID _id', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/activity/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({ "_id": "my-uuid-12345", "created_at": "2024-01-01T00:00:00Z", "steps": 1000 })
|
||||||
|
.expect(400)
|
||||||
|
.expect(function(response) {
|
||||||
|
response.body.should.have.property('status', 400);
|
||||||
|
response.body.message.should.match(/Invalid _id format/i);
|
||||||
|
})
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for PUT with invalid _id', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.put('/api/activity/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({ "_id": "not-valid", "created_at": "2024-01-01T00:00:00Z", "steps": 1000 })
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for DELETE with invalid _id', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.delete('/api/activity/invalid-id')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept POST without _id (auto-generate)', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/activity/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({ "created_at": "2024-01-01T00:00:00Z", "steps": 1000 })
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Food API _id validation', function() {
|
||||||
|
it('should return 400 for POST with invalid UUID _id', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/food/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({ "_id": "my-uuid-12345", "name": "Apple", "type": "food", "carbs": 15 })
|
||||||
|
.expect(400)
|
||||||
|
.expect(function(response) {
|
||||||
|
response.body.should.have.property('status', 400);
|
||||||
|
response.body.message.should.match(/Invalid _id format/i);
|
||||||
|
})
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for PUT with invalid _id', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.put('/api/food/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({ "_id": "not-valid", "name": "Apple", "type": "food", "carbs": 15 })
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for DELETE with invalid _id', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.delete('/api/food/invalid-id')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept POST without _id (auto-generate)', function(done) {
|
||||||
|
request(self.app)
|
||||||
|
.post('/api/food/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({ "name": "Banana", "type": "food", "carbs": 27 })
|
||||||
|
.expect(200)
|
||||||
|
.expect(function(response) {
|
||||||
|
response.body.should.have.property('_id');
|
||||||
|
})
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user