Skip to content
Merged
20 changes: 16 additions & 4 deletions packages/api/lib/controllers/boxesController.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ const getBox = async function getBox (req, res, next) {
* @apiParam (RequestBody) {String="hdc1080","bmp280","tsl45315","veml6070","sds011","bme680","smt50","soundlevelmeter", "windspeed"} [sensorTemplates] Specify which sensors should be included.
* @apiParam (RequestBody) {Object} [mqtt] specify parameters of the MQTT integration for external measurement upload. Please see below for the accepted parameters
* @apiParam (RequestBody) {Object} [ttn] specify parameters for the TTN integration for measurement from TheThingsNetwork.org upload. Please see below for the accepted parameters
* @apiParam (RequestBody) {Boolean="true","false"} [useAuth] whether to use access_token or not for authentication
*
* @apiUse LocationBody
* @apiUse SensorBody
Expand Down Expand Up @@ -424,7 +425,8 @@ const getSketch = async function getSketch (req, res, next) {
res.header('Content-Type', 'text/plain; charset=utf-8');
try {
const box = await Box.findBoxById(req._userParams.boxId, { populate: false, lean: false });
res.send(box.getSketch({

const params = {
serialPort: req._userParams.serialPort,
soilDigitalPort: req._userParams.soilDigitalPort,
soundMeterPort: req._userParams.soundMeterPort,
Expand All @@ -434,15 +436,22 @@ const getSketch = async function getSketch (req, res, next) {
devEUI: req._userParams.devEUI,
appEUI: req._userParams.appEUI,
appKey: req._userParams.appKey
}));
};

// pass access token only if useAuth is true and access_token is available
if (box.access_token) {
params.access_token = box.access_token;
}

res.send(box.getSketch(params));
} catch (err) {
handleError(err, next);
}
};

/**
* @api {delete} /boxes/:senseBoxId Mark a senseBox and its measurements for deletion
* @apiDescription This will delete all the measurements of the senseBox. Please not that the deletion isn't happening immediately.
* @apiDescription This will delete all the measurements of the senseBox. Please note that the deletion isn't happening immediately.
* @apiName deleteBox
* @apiGroup Boxes
* @apiUse ContentTypeJSON
Expand Down Expand Up @@ -506,7 +515,9 @@ module.exports = {
{ name: 'ttn', dataType: 'object' },
{ name: 'sensors', dataType: ['object'] },
{ name: 'addons', dataType: 'object' },
{ predef: 'location' }
{ predef: 'location' },
{ name: 'useAuth', allowedValues: ['true', 'false'] },
{ name: 'generate_access_token', allowedValues: ['true', 'false'] }
]),
checkPrivilege,
updateBox
Expand Down Expand Up @@ -537,6 +548,7 @@ module.exports = {
{ name: 'windSpeedPort', dataType: 'String', defaultValue: 'C', allowedValues: ['A', 'B', 'C'] },
{ name: 'mqtt', dataType: 'object' },
{ name: 'ttn', dataType: 'object' },
{ name: 'useAuth', allowedValues: ['true', 'false'] },
{ predef: 'location', required: true }
]),
postNewBox
Expand Down
33 changes: 28 additions & 5 deletions packages/api/lib/controllers/measurementsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const
* @apiName getLatestMeasurementOfSensor
* @apiUse BoxIdParam
* @apiUse SensorIdParam
* @apiParam {Boolean="true","false"} [onlyValue] If set to true only returns the measured value without information about the sensor. Requires a sensorId.
*/
const getLatestMeasurements = async function getLatestMeasurements (req, res, next) {
const { _userParams: params } = req;
Expand All @@ -44,6 +45,14 @@ const getLatestMeasurements = async function getLatestMeasurements (req, res, ne
if (params.sensorId) {
const sensor = box.sensors.find(s => s._id.equals(params.sensorId));
if (sensor) {
if(params.onlyValue){
if(!sensor.lastMeasurement){
res.send(undefined);
return;
}
res.send(sensor.lastMeasurement.value);
return;
}
res.send(sensor);

return;
Expand Down Expand Up @@ -92,15 +101,17 @@ const getData = function getData (req, res, next) {
let stringifier;

// IDEA: add geojson point featurecollection format
if (format === 'csv' || (download === 'true')) {
if (format === 'csv') {
res.header('Content-Type', 'text/csv');
res.header('Content-Disposition', `attachment; filename=${sensorId}.${format}`);
stringifier = csvStringifier(['createdAt', 'value'], delimiter);
} else if (format === 'json') {
res.header('Content-Type', 'application/json; charset=utf-8');
// IDEA: add geojson point featurecollection format
stringifier = jsonstringify({ open: '[', close: ']' }, jsonLocationReplacer);
}
if (download === 'true') {
res.header('Content-Disposition', `attachment; filename=${sensorId}.${format}`);
}

let measurementsStream = Measurement.getMeasurementsStream(req._userParams)
.on('error', function (err) {
Expand Down Expand Up @@ -208,12 +219,17 @@ const getDataMulti = async function getDataMulti (req, res, next) {
* @apiParam (RequestBody) {String} value the measured value of the sensor. Also accepts JSON float numbers.
* @apiParam (RequestBody) {RFC3339Date} [createdAt] the timestamp of the measurement. Should conform to RFC 3339. Is needed when posting with Location Values!
* @apiParam (RequestBody) {Location} [location] the WGS84-coordinates of the measurement.
* @apiHeader {String} access_token Box' unique access_token. Will be used as authorization token if box has auth enabled (e.g. useAuth: true)
*/
const postNewMeasurement = async function postNewMeasurement (req, res, next) {
const { boxId, sensorId, value, createdAt, location } = req._userParams;

try {
const box = await Box.findBoxById(boxId, { populate: false, lean: false });
if (box.useAuth && box.access_token && box.access_token !== req.headers.authorization) {
throw new UnauthorizedError('Box access token not valid!');
}

const [measurement] = await Measurement.decodeMeasurements([{
sensor_id: sensorId,
value,
Expand Down Expand Up @@ -261,6 +277,7 @@ const postNewMeasurement = async function postNewMeasurement (req, res, next) {
* @apiUse LocationBody
* @apiParam {String} [luftdaten] Specify whatever you want (like `luftdaten=1`. Signals the api to treat the incoming data as luftdaten.info formatted json.
* * @apiParam {String} [hackair] Specify whatever you want (like `hackair=1`. Signals the api to treat the incoming data as hackair formatted json.
* @apiHeader {String} access_token Box' unique access_token. Will be used as authorization token if box has auth enabled (e.g. useAuth: true)
* @apiParamExample {application/json} JSON-Object:
* {
* "sensorID": "value",
Expand Down Expand Up @@ -317,10 +334,15 @@ const postNewMeasurements = async function postNewMeasurements (req, res, next)

if (Measurement.hasDecoder(contentType)) {
try {
const box = await Box.findBoxById(boxId, { populate: false, lean: false, projection: { sensors: 1, locations: 1, lastMeasurementAt: 1, currentLocation: 1, model: 1, access_token: 1 } });
const box = await Box.findBoxById(boxId, { populate: false, lean: false, projection: { sensors: 1, locations: 1, lastMeasurementAt: 1, currentLocation: 1, model: 1, access_token: 1, useAuth: 1 } });

// if (contentType === 'hackair' && box.access_token !== req.headers.authorization) {
// throw new UnauthorizedError('Box access token not valid!');
// }

if (contentType === 'hackair' && box.access_token !== req.headers.authorization) {
throw new UnauthorizedError('Access token not valid!');
// authorization for all boxes that have not opt out
if ((box.useAuth || contentType === 'hackair') && box.access_token && box.access_token !== req.headers.authorization) {
throw new UnauthorizedError('Box access token not valid!');
}

const measurements = await Measurement.decodeMeasurements(req.body, { contentType, sensors: box.sensors });
Expand Down Expand Up @@ -389,6 +411,7 @@ module.exports = {
retrieveParameters([
{ predef: 'boxId', required: true },
{ predef: 'sensorId' },
{ name: 'onlyValue', required: false }
]),
getLatestMeasurements
]
Expand Down
10 changes: 5 additions & 5 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"Norwin Roosen"
],
"dependencies": {
"@sensebox/opensensemap-api-models": "^0.0.25",
"@sensebox/opensensemap-api-models": "^0.0.26-beta.0",
"@turf/area": "^6.0.1",
"@turf/bbox": "^6.0.1",
"@turf/centroid": "^6.0.2",
Expand All @@ -21,19 +21,19 @@
"@turf/triangle-grid": "^6.0.1",
"apicache": "^1.4.0",
"bunyan": "^1.8.14",
"config": "^3.3.1",
"config": "^3.3.2",
"csv-stringify": "^5.5.1",
"dashify": "^2.0.0",
"got": "^11.6.0",
"got": "^11.7.0",
"honeybadger": "^1.4.0",
"isemail": "^3.0.0",
"jsonwebtoken": "^8.1.0",
"millify": "^3.3.0",
"moment": "^2.27.0",
"moment": "^2.29.1",
"ms": "^2.1.1",
"restify": "^5.2.0",
"restify-errors": "^8.0.2",
"simple-statistics": "^7.2.0",
"simple-statistics": "^7.3.0",
"stringify-stream": "^1.0.5",
"uuid": "^7.0.3"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/models/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

## v0.0.26-beta.0
- Authorization

## v0.0.25
- Add Cayenne LPP Decoder

Expand Down
10 changes: 5 additions & 5 deletions packages/models/package.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
{
"name": "@sensebox/opensensemap-api-models",
"description": "openSenseMap data models and database connection",
"version": "0.0.25",
"version": "0.0.26-beta.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@sensebox/osem-protos": "^1.1.0",
"@sensebox/sketch-templater": "^1.8.2",
"@sensebox/sketch-templater": "^1.8.3-beta.0",
"bcrypt": "^5.0.0",
"bunyan": "^1.8.14",
"config": "^3.3.1",
"got": "^11.6.0",
"config": "^3.3.2",
"got": "^11.7.0",
"grpc": "^1.24.3",
"isemail": "^3.0.0",
"jsonpath": "^1.0.0",
"lodash.isequal": "^4.5.0",
"moment": "^2.27.0",
"moment": "^2.29.1",
"mongoose": "^4.13.6",
"mongoose-timestamp": "^0.6",
"uuid": "^7.0.3"
Expand Down
36 changes: 31 additions & 5 deletions packages/models/src/box/box.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ const boxSchema = new Schema({
},
access_token: {
type: String,
required: false
required: true
},
useAuth: {
type: Boolean,
required: true,
default: false,
}
}, { usePushEach: true });
boxSchema.plugin(timestamp);
Expand Down Expand Up @@ -167,6 +172,7 @@ boxSchema.set('toJSON', {
if (options && options.includeSecrets) {
box.integrations = ret.integrations;
box.access_token = ret.access_token;
box.useAuth = ret.useAuth;
}

return box;
Expand Down Expand Up @@ -225,7 +231,8 @@ boxSchema.statics.initNew = function ({
} = { enabled: false },
ttn: {
app_id, dev_id, port, profile, decodeOptions: ttnDecodeOptions
} = {}
} = {},
useAuth
}) {
// if model is not empty, get sensor definitions from products
// otherwise, sensors should not be empty
Expand All @@ -244,6 +251,14 @@ boxSchema.statics.initNew = function ({
sensors = sensorLayouts.getSensorsForModel(model);
}
}
if(model){
//activate useAuth only for certain models until all sketches are updated
if(['homeV2Lora' , 'homeV2Ethernet' , 'homeV2EthernetFeinstaub' , 'homeV2Wifi' , 'homeV2WifiFeinstaub' , 'homeEthernet' , 'homeWifi' , 'homeEthernetFeinstaub' , 'homeWifiFeinstaub' , 'hackair_home_v2'].indexOf(model) != -1){
useAuth = true
} else {
useAuth = false
}
}

const integrations = {
mqtt: { enabled, url, topic, decodeOptions: mqttDecodeOptions, connectionOptions, messageFormat },
Expand Down Expand Up @@ -271,7 +286,8 @@ boxSchema.statics.initNew = function ({
model,
sensors,
integrations,
access_token
access_token,
useAuth
});

};
Expand Down Expand Up @@ -801,7 +817,7 @@ boxSchema.methods.updateSensors = function updateSensors (sensors) {
}
};

boxSchema.methods.getSketch = function getSketch ({ encoding, serialPort, soilDigitalPort, soundMeterPort, windSpeedPort, ssid, password, devEUI, appEUI, appKey } = {}) {
boxSchema.methods.getSketch = function getSketch ({ encoding, serialPort, soilDigitalPort, soundMeterPort, windSpeedPort, ssid, password, devEUI, appEUI, appKey, access_token } = {}) {
if (serialPort) {
this.serialPort = serialPort;
}
Expand All @@ -820,6 +836,7 @@ boxSchema.methods.getSketch = function getSketch ({ encoding, serialPort, soilDi
this.devEUI = devEUI;
this.appEUI = appEUI,
this.appKey = appKey;
this.access_token = access_token;

return templateSketcher.generateSketch(this, { encoding });
};
Expand Down Expand Up @@ -860,12 +877,21 @@ boxSchema.methods.updateBox = function updateBox (args) {
const box = this;

// only grouptag, description and weblink can removed through setting them to empty string ('')
for (const prop of ['name', 'exposure', 'grouptag', 'description', 'weblink', 'image', 'integrations.mqtt', 'integrations.ttn', 'model']) {
for (const prop of ['name', 'exposure', 'grouptag', 'description', 'weblink', 'image', 'integrations.mqtt', 'integrations.ttn', 'model', 'useAuth']) {
if (typeof args[prop] !== 'undefined') {
box.set(prop, (args[prop] === '' ? undefined : args[prop]));
}
}

// if user wants a new access_token
if (typeof args['generate_access_token'] !== 'undefined') {
if (args['generate_access_token'] == 'true') {
// Create new acces token for box
const access_token = crypto.randomBytes(32).toString('hex');
box.set('access_token', access_token);
}
}

if (sensors) {
box.updateSensors(sensors);
} else if (addonToAdd) {
Expand Down
32 changes: 19 additions & 13 deletions packages/models/src/user/mails.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,30 @@ if (config.get('mailer.url')) {

const mailTemplates = {
'newBox' (user, box) {
let sketchParams = {
encoding: 'base64',
ssid: '',
password: '',
serialPort: box.serialPort,
soilDigitalPort: box.soilDigitalPort,
soundMeterPort: box.soundMeterPort,
windSpeedPort: box.windSpeedPort,
devEUI: '',
appEUI: '',
appKey: ''
}

if(box.access_token) {
sketchParams.access_token = box.access_token
}

return {
payload: {
box
},
attachment: {
filename: 'senseBox.ino',
contents: box.getSketch({
encoding: 'base64',
ssid: '',
password: '',
serialPort: box.serialPort,
soilDigitalPort: box.soilDigitalPort,
soundMeterPort: box.soundMeterPort,
windSpeedPort: box.windSpeedPort,
devEUI: '',
appEUI: '',
appKey: ''
})
contents: box.getSketch(sketchParams)
}
};
},
Expand Down Expand Up @@ -112,7 +118,7 @@ if (config.get('mailer.url')) {
},
attachment: {
filename: 'senseBox.ino',
contents: box.getSketch({ encoding: 'base64' })
contents: box.getSketch({ encoding: 'base64', access_token: box.access_token })
}
};
},
Expand Down
6 changes: 6 additions & 0 deletions tests/data/getUserBoxesSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ module.exports = {
'updatedAt': {
'type': 'string'
},
'useAuth': {
'type': 'boolean'
},
'access_token': {
'type': 'string'
},
'currentLocation': {
'type': 'object',
'properties': {
Expand Down
Loading