Merge branch 'dev' into master

This commit is contained in:
gth001
2023-10-16 09:15:46 -07:00
committed by GitHub
148 changed files with 7206 additions and 8378 deletions
+12
View File
@@ -0,0 +1,12 @@
# Browsers we support
# See https://github.com/browserslist/browserslist for details
> 0.25%
ios_saf 9.3
ios_saf 10.3
ios_saf 13.7
ios_saf 14.8
not dead
not and_uc 12.12
not ie 11
+4 -4
View File
@@ -37,11 +37,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -52,7 +52,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -66,4 +66,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
+5 -5
View File
@@ -16,15 +16,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
mongodb-version: [4.2, 4.4]
node-version: [14.x, 16.x]
mongodb-version: [4.4, 5.0, 6.0]
steps:
- name: Git Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
@@ -60,7 +60,7 @@ jobs:
password: ${{ secrets.DOCKER_PASS }}
- name: Clean git Checkout
if: success()
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Build, tag and push the dev Docker image
if: success() && github.ref == 'refs/heads/dev'
uses: docker/build-push-action@v2
+1 -1
View File
@@ -1 +1 @@
14.15.3
16.16.0
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:14.15.3-alpine
FROM node:16.16.0-alpine
LABEL maintainer="Nightscout Contributors"
+129 -31
View File
@@ -1,5 +1,5 @@
Nightscout Web Monitor (a.k.a. cgm-remote-monitor)
======================================
==================================================
![nightscout horizontal](https://cloud.githubusercontent.com/assets/751143/8425633/93c94dc0-1ebc-11e5-99e7-71a8f464caac.png)
@@ -9,8 +9,6 @@ Nightscout Web Monitor (a.k.a. cgm-remote-monitor)
[![Codacy Badge][codacy-img]][codacy-url]
[![Discord chat][discord-img]][discord-url]
[![Deploy to Heroku][heroku-img]][heroku-url] [![Update your site][update-img]][update-fork]
This acts as a web-based CGM (Continuous Glucose Monitor) to allow
multiple caregivers to remotely view a patient's glucose data in
real time. The server reads a MongoDB which is intended to be data
@@ -105,8 +103,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
- [`treatmentnotify` (Treatment Notifications)](#treatmentnotify-treatment-notifications)
- [`basal` (Basal Profile)](#basal-basal-profile)
- [`bolus` (Bolus Rendering)](#bolus-bolus-rendering)
- [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge)
- [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge)
- [`connect` (Nightscout Connect)](#connect-nightscout-connect)
- [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge), _deprecated_
- [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge), _deprecated_
- [`pump` (Pump Monitoring)](#pump-pump-monitoring)
- [`openaps` (OpenAPS)](#openaps-openaps)
- [`loop` (Loop)](#loop-loop)
@@ -132,40 +131,38 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
## Supported configurations:
If you plan to use Nightscout, we recommend using [Heroku](https://nightscout.github.io/nightscout/new_user/) as this is free and easy to use.
We used to recommend hostig at Azure, but the resource needs of Nightscout have grown over the years and Azure won't comfortably run Nightscout
anymore in the free tier. If you're hosting in Azure and looking to update your site, we recommend you
[switch from Azure to Heroku](http://openaps.readthedocs.io/en/latest/docs/While%20You%20Wait%20For%20Gear/nightscout-setup.html#switching-from-azure-to-heroku)
as you're likely to hit issues in the process of updating the site.
- [Nightscout Setup with Heroku](https://nightscout.github.io/nightscout/new_user/) (recommended)
- [Nightscout Setup](https://nightscout.github.io/nightscout/new_user/) (recommended)
While you can install Nightscout on a virtual server or a Raspberry Pi, we do not recommend this unless you have at least some
experience hosting Node applications and development using the toolchain in use with Nightscout. Heroku automates all of the
hosting for you and even many of the dvelopers run their production sites in Heroku due to convenience.
experience hosting Node applications and development using the toolchain in use with Nightscout.
If you're a hosting provider and want to provide our users additional free hosting options,
If you're a hosting provider and want to provide our users additional hosting options,
you're welcome to issue a documentation pull request with instructions on how to setup Nightscout on your system.
## Recommended minimum browser versions for using Nightscout:
Older versions of the browsers might work, but are untested.
Our [browserslist](https://github.com/browserslist/browserslist) policy is documented in `.browserlistrc`.
We currently support approximately [91%](https://browsersl.ist/?q=%3E+0.25%25%2C+ios_saf+9.3%2C+ios_saf+10.3%2C+ios_saf+13.7%2C+ios_saf+14.8%2C+not+dead%2C+not+and_uc+12.12%2C+not+ie+11%0A) of all browsers globally used. These include:
- Android 4
- iOS 6
- Chrome 35
- Edge 17
- Firefox 61
- Opera 12.1
- Safari 6 (macOS 10.7)
- Internet Explorer: not supported
- Android Chrome: 104 or later (`and_chr`)
- Google Chrome: 101 or later (`chrome`)
- Microsoft Edge: 103 or later (`edge`)
- Mozilla Firefox: 102 or later (`firefox`)
- Apple Safari on iOS: 15.5 or later (`ios_saf`)
- Opera Mini on Android: 63 or later (`op_mini`)
- Opera: 88 or later (`opera`)
- Apple Safari for macOS 10.15 Catalina or later: : 15.5 or later (`safari`)
- Samsung Internet on Android: 17.0 or later (`samsung`)
- Internet Explorer 11 : not supported
Some features may not work with devices/browsers on the older end of these requirements.
Older versions or other browsers might work, but are untested and unsupported. We'll try to to keep Nightscout compatible with older iPads (e.g. Safari on iOS 10.3.4), but note that those devices are not supported by Apple anymore and have known security issues. Debugging these old devices gets harder due to Apple not supporting debugging the old devices on Macs that have been updated. Some features may not work with devices/browsers on the older end of these requirements.
## Windows installation software requirements:
- [Node.js](http://nodejs.org/) Latest Node 12 LTS. Node versions that do not have the latest security patches will not work. Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `bin/setup.sh`)
- [MongoDB](https://www.mongodb.com/download-center?jmp=nav#community) 3.x or later. MongoDB 2.4 is only supported for Raspberry Pi.
## Installation software requirements:
- [Node.js](http://nodejs.org/) Latest Node v14 or v16 LTS. Node versions that do not have the latest security patches will not be supported. Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `bin/setup.sh`)
- [MongoDB](https://www.mongodb.com/download-center?jmp=nav#community) 4.2 or 4.4.
As a non-root user clone this repo then install dependencies into the root of the project:
@@ -184,7 +181,7 @@ $ npm install
- If deploying the software to Microsoft Azure, you must set ** in the app settings for *WEBSITE_NODE_DEFAULT_VERSION* and *SCM_COMMAND_IDLE_TIMEOUT* **before** you deploy the latest Nightscout or the site deployment will likely fail. Other hosting environments do not require this setting. Additionally, if using the Azure free hosting tier, the installation might fail due to resource constraints imposed by Azure on the free hosting. Please set the following settings to the environment in Azure:
```
WEBSITE_NODE_DEFAULT_VERSION=10.15.2
WEBSITE_NODE_DEFAULT_VERSION=16.16.0
SCM_COMMAND_IDLE_TIMEOUT=300
```
- See [install MongoDB, Node.js, and Nightscouton a single Windows system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). if you want to host your Nightscout outside of the cloud. Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10.
@@ -232,7 +229,7 @@ Once you've installed Nightscout, you can access API documentation by loading `/
* Boluses over 2U: `http://localhost:1337/api/v1/treatments.json?find[insulin][$gte]=2`
The API is Swagger enabled, so you can generate client code to make working with the API easy.
To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or review [swagger.yaml](swagger.yaml).
To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or review [swagger.yaml](lib/server/swagger.yaml).
## Environment
@@ -256,6 +253,14 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
* `IMPORT_CONFIG` - Used to import settings and extended settings from a url such as a gist. Structure of file should be something like: `{"settings": {"theme": "colors"}, "extendedSettings": {"upbat": {"enableAlerts": true}}}`
* `TREATMENTS_AUTH` (`on`) - possible values `on` or `off`. Deprecated, if set to `off` the `careportal` role will be added to `AUTH_DEFAULT_ROLES`
#### Data Rights
These are useful to help protect your rights to portability and
autonomy for your data:
* `OBSCURED` - list, identical to `ENABLE`, a list of plugins to
obscure.
* `OBSCURE_DEVICE_PROVENANCE` - Required, a string visible to the [companies deciding to filter based on your data](https://help.sugarmate.io/en/articles/4673402-adding-a-nightscout-data-source). For example, `my-data-rights`.
### Alarms
These alarm setting affect all delivery methods (browser, Pushover, IFTTT, etc.). Values and settings entered here will be the defaults for new browser views, but will be overridden if different choices are made in the settings UI.
@@ -484,8 +489,98 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
* `BOLUS_RENDER_FORMAT` (`default`) - Possible values are `hidden`, `default` (with leading zero and U), `concise` (with U, without leading zero), and `minimal` (without leading zero and U).
* `BOLUS_RENDER_FORMAT_SMALL` (`default`) - Possible values are `hidden`, `default` (with leading zero and U), `concise` (with U, without leading zero), and `minimal` (without leading zero and U).
##### `connect` (Nightscout Connect)
Connect common diabetes cloud resources to Nightscout.
Include the keyword `connect` in the `ENABLE` list.
Nightscout connection uses extended settings using the environment variable prefix `CONNECT_`.
* `CONNECT_SOURCE` - The name for the source of one of the supported inputs. one of `nightscout`, `dexcomshare`, etc...
###### Nightscout
> Work in progress
To sync from another Nightscout site, include `CONNECT_SOURCE_ENDPOINT` and
`CONNECT_SOURCE_API_SECRET`.
* `CONNECT_SOURCE=nightscout`
* `CONNECT_SOURCE_ENDPOINT=<URL>`
* `CONNECT_SOURCE_API_SECRET=<OPTIONAL_API_SECRET>`
The `CONNECT_SOURCE_ENDPOINT` must be a fully qualified URL and may contain a
`?token=<subject>` query string to specify an accessToken.
The `CONNECT_SOURCE_API_SECRET`, if provided, will be used to create a token
called `nightscout-connect-reader`. This information or the token provided in
the query will be used to read information from Nightscout and is optional if
the site is readable by default.
Select this driver by setting `CONNECT_SOURCE` equal to `nightscout`.
###### Dexcom Share
To synchronize from Dexcom Share use the following variables.
* `CONNECT_SOURCE=dexcomshare`
* `CONNECT_SHARE_ACCOUNT_NAME=`
* `CONNECT_SHARE_PASSWORD=`
Optional, `CONNECT_SHARE_REGION` and `CONNECT_SHARE_SERVER` do the same thing, only specify one.
* `CONNECT_SHARE_REGION=` `ous` or `us`. `us` is the default if nothing is
provided. Selecting `us` sets `CONNECT_SHARE_SERVER` to `share2.dexcom.com`.
Selecting `ous` here sets `CONNECT_SHARE_SERVER` to `shareous1.dexcom.com`.
* `CONNECT_SHARE_SERVER=` set the server domain to use.
###### Glooko
> Note: Experimental.
To synchronize from Glooko use the following variables.
* `CONNECT_SOURCE=glooko`
* `CONNECT_GLOOKO_EMAIL=`
* `CONNECT_GLOOKO_PASSWORD=`
By default, `CONNECT_GLOOKO_SERVER` is set to `api.glooko.com` because the
default value for `CONNECT_GLOOKO_ENV` is `default`.
* `CONNECT_GLOOKO_ENV` is the word `default` by defalt. Other values are
`development`, `production`, for `api.glooko.work`, and
`externalapi.glooko.com`, respectively.
* `CONNECT_GLOOKO_SERVER` the hostname server to use - `api.glooko.com` by `default`.
If both, `CONNECT_GLOOKO_SERVER` and `CONNECT_GLOOKO_ENV` are set, only
`CONNECT_GLOOKO_SERVER` will be used.
###### Libre Link Up
To synchronize from Libre Link Up use the following variables.
* `CONNECT_SOURCE=linkup`
* `CONNECT_LINK_UP_USERNAME=`
* `CONNECT_LINK_UP_PASSWORD=`
By default, `CONNECT_LINK_UP_SERVER` is set to `api-eu.libreview.io` because the
default value for `CONNECT_LINK_UP_REGION` is `EU`.
Other available values for `CONNECT_LINK_UP_REGION`:
* `US`, `EU`, `DE`, `FR`, `JP`, `AP`, `AU`, `AE`
For folks connected to many patients, you can provide the patient ID by setting
the `CONNECT_LINK_UP_PATIENT_ID` variable.
###### Minimed Carelink
To synchronize from Medtronic Minimed Carelink, set the following
environment variables.
* `CONNECT_SOURCE=minimedcarelink`
* `CONNECT_CARELINK_USERNAME`
* `CONNECT_CARELINK_PASSWORD`
* `CONNECT_CARELINK_REGION` Either `eu` to set `CONNECT_CARELINK_SERVER` to
`carelink.minimed.eu` or `us` to use `carelink.minimed.com`.
For folks using the new Many to Many feature, please provide the username of the
patient to follow using `CONNECT_CARELINK_PATIENT_USERNAME` variable.
##### `bridge` (Share2Nightscout bridge)
Glucose reading directly from the Dexcom Share service, uses these extended settings:
> **Deprecated** Please consider using the `connect` plugin instead.
Fetch glucose reading directly from the Dexcom Share service, uses these extended settings:
* `BRIDGE_USER_NAME` - Your username for the Share service.
* `BRIDGE_PASSWORD` - Your password for the Share service.
* `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time (in milliseconds) to wait between each update.
@@ -496,6 +591,9 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
* `BRIDGE_SERVER` (``) - The default blank value is used to fetch data from Dexcom servers in the US. Set to (`EU`) to fetch from European servers instead.
##### `mmconnect` (MiniMed Connect bridge)
> **Deprecated** Please consider using the `connect` plugin instead.
Transfer real-time MiniMed Connect data from the Medtronic CareLink server into Nightscout ([read more](https://github.com/mddub/minimed-connect-to-nightscout))
* `MMCONNECT_USER_NAME` - Your user name for CareLink Connect.
* `MMCONNECT_PASSWORD` - Your password for CareLink Connect.
+5
View File
@@ -151,6 +151,11 @@
"description": "Default setting for new browser views, for the time mode. ('12' or '24')",
"value": "12",
"required": false
},
"USE_NPM_INSTALL": {
"description": "You need to have this set for deployment to work in Heroku",
"value": "true",
"required": true
}
},
"addons": [
+1 -1
View File
@@ -218,7 +218,7 @@
},
"WEBSITE_NODE_DEFAULT_VERSION": {
"type": "string",
"defaultValue": "8.11.1"
"defaultValue": "16.16.0"
}
},
"resources": [{
+8 -2
View File
@@ -18,14 +18,20 @@ require('../node_modules/flot/jquery.flot.time');
require('../node_modules/flot/jquery.flot.pie');
require('../node_modules/flot/jquery.flot.fillbetween');
window.moment = require('moment-timezone');
const moment = require('moment-timezone');
window.moment = moment;
window.Nightscout = window.Nightscout || {};
var ctx = {
moment: moment
};
window.Nightscout = {
client: require('../lib/client'),
units: require('../lib/units')(),
admin_plugins: require('../lib/admin_plugins/')()
admin_plugins: require('../lib/admin_plugins/')(ctx)
};
window.Nightscout.report_plugins_preinit = require('../lib/report_plugins/');
+10
View File
@@ -1,10 +1,18 @@
version: '3'
x-logging:
&default-logging
options:
max-size: '10m'
max-file: '5'
driver: json-file
services:
mongo:
image: mongo:4.4
volumes:
- ${NS_MONGO_DATA_DIR:-./mongo-data}:/data/db:cached
logging: *default-logging
nightscout:
image: nightscout/cgm-remote-monitor:latest
@@ -19,6 +27,7 @@ services:
- 'traefik.http.routers.nightscout.rule=Host(`localhost`)'
- 'traefik.http.routers.nightscout.entrypoints=websecure'
- 'traefik.http.routers.nightscout.tls.certresolver=le'
logging: *default-logging
environment:
### Variables for the container
NODE_ENV: production
@@ -74,3 +83,4 @@ services:
volumes:
- './letsencrypt:/letsencrypt'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
logging: *default-logging
+3 -2
View File
@@ -1,6 +1,6 @@
'use strict';
var moment = require('moment');
var moment;
var cleanentriesdb = {
name: 'cleanentriesdb'
@@ -8,7 +8,8 @@ var cleanentriesdb = {
, pluginType: 'admin'
};
function init() {
function init(ctx) {
moment = ctx.moment;
return cleanentriesdb;
}
+3 -2
View File
@@ -1,6 +1,6 @@
'use strict';
var moment = require('moment');
var moment;
var cleanstatusdb = {
name: 'cleanstatusdb'
@@ -8,7 +8,8 @@ var cleanstatusdb = {
, pluginType: 'admin'
};
function init () {
function init (ctx) {
moment = ctx.moment;
return cleanstatusdb;
}
+3 -2
View File
@@ -1,6 +1,6 @@
'use strict';
var moment = require('moment');
var moment;
var cleantreatmentsdb = {
name: 'cleantreatmentsdb'
@@ -8,7 +8,8 @@ var cleantreatmentsdb = {
, pluginType: 'admin'
};
function init() {
function init(ctx) {
moment = ctx.moment;
return cleantreatmentsdb;
}
+7 -7
View File
@@ -3,14 +3,14 @@
var _find = require('lodash/find');
var _each = require('lodash/each');
function init() {
function init(ctx) {
var allPlugins = [
require('./subjects')()
, require('./roles')()
, require('./cleanstatusdb')()
, require('./cleantreatmentsdb')()
, require('./cleanentriesdb')()
, require('./futureitems')()
require('./subjects')(ctx)
, require('./roles')(ctx)
, require('./cleanstatusdb')(ctx)
, require('./cleantreatmentsdb')(ctx)
, require('./cleanentriesdb')(ctx)
, require('./futureitems')(ctx)
];
function plugins(name) {
+1 -1
View File
@@ -88,7 +88,7 @@ function configure(app, wares, ctx) {
api.post('/activity/', ctx.authorization.isPermitted('api:activity:create'), post_response);
api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) {
ctx.activity.remove(req.params._id, function() {
ctx.activity.deleteOne(req.params._id, function() {
res.json({});
});
});
+1 -8
View File
@@ -94,7 +94,7 @@ function configure (app, wares, ctx, env) {
var handler = ctx.alexa.getIntentHandler(intentName, metric);
if (handler){
var sbx = initializeSandbox();
var sbx = ctx.sbx;
handler(next, slots, sbx);
return;
} else {
@@ -103,13 +103,6 @@ function configure (app, wares, ctx, env) {
}
}
function initializeSandbox() {
var sbx = require('../../sandbox')();
sbx.serverInit(env, ctx);
ctx.plugins.setProperties(sbx);
return sbx;
}
return api;
}
+6 -4
View File
@@ -472,14 +472,16 @@ function configure (app, wares, ctx, env) {
});
} else {
inMemoryCollection = ctx.cache.getData('entries');
inMemoryCollection = _.sortBy(inMemoryCollection, function(item) {
return item.mills;
}).reverse();
}
if (inMemoryPossible && query.count <= inMemoryCollection.length) {
res.entries = _.cloneDeep(_.take(inMemoryCollection,query.count));
for (let i = 0; i < res.entries.length; i++) {
let e = res.entries[i];
e.mills = e.mills || e.date;
}
res.entries_err = null;
return next();
}
+1 -1
View File
@@ -71,7 +71,7 @@ function configure (app, wares, ctx) {
});
// delete record
api.delete('/food/:_id', ctx.authorization.isPermitted('api:food:delete'), function(req, res) {
ctx.food.remove(req.params._id, function ( ) {
ctx.food.deleteOne(req.params._id, function ( ) {
res.json({ });
});
});
+1 -8
View File
@@ -31,7 +31,7 @@ function configure (app, wares, ctx, env) {
var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters.metric);
if (handler){
var sbx = initializeSandbox();
var sbx = ctx.sbx;
handler(function (title, response) {
res.json(ctx.googleHome.buildSpeechletResponse(response, false));
next( );
@@ -46,13 +46,6 @@ function configure (app, wares, ctx, env) {
ctx.virtAsstBase.setupMutualIntents(ctx.googleHome);
function initializeSandbox() {
var sbx = require('../../sandbox')();
sbx.serverInit(env, ctx);
ctx.plugins.setProperties(sbx);
return sbx;
}
return api;
}
+4
View File
@@ -1,12 +1,16 @@
'use strict';
var consts = require('../constants');
var bodyParser = require('body-parser');
function configure (app, wares, ctx) {
var express = require('express')
, api = express.Router( )
;
app.use(bodyParser.urlencoded({extended : true}));
app.use(bodyParser.json());
api.post('/notifications/pushovercallback', function (req, res) {
if (ctx.pushnotify.pushoverAck(req.body)) {
res.sendStatus(consts.HTTP_OK);
+1 -1
View File
@@ -92,7 +92,7 @@ function configure (app, wares, ctx) {
});
api.delete('/profile/:_id', ctx.authorization.isPermitted('api:profile:delete'), function(req, res) {
ctx.profile.remove(req.params._id, function ( ) {
ctx.profile.deleteOne(req.params._id, function ( ) {
res.json({ });
});
});
+12 -10
View File
@@ -63,18 +63,20 @@ function configure (app, wares, ctx, env) {
}
});
if (!_isNil(d1)) res.setHeader('Last-Modified', d1.toUTCString());
if (!_isNil(d1)) {
res.setHeader('Last-Modified', d1.toUTCString());
if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) {
res.status(304).send({
status: 304
, message: 'Not modified'
, type: 'internal'
});
return;
} else {
return res.json(results);
if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) {
res.status(304).send({
status: 304
, message: 'Not modified'
, type: 'internal'
});
return;
}
}
return res.json(results);
}
// List treatments available
+22
View File
@@ -0,0 +1,22 @@
'use strict';
function create (env, ctx, apiv1) {
var express = require('express')
, app = express( )
;
const ddata = require('../data/endpoints')(env, ctx);
const notificationsV2 = require('./notifications-v2')(app, ctx);
const summary = require('./summary')(env, ctx);
app.use('/', apiv1);
app.use('/properties', ctx.properties);
app.use('/authorization', ctx.authorization.endpoints);
app.use('/ddata', ddata);
app.use('/notifications', notificationsV2);
app.use('/summary', summary);
return app;
}
module.exports = create;
@@ -18,11 +18,11 @@ function create (env, ctx) {
*
* Expecting to define extended syntax and support for several query params
*/
properties.use(ctx.authorization.isPermitted('api:entries:read'),
ctx.authorization.isPermitted('api:treatments:read'));
properties.get(['/', '/*'], function getProperties (req, res) {
var sbx = sandbox.serverInit(env, ctx);
ctx.plugins.setProperties(sbx);
if (!ctx.sbx) res.json({});
function notEmpty (part) {
return ! _isEmpty(part);
@@ -36,10 +36,10 @@ function create (env, ctx) {
selected = _filter(segments[0].split(','), notEmpty);
}
var result = sbx.properties;
var result = ctx.sbx.properties;
if (selected.length > 0) {
result = _pick(sbx.properties, selected);
result = _pick(ctx.sbx.properties, selected);
}
result = env.settings.filteredSettings(result);
@@ -57,4 +57,4 @@ function create (env, ctx) {
return properties;
}
module.exports = create;
module.exports = create;
+139
View File
@@ -0,0 +1,139 @@
const { data } = require("jquery");
const dataProcessor = {};
function _hhmmAfter (hhmm, mills) {
var date = new Date(mills);
var withSameDate = new Date(
1900 + date.getYear()
, date.getMonth()
, date.getDate()
, parseInt(hhmm.substr(0, 2), 10)
, parseInt(hhmm.substr(3, 5), 10)
).getTime();
return withSameDate > date ? withSameDate : withSameDate + 24 * 60 * 60 * 1000;
}
// Outputs temp basal objects describing the profile temps for the duration
function _profileBasalsInWindow (basals, start, end) {
if (basals.length === 0) {
return [];
}
var i;
var out = [];
function nextProfileBasal () {
i = (i + 1) % basals.length;
var lastStart = out[out.length - 1].start;
return {
start: _hhmmAfter(basals[i]['time'], lastStart)
, absolute: parseFloat(basals[i]['value'])
, profile: 1
};
}
i = 0;
var startHHMM = new Date(start).toTimeString().substr(0, 5);
while (i < basals.length - 1 && basals[i + 1]['time'] <= startHHMM) {
i++;
}
out.push({
start: start
, absolute: parseFloat(basals[i]['value'])
, });
var next = nextProfileBasal();
while (next.start < end) {
out.push(next);
next = nextProfileBasal();
}
return out;
}
dataProcessor.filterSameAbsTemps = function filterSameAbsTemps (tempdata) {
var out = [];
var j = 0;
for (let i = 0; i < tempdata.length; i++) {
const temp = tempdata[i];
if (i == tempdata.length - 1) {
// If last was merged, skip
if (j != i) {
out.push(temp);
}
break;
}
const nextTemp = tempdata[i + 1];
if (temp.duration && (temp.start + temp.duration) >= nextTemp.start) {
if (temp.absolute == nextTemp.absolute) {
// Merge and skip next
temp.duration = nextTemp.start - temp.start + nextTemp.duration;
i += 1;
j = i;
} else {
// Adjust duration
temp.duration = nextTemp.start - temp.start;
}
}
out.push(temp);
}
return out;
}
dataProcessor.processTempBasals = function processTempBasals (profile, tempBasals, dataCap) {
var profileBasals = profile.basal;
var temps = tempBasals.map(function(temp) {
return {
start: new Date(temp['created_at']).getTime()
, duration: temp['duration'] === undefined ? 0 : parseInt(temp['duration'], 10) * 60 * 1000
, absolute: temp['absolute'] === undefined ? 0 : parseFloat(temp['absolute'])
};
}).concat([
{ start: Date.now() - 24 * 60 * 60 * 1000, duration: 0 }
, { start: Date.now(), duration: 0}
]).sort(function(a, b) {
return a.start - b.start;
});
var out = [];
temps.forEach(function(temp) {
var last = out[out.length - 1];
if (last && last.duration !== undefined && last.start + last.duration < temp.start) {
Array.prototype.push.apply(out, _profileBasalsInWindow(profileBasals, last.start + last.duration, temp.start));
}
if (temp.duration) out.push(temp);
});
var o2 = out;
var prevLength = 1;
var newLength = 0;
while (prevLength != newLength) {
prevLength = o2.length;
o2 = dataProcessor.filterSameAbsTemps(o2);
newLength = o2.length;
}
var o3 = [];
// Return temps from last hours
for (var i = 0; i < o2.length; i++) {
if ((o2[i].start + o2[i].duration) > dataCap) o3.push(o2[i]);
}
// Convert durations to seconds
for (var i = 0; i < o3.length; i++) {
o3[i].duration = o3[i].duration / 1000;
}
return o3;
}
module.exports = dataProcessor;
+135
View File
@@ -0,0 +1,135 @@
'use strict';
function configure (env, ctx) {
const _ = require('lodash')
, basalProcessor = require('./basaldataprocessor')
, express = require('express')
, api = express.Router();
const defaultHours = 6;
api.use(ctx.wares.compression());
function removeProps(obj,keys){
if(Array.isArray(obj)){
obj.forEach(function(item){
removeProps(item,keys)
});
}
else if(typeof obj === 'object' && obj != null){
Object.getOwnPropertyNames(obj).forEach(function(key){
if(keys.indexOf(key) !== -1)delete obj[key];
else removeProps(obj[key],keys);
});
}
}
function processSGVs(sgvs, hours) {
const bgData = [];
const dataCap = Date.now() - (hours * 60 * 60 * 1000);
for (let i = 0; i < sgvs.length; i++) {
const bg = sgvs[i];
if (bg.mills >= dataCap) {
let item = {
sgv: bg.mgdl
, mills: bg.mills
};
// only push noise data if there is noise
if (bg.noise != 1) { item.noise = bg.noise; }
bgData.push(item);
}
}
return bgData;
}
// Collect treatments that contain insulin or carbs, temp basals
function processTreatments(treatments, profile, hours) {
const rVal = {
tempBasals: [],
treatments: [],
targets: []
};
let _temps = [];
const dataCap = Date.now() - (hours * 60 * 60 * 1000);
for (let i = 0; i < treatments.length; i++) {
const t = treatments[i];
if (t.eventType == 'Temp Basal') {
_temps.push(t);
continue;
}
if (t.eventType == 'Temporary Target') {
rVal.targets.push({
targetTop: Math.round(t.targetTop),
targetBottom: Math.round(t.targetBottom),
duration: t.duration*60,
mills: t.mills
});
continue;
}
if (t.insulin || t.carbs) {
if (t.mills >= dataCap) {
const _t = {
mills: t.mills
};
if (!isNaN(t.carbs)) _t.carbs = t.carbs;
if (!isNaN(t.insulin)) _t.insulin = t.insulin;
rVal.treatments.push(_t);
}
continue;
}
}
rVal.tempBasals = basalProcessor.processTempBasals(profile,_temps, dataCap);
return rVal;
}
function constructState() {
const p = _.get(ctx, 'sbx.properties');
const state = {
iob: Math.round(_.get(p,'iob.iob')*100)/100,
cob: Math.round(_.get(p,'cob.cob')),
bwp: Math.round(_.get(p,'bwp.bolusEstimate')*100)/100,
cage: _.get(p,'cage.age'),
sage: _.get(p,'sage.age'),
iage: _.get(p,'iage.age'),
bage: _.get(p,'bage.age'),
battery: _.get(p,'upbat.level')
}
return state;
}
api.get('/', ctx.authorization.isPermitted('api:*:read'), function (req, res) {
const hours = req.query.hours || defaultHours;
const sgvs = processSGVs(ctx.ddata.sgvs, hours);
const profile = _.clone(ctx.sbx.data.profile.getCurrentProfile());
removeProps(profile,['timeAsSeconds']);
const treatments = processTreatments(ctx.ddata.treatments, profile, hours);
const state = constructState();
res.setHeader('content-type', 'application/json');
res.write(JSON.stringify({
sgvs,
treatments,
profile,
state
}));
res.end( );
});
return api;
}
module.exports = configure;
+152
View File
@@ -0,0 +1,152 @@
'use strict';
const apiConst = require('./const');
const forwarded = require('forwarded-for');
function getRemoteIP (req) {
const address = forwarded(req, req.headers);
return address.ip;
}
/**
* Socket.IO broadcaster of alarm and annoucements
*/
function AlarmSocket (app, env, ctx) {
const self = this;
var levels = ctx.levels;
const LOG_GREEN = '\x1B[32m'
, LOG_MAGENTA = '\x1B[35m'
, LOG_RESET = '\x1B[0m'
, LOG = LOG_GREEN + 'ALARM SOCKET: ' + LOG_RESET
, LOG_ERROR = LOG_MAGENTA + 'ALARM SOCKET: ' + LOG_RESET
, NAMESPACE = '/alarm'
;
/**
* Initialize socket namespace and bind the events
* @param {Object} io Socket.IO object to multiplex namespaces
*/
self.init = function init (io) {
self.io = io;
self.namespace = io.of(NAMESPACE);
self.namespace.on('connection', function onConnected (socket) {
const remoteIP = getRemoteIP(socket.request);
console.log(LOG + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP);
socket.on('disconnect', function onDisconnect () {
console.log(LOG + 'Disconnected client ID: ', socket.client.id);
});
socket.on('subscribe', function onSubscribe (message, returnCallback) {
self.subscribe(socket, message, returnCallback);
});
});
ctx.bus.on('notification', self.emitNotification);
};
/**
* Authorize Socket.IO client and subscribe him to authorized rooms
*
* Support webclient authorization with api_secret is added
*
* @param {Object} socket
* @param {Object} message input message from the client
* @param {Function} returnCallback function for returning a value back to the client
*/
self.subscribe = function subscribe (socket, message, returnCallback) {
const shouldCallBack = typeof(returnCallback) === 'function';
// Native client
if (message && message.accessToken) {
return ctx.authorization.resolveAccessToken(message.accessToken, function resolveFinishForToken (err, auth) {
if (err) {
console.log(`${LOG_ERROR} Authorization failed for accessToken:`, message.accessToken);
if (shouldCallBack) {
returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN });
}
return err;
} else {
// Subscribe for acking alarms
socket.on('ack', function onAck (level, group, silenceTime) {
ctx.notifications.ack(level, group, silenceTime, true);
console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime);
});
var okResponse = { success: true, message: 'Subscribed for alarms' }
if (shouldCallBack) {
returnCallback(okResponse);
}
return okResponse;
}
});
}
// Web client (jwt access token or api_hash)
if (message && (message.jwtToken || message.secret)) {
return ctx.authorization.resolve({ api_secret: message.secret, token: message.jwtToken, ip: getRemoteIP(socket.request) }, function resolveFinish (err, auth) {
if (err) {
console.log(`${LOG_ERROR} Authorization failed for jwtToken:`, message.jwtToken);
if (shouldCallBack) {
returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN });
}
return err;
} else {
// Subscribe for acking alarms
socket.on('ack', function onAck (level, group, silenceTime) {
ctx.notifications.ack(level, group, silenceTime, true);
console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime);
});
var okResponse = { success: true, message: 'Subscribed for alarms' }
if (shouldCallBack) {
returnCallback(okResponse);
}
return okResponse;
}
});
}
console.log(`${LOG_ERROR} Authorization failed for message:`, message);
if (shouldCallBack) {
returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN});
}
};
/**
* Emit alarm to subscribed clients
* @param {Object} notofication to emit
*/
self.emitNotification = function emitNotification (notify) {
if (notify.clear) {
self.namespace.emit('clear_alarm', notify);
console.info(LOG + 'emitted clear_alarm to all clients');
} else if (notify.level === levels.WARN) {
self.namespace.emit('alarm', notify);
console.info(LOG + 'emitted alarm to all clients');
} else if (notify.level === levels.URGENT) {
self.namespace.emit('urgent_alarm', notify);
console.info(LOG + 'emitted urgent_alarm to all clients');
} else if (notify.isAnnouncement) {
self.namespace.emit('announcement', notify);
console.info(LOG + 'emitted announcement to all clients');
} else {
self.namespace.emit('notification', notify);
console.info(LOG + 'emitted notification to all clients');
}
};
}
module.exports = AlarmSocket;
+151
View File
@@ -0,0 +1,151 @@
# APIv3: Socket.IO alarm channel
### Complete sample client code
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>APIv3 Socket.IO sample for alarms</title>
<link rel="icon" href="images/favicon.png" />
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>
<script>
const socket = io('https://nsapiv3.herokuapp.com/alarm');
socket.on('connect', function () {
socket.emit('subscribe', {
accessToken: 'testadmin-ad3b1f9d7b3f59d5'
}, function (data) {
if (data.success) {
console.log('subscribed for alarms', data.message);
}
else {
console.error(data.message);
}
});
});
socket.on('announcement', function (data) {
console.log(data);
});
socket.on('alarm', function (data) {
console.log(data);
});
socket.on('urgent_alarm', function (data) {
console.log(data);
});
socket.on('clear_alarm', function (data) {
console.log(data);
});
</script>
</body>
</html>
```
### Subscription (authorization)
The client must first subscribe to the channel that is exposed at `alarm` namespace, ie the `/alarm` subadress of the base Nightscout's web address (without `/api/v3` subaddress).
```javascript
const socket = io('https://nsapiv3.herokuapp.com/alarm');
```
Subscription is requested by emitting `subscribe` event to the server, while including document with parameter:
* `accessToken`: required valid accessToken of the security subject, which has been prepared in *Admin Tools* of Nightscout.
```javascript
socket.on('connect', function () {
socket.emit('subscribe', {
accessToken: 'testadmin-ad3b1f9d7b3f59d5'
}, ...
```
On the server, the subject is identified and authenticated (by the accessToken). Ne special rights are required.
If the authentication was successful `success` = `true` is set in the response object and the field `message` contains a text response.
In other case `success` = `false` is set in the response object and the field `message` contains an error message.
```javascript
function (data) {
if (data.success) {
console.log('subscribed for alarms', data.message);
}
else {
console.error(data.message);
}
});
});
```
### Acking alarms and announcements
If the client is successfully subscribed it can ack alarms and announcements by emitting `ack` message.
```javascript
socket.emit('ack', level, group, silenceTimeInMilliseconds);
```
where `level` and `group` are values from alarm being acked and `silenceTimeInMilliseconds` is duration. During this time alarms of the same type are not emmited.
### Receiving events
After the successful subscription the client can start listening to `announcement`, `alarm` , `urgent_alarm` and/or `clear_alarm` events of the socket.
##### announcement
The received object contains similiar json:
```javascript
{
"level":0,
"title":"Announcement",
"message":"test",
"plugin":{"name":"treatmentnotify","label":"Treatment Notifications","pluginType":"notification","enabled":true},
"group":"Announcement",
"isAnnouncement":true,
"key":"9ac46ad9a1dcda79dd87dae418fce0e7955c68da"
}
```
##### alarm, urgent_alarm
The received object contains similiar json:
```javascript
{
"level":1,
"title":"Warning HIGH",
"message":"BG Now: 5 -0.2 → mmol\/L\nRaw BG: 4.8 mmol\/L Čistý\nBG 15m: 4.8 mmol\/L\nIOB: -0.02U\nCOB: 0g",
"eventName":"high",
"plugin":{"name":"simplealarms","label":"Simple Alarms","pluginType":"notification","enabled":true},
"pushoverSound":"climb",
"debug":{"lastSGV":5,"thresholds":{"bgHigh":180,"bgTargetTop":75,"bgTargetBottom":72,"bgLow":70}},
"group":"default",
"key":"simplealarms_1"
}
```
##### clear_alarm
The received object contains similiar json:
```javascript
{
"clear":true,
"title":"All Clear",
"message":"default - Urgent was ack'd",
"group":"default"
}
```
+1 -2
View File
@@ -20,8 +20,7 @@ The identity of the client is represented by the *subject* to whom the access le
For each security *subject*, the system automatically generates an *access token* that is difficult to guess since it is derived from the secret *API_SECRET*. The *access token* must be included in every secured API operation to decode the client's identity and determine its authorization level. In this way, it is then possible to resolve whether the client has the permission required by a particular API operation.
There are two ways to authorize API calls:
- use `token` query parameter to pass the *access token*, eg. `token=testreadab-76eaff2418bfb7e0`
There is only one way to authorize API calls:
- use so-called [JSON Web Tokens](https://jwt.io "JSON Web Tokens")
- at first let the `/api/v2/authorization/request` generates you a particular JWT, eg. `GET https://nsapiv3.herokuapp.com/api/v2/authorization/request/testreadab-76eaff2418bfb7e0`
- then, to each secure API operation attach a JWT token in the HTTP header, eg. `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NUb2tlbiI6InRlc3RyZWFkYWItNzZlYWZmMjQxOGJmYjdlMCIsImlhdCI6MTU2NTAzOTczMSwiZXhwIjoxNTY1MDQzMzMxfQ.Y-OFtFJ-gZNJcnZfm9r4S7085Z7YKVPiaQxuMMnraVk` (until the JWT expires)
+198 -129
View File
@@ -16,22 +16,23 @@ It is public (there is no need to add authorization parameters/headers).
Sample GET `/version` client code (to get actual versions):
```javascript
const request = require('request');
request('https://nsapiv3.herokuapp.com/api/v3/version',
(error, response, body) => console.log(body));
const axios = require('axios');
axios.get(`https://nsapiv3.herokuapp.com/api/v3/version`)
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
{
"status": 200,
"result": {
"version": "14.1.0",
"apiVersion": "3.0.2-alpha",
"srvDate": 1609402081548,
"version": "14.2.0",
"apiVersion": "3.0.4-alpha",
"srvDate": 1613056980085,
"storage": {
"storage": "mongodb",
"version": "4.2.11"
"version": "4.4.3"
}
}
}
@@ -46,23 +47,33 @@ It is public (there is no need to add authorization parameters/headers).
Sample GET `/status` client code (to get my actual permissions):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`,
(error, response, body) => console.log(body));
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios.get(`https://nsapiv3.herokuapp.com/api/v3/status`,
{
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
{
"status": 200,
"result": {
"version": "14.1.0",
"apiVersion": "3.0.2-alpha",
"srvDate": 1609427571833,
"version": "14.2.0",
"apiVersion": "3.0.4-alpha",
"srvDate": 1613057148579,
"storage": {
"storage": "mongodb",
"version": "4.2.11"
"version": "4.4.3"
},
"apiPermissions": {
"devicestatus": "crud",
@@ -85,31 +96,41 @@ Sample result:
Sample GET `/entries` client code (to retrieve last 3 BG values):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&limit=3&fields=dateString,sgv,direction`,
(error, response, body) => console.log(body));
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios.get(`https://nsapiv3.herokuapp.com/api/v3/entries?sort$desc=date&limit=3&fields=dateString,sgv,direction`,
{
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
{
"status": 200,
"result": [
{
"dateString": "2019-07-30T02:24:50.434+0200",
"sgv": 115,
"status": 200,
"result": [
{
"dateString": "2021-02-11T15:25:28.928Z",
"sgv": 116,
"direction": "FortyFiveDown"
},
{
"dateString": "2019-07-30T02:19:50.374+0200",
"sgv": 121,
{
"dateString": "2021-02-11T15:20:28.239Z",
"sgv": 124,
"direction": "FortyFiveDown"
},
{
"dateString": "2019-07-30T02:14:50.450+0200",
"sgv": 129,
"direction": "FortyFiveDown"
{
"dateString": "2021-02-11T15:15:28.225Z",
"sgv": 130,
"direction": "Flat"
}
]
}
@@ -123,29 +144,37 @@ Sample result:
Sample POST `/treatments` client code:
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const doc = {
date: 1564591511232, // (new Date()).getTime(),
date: 1613057404186, // (new Date()).getTime(),
app: 'AndroidAPS',
device: 'Samsung XCover 4-861536030196001',
eventType: 'Correction Bolus',
insulin: 0.3
};
request({
method: 'post',
body: doc,
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}`
},
(error, response, body) => console.log(body));
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments`,
{
method: 'post',
data: doc,
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
{
"status": 201,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"lastModified": 1564591511711
"identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975",
"lastModified": 1613057523148
}
```
@@ -157,28 +186,38 @@ Sample result:
Sample GET `/treatments/{identifier}` client code:
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`,
(error, response, body) => console.log(body));
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios.get(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`,
{
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
{
"status": 200,
"result": {
"date": 1564591511232,
"result": {
"date": 1613057404186,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus",
"insulin": 0.3,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"utcOffset": 0,
"created_at": "2019-07-31T16:45:11.232Z",
"srvModified": 1564591627732,
"srvCreated": 1564591511711,
"created_at": "2021-02-11T15:30:04.186Z",
"identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975",
"srvModified": 1613057523148,
"srvCreated": 1613057523148,
"subject": "test-admin"
}
}
@@ -192,23 +231,33 @@ Sample result:
Sample GET `/lastModified` client code (to get latest modification dates):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`,
(error, response, body) => console.log(body));
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios.get(`https://nsapiv3.herokuapp.com/api/v3/lastModified`,
{
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
{
{
"status": 200,
"result": {
"srvDate": 1564591783202,
"collections": {
"devicestatus": 1564591490074,
"entries": 1564591486801,
"profile": 1548524042744,
"treatments": 1564591627732
"result": {
"srvDate": 1613057924021,
"collections": {
"devicestatus": 1613057731281,
"entries": 1613057728148,
"profile": 1580337948416,
"treatments": 1613057523148
}
}
}
@@ -222,29 +271,37 @@ Sample result:
Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
const doc = {
date: 1564591511232,
date: 1613057404186,
app: 'AndroidAPS',
device: 'Samsung XCover 4-861536030196001',
eventType: 'Correction Bolus',
insulin: 0.4
};
request({
method: 'put',
body: doc,
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(body));
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`,
{
method: 'put',
data: doc,
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
{
"status": 200
"status": 200,
"lastModified": 1613058295307
}
```
@@ -256,20 +313,27 @@ Sample result:
Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
const doc = {
insulin: 0.5
};
request({
method: 'patch',
body: doc,
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(body));
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`,
{
method: 'patch',
data: doc,
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
@@ -284,17 +348,25 @@ Sample result:
[DELETE](https://nsapiv3insecure.herokuapp.com/api3-docs/#/generic/delete__collection___identifier_) operation deletes existing document from the collection.
Sample DELETE `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5):
Sample DELETE `/treatments/{identifier}` client code:
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
request({
method: 'delete',
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(body));
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`,
{
method: 'delete',
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
@@ -311,12 +383,22 @@ Sample result:
Sample HISTORY `/treatments/history/{lastModified}` client code:
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const lastModified = 1564521267421;
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}?${auth}`,
(error, response, body) => console.log(response.body));
const axios = require('axios');
const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const lastModified = 1613057520148;
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
.then(res => {
const jwt = res.data.token;
return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}`,
{
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
```
Sample result:
```json
@@ -324,32 +406,19 @@ Sample result:
"status": 200,
"result": [
{
"date": 1564521267421,
"date": 1613057404186,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus",
"insulin": 0.5,
"utcOffset": 0,
"created_at": "2019-07-30T21:14:27.421Z",
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"srvModified": 1564592440416,
"srvCreated": 1564592334853,
"created_at": "2021-02-11T15:30:04.186Z",
"identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975",
"srvModified": 1613058548149,
"srvCreated": 1613057523148,
"subject": "test-admin",
"modifiedBy": "test-admin",
"isValid": false
},
{
"date": 1564592545299,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Snack Bolus",
"carbs": 10,
"identifier": "267c43c2-f629-5191-a542-4f410c69e486",
"utcOffset": 0,
"created_at": "2019-07-31T17:02:25.299Z",
"srvModified": 1564592545781,
"srvCreated": 1564592545781,
"subject": "test-admin"
}
]
}
+2 -6
View File
@@ -103,9 +103,7 @@ function parseFilter (opCtx) {
}
return [
{ field: 'srvModified', operator: operator, value: lastModified.getTime() },
{ field: 'created_at', operator: operator, value: lastModified.toISOString() },
{ field: 'date', operator: operator, value: lastModified.getTime() }
{ field: 'srvModified', operator: operator, value: lastModified.getTime() }
];
}
@@ -116,9 +114,7 @@ function parseFilter (opCtx) {
*/
function prepareSort () {
return {
srvModified: 1,
created_at: 1,
date: 1
srvModified: 1
};
}
+4 -2
View File
@@ -22,8 +22,10 @@ async function patch (opCtx) {
}
await security.demandPermission(opCtx, `api:${col.colName}:update`);
col.parseDate(doc);
// parseDate is not valid for patch operation
// (it is adding new fields)
// col.parseDate(doc);
const identifier = req.params.identifier
, identifyingFilter = col.storage.identifyingFilter(identifier);
+4 -2
View File
@@ -3,7 +3,8 @@
const express = require('express')
, bodyParser = require('body-parser')
, renderer = require('./shared/renderer')
, StorageSocket = require('./storageSocket')
, storageSocket = require('./storageSocket')
, alarmSocket = require('./alarmSocket')
, apiConst = require('./const.json')
, security = require('./security')
, genericSetup = require('./generic/setup')
@@ -108,7 +109,8 @@ function configure (env, ctx) {
opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND, apiConst.MSG.HTTP_404_BAD_OPERATION);
})
ctx.storageSocket = new StorageSocket(app, env, ctx);
ctx.storageSocket = new storageSocket(app, env, ctx);
ctx.alarmSocket = new alarmSocket(app, env, ctx);
return app;
}
+8 -1
View File
@@ -25,7 +25,14 @@ function authenticate (opCtx) {
return resolve({ shiros: [ adminShiro ] });
}
let token = ctx.authorization.extractToken(req);
let token
if (req.header('Authorization')) {
const parts = req.header('Authorization').split(' ');
if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') {
token = parts[1];
}
}
if (!token) {
return reject(
opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN));
+2 -2
View File
@@ -2,7 +2,7 @@
const apiConst = require('../const.json')
, stringTools = require('./stringTools')
, uuidv5 = require('uuid/v5')
, uuid = require('uuid')
, uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-)
;
@@ -103,7 +103,7 @@ function calculateIdentifier (doc) {
key += '_' + doc.eventType;
}
return uuidv5(key, uuidNamespace);
return uuid.v5(key, uuidNamespace);
}
+2 -2
View File
@@ -69,7 +69,7 @@ function updateOne (col, identifier, setFields) {
if (err) {
reject(err);
} else {
resolve({ updated: result.result.nModified });
resolve({ updated: result.modifiedCount });
}
});
});
@@ -91,7 +91,7 @@ function deleteOne (col, identifier) {
if (err) {
reject(err);
} else {
resolve({ deleted: result.result.n });
resolve({ deleted: result.deletedCount });
}
});
});
+3 -3
View File
@@ -2,7 +2,7 @@
const _ = require('lodash')
, checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$")
, ObjectID = require('mongodb').ObjectID
, ObjectID = require('mongodb-legacy').ObjectId
;
@@ -112,7 +112,7 @@ function filterForOne (identifier) {
// fallback to "identifier = _id"
if (checkForHexRegExp.test(identifier)) {
filterOpts.push({ _id: ObjectID(identifier) });
filterOpts.push({ _id: new ObjectID(identifier) });
}
return { $or: filterOpts };
@@ -137,7 +137,7 @@ function identifyingFilter (identifier, doc, dedupFallbackFields) {
// fallback to "identifier = _id" (APIv1)
if (checkForHexRegExp.test(identifier)) {
filterItems.push({ identifier: { $exists: false }, _id: ObjectID(identifier) });
filterItems.push({ identifier: { $exists: false }, _id: new ObjectID(identifier) });
}
}
+6 -2
View File
@@ -3,6 +3,11 @@
const apiConst = require('./const');
const forwarded = require('forwarded-for');
function getRemoteIP (req) {
const address = forwarded(req, req.headers);
return address.ip;
}
/**
* Socket.IO broadcaster of any storage change
*/
@@ -29,8 +34,7 @@ function StorageSocket (app, env, ctx) {
self.namespace = io.of(NAMESPACE);
self.namespace.on('connection', function onConnected (socket) {
const address = forwarded(socket.request, socket.request.headers);
const remoteIP = address.ip;
const remoteIP = getRemoteIP(socket.request);
console.log(LOG + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP);
socket.on('disconnect', function onDisconnect () {
+13 -161
View File
@@ -11,7 +11,7 @@
"name": "AGPL 3",
"url": "https://www.gnu.org/licenses/agpl.txt"
},
"version": "3.0.3"
"version": "3.0.4"
},
"servers": [
{
@@ -49,17 +49,6 @@
"$ref": "#/components/schemas/paramCollection"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
{
"name": "filter_parameters",
"in": "query",
@@ -175,7 +164,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -216,9 +205,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -241,17 +227,6 @@
"schema": {
"$ref": "#/components/schemas/paramCollection"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
@@ -313,7 +288,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -354,9 +329,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -393,17 +365,6 @@
"$ref": "#/components/schemas/paramIdentifier"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
{
"name": "If-Modified-Since",
"in": "header",
@@ -473,7 +434,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -524,9 +485,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -561,17 +519,6 @@
"$ref": "#/components/schemas/paramIdentifier"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
{
"name": "If-Unmodified-Since",
"in": "header",
@@ -632,7 +579,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -693,9 +640,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -730,17 +674,6 @@
"$ref": "#/components/schemas/paramIdentifier"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
{
"name": "permanent",
"in": "query",
@@ -765,7 +698,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -806,9 +739,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -843,17 +773,6 @@
"$ref": "#/components/schemas/paramIdentifier"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
{
"name": "If-Unmodified-Since",
"in": "header",
@@ -899,7 +818,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -960,9 +879,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -989,17 +905,6 @@
"$ref": "#/components/schemas/paramCollection"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
{
"name": "Last-Modified",
"in": "header",
@@ -1087,7 +992,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -1128,9 +1033,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -1169,17 +1071,6 @@
"format": "int64"
}
},
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
@@ -1256,7 +1147,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -1297,9 +1188,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -1346,7 +1234,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -1367,9 +1255,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -1384,19 +1269,6 @@
"summary": "LAST MODIFIED: Retrieves timestamp of the last modification of every collection",
"description": "LAST MODIFIED operation inspects collections separately (in parallel) and for each of them it finds the date of any last modification (insertion, update, deletion).\nNot only `srvModified`, but also `date` and `created_at` fields are inspected (as a fallback to previous API).\n\nThis operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to.",
"operationId": "LAST-MODIFIED",
"parameters": [
{
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful operation returning the timestamps",
@@ -1409,7 +1281,7 @@
}
},
"401": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -1430,9 +1302,6 @@
}
},
"security": [
{
"accessToken": []
},
{
"jwtoken": []
}
@@ -1545,7 +1414,7 @@
},
"subject": {
"type": "string",
"description": "Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT.\n\nNote&#58; this field is immutable by the client (it cannot be updated or patched)",
"description": "Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed JWT.\n\nNote&#58; this field is immutable by the client (it cannot be updated or patched)",
"example": "uploader"
},
"srvModified": {
@@ -2352,7 +2221,7 @@
}
},
"401Unauthorized": {
"description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.",
"description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.",
"content": {
"application/json": {
"schema": {
@@ -2506,17 +2375,6 @@
}
},
"parameters": {
"tokenParam": {
"name": "token",
"in": "query",
"description": "An alternative way of authorization - passing accessToken in a query parameter.\n\nExample&#58;\n\n<pre>token=testadmin-bf2591231bd2c042</pre>",
"required": false,
"style": "form",
"explode": true,
"schema": {
"type": "string"
}
},
"limitParam": {
"name": "limit",
"in": "query",
@@ -2645,12 +2503,6 @@
}
},
"securitySchemes": {
"accessToken": {
"type": "apiKey",
"description": "Add token as query item in the URL or as HTTP header. You can manage access token in `/admin`.\nEach operation requires a specific permission that has to be granted (via security role) to the security subject, which was authenticated by `token` parameter/header or `JWT`. E.g. for creating new `devicestatus` document via API you need `api:devicestatus:create` permission.",
"name": "token",
"in": "query"
},
"jwtoken": {
"type": "http",
"description": "Use this if you know the temporary json webtoken.",
+3 -50
View File
@@ -2,7 +2,7 @@ openapi: 3.0.0
servers:
- url: '/api/v3'
info:
version: 3.0.3
version: 3.0.4
title: Nightscout API
contact:
name: NS development discussion channel
@@ -75,8 +75,6 @@ paths:
schema:
$ref: '#/components/schemas/paramCollection'
- $ref: '#/components/parameters/tokenParam'
######################################################################################
get:
tags:
@@ -113,7 +111,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -158,7 +155,6 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -196,8 +192,6 @@ paths:
schema:
$ref: '#/components/schemas/paramIdentifier'
- $ref: '#/components/parameters/tokenParam'
######################################################################################
get:
tags:
@@ -221,7 +215,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -276,7 +269,6 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -341,7 +333,6 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -382,7 +373,6 @@ paths:
- $ref: '#/components/parameters/permanentParam'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -408,8 +398,6 @@ paths:
schema:
$ref: '#/components/schemas/paramCollection'
- $ref: '#/components/parameters/tokenParam'
get:
tags:
- generic
@@ -439,7 +427,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -475,8 +462,6 @@ paths:
type: integer
format: int64
- $ref: '#/components/parameters/tokenParam'
get:
tags:
- generic
@@ -497,7 +482,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- accessToken: []
- jwtoken: []
responses:
@@ -543,7 +527,6 @@ paths:
This operation requires authorization in contrast with VERSION operation.
security:
- accessToken: []
- jwtoken: []
responses:
@@ -560,9 +543,6 @@ paths:
######################################################################################
/lastModified:
parameters:
- $ref: '#/components/parameters/tokenParam'
get:
tags:
- other
@@ -577,7 +557,6 @@ paths:
This operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to.
security:
- accessToken: []
- jwtoken: []
responses:
@@ -594,22 +573,6 @@ components:
parameters:
tokenParam:
in: query
name: token
schema:
type: string
required: false
description:
An alternative way of authorization - passing accessToken in a query parameter.
Example&#58;
<pre>token=testadmin-bf2591231bd2c042</pre>
limitParam:
in: query
name: limit
@@ -887,7 +850,7 @@ components:
example: 400
401Unauthorized:
description: The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.
description: The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.
content:
application/json:
schema:
@@ -1226,7 +1189,7 @@ components:
subject:
type: string
description:
Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT.
Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed JWT.
Note&#58; this field is immutable by the client (it cannot be updated or patched)
@@ -1750,16 +1713,6 @@ components:
######################################################################################
securitySchemes:
accessToken:
type: apiKey
name: token
in: query
description: >-
Add token as query item in the URL or as HTTP header. You can manage access token in
`/admin`.
Each operation requires a specific permission that has to be granted (via security role) to the security subject, which was authenticated by `token` parameter/header or `JWT`. E.g. for creating new `devicestatus` document via API you need `api:devicestatus:create` permission.
jwtoken:
type: http
scheme: bearer
+1 -1
View File
@@ -298,7 +298,7 @@ function init (env, ctx) {
const token = env.enclave.signJWT({ accessToken: subject.accessToken });
const decoded = env.enclave.verifyJWT(token);
var roles = _.uniq(subject.roles.concat(defaultRoles));
var roles = subject.roles ? _.uniq(subject.roles.concat(defaultRoles)) : defaultRoles;
authorized = {
token
+20 -6
View File
@@ -3,7 +3,7 @@
var _ = require('lodash');
var crypto = require('crypto');
var shiroTrie = require('shiro-trie');
var ObjectID = require('mongodb').ObjectID;
var ObjectID = require('mongodb-legacy').ObjectId;
var find_options = require('../server/query');
@@ -27,14 +27,14 @@ function init (env, ctx) {
if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) {
obj.created_at = (new Date()).toISOString();
}
collection.insert(obj, function (err, doc) {
collection.insertOne(obj, function (err, doc) {
if (err != null && err.message) {
console.log('Data insertion error', err.message);
fn(err.message, null);
return;
}
storage.reload(function loaded() {
fn(null, doc.ops);
fn(null, obj);
});
});
}
@@ -42,7 +42,7 @@ function init (env, ctx) {
}
function list (collection) {
function doList(opts, fn) {
function doList(opts, fn) {
// these functions, find, sort, and limit, are used to
// dynamically configure the request, based on the options we've
// been given
@@ -65,6 +65,8 @@ function init (env, ctx) {
fn(err, entries);
}
console.log('Loading',opts);
// now just stitch them all together
limit.call(collection
.find(query_for(opts))
@@ -77,7 +79,7 @@ function init (env, ctx) {
function remove (collection) {
function doRemove (_id, callback) {
collection.remove({ '_id': new ObjectID(_id) }, function (err) {
collection.deleteOne({ '_id': new ObjectID(_id) }, function (err) {
storage.reload(function loaded() {
callback(err, null);
});
@@ -92,7 +94,7 @@ function init (env, ctx) {
if (!obj.created_at) {
obj.created_at = (new Date()).toISOString();
}
collection.save(obj, function (err) {
collection.insertOne(obj, function (err) {
//id should be added for new docs
storage.reload(function loaded() {
callback(err, obj);
@@ -122,6 +124,11 @@ function init (env, ctx) {
, { name: 'activity', permissions: [ 'api:activity:create' ] }
];
storage.ensureIndexes = function ensureIndexes() {
ctx.store.ensureIndexes(rolesCollection, ['name']);
ctx.store.ensureIndexes(subjectsCollection, ['name']);
}
storage.getSHA1 = function getSHA1 (message) {
var shasum = crypto.createHash('sha1');
shasum.update(message);
@@ -130,8 +137,14 @@ function init (env, ctx) {
storage.reload = function reload (callback) {
console.log('Reloading auth data');
storage.listRoles({sort: {name: 1}}, function listResults (err, results) {
console.log('Roles listed');
if (err) {
console.log('Problem listing roles', err);
return callback && callback(err);
}
@@ -147,6 +160,7 @@ function init (env, ctx) {
storage.listSubjects({sort: {name: 1}}, function listResults (err, results) {
if (err) {
console.log('Problem listing subjects', err);
return callback && callback(err);
}
+5 -7
View File
@@ -1,7 +1,6 @@
'use strict';
var _ = require('lodash');
var moment = require('moment-timezone');
var times = require('../times');
var Storages = require('js-storage');
@@ -46,9 +45,9 @@ function init (client, $) {
}
function setDateAndTime (time) {
time = time || moment();
eventTime.val(time.format('HH:mm'));
eventDate.val(time.format('YYYY-MM-DD'));
time = time || new Date();
eventTime.val(time.getHours() + ":" + time.getMinutes());
eventDate.val(time.toISOString().split('T')[0]);
}
function mergeDateAndTime () {
@@ -125,16 +124,15 @@ function init (client, $) {
boluscalc.calculateInsulin();
maybePrevent(event);
// Nightscout.utils.updateBrushToTime(moment.toDate());
};
boluscalc.eventTimeTypeChange = function eventTimeTypeChange (event) {
if ($('#bc_othertime').is(':checked')) {
$('#bc_eventTimeValue').focus();
$('#bc_retro').css('display', '');
if (mergeDateAndTime() < moment()) {
if (mergeDateAndTime() < Date.now()) {
$('#bc_retro').css('background-color', 'red').text(translate('RETRO MODE'));
} else if (mergeDateAndTime() > moment()) {
} else if (mergeDateAndTime() > Date.now()) {
$('#bc_retro').css('background-color', 'blue').text(translate('IN THE FUTURE'));
} else {
$('#bc_retro').css('display', 'none');
+3 -2
View File
@@ -142,9 +142,10 @@ function init (client, serverSettings, $) {
const id = e.plugin.name + "-" + p.id;
const label = p.label;
if (p.type == 'boolean') {
const html = $(`<dd><input type="checkbox" id="${id}" value="true" /><label for="${id}r">` + translate(label) + `</label></dd>`);
const html = $(`<dd><input type="checkbox" id="${id}" value="true" /><label for="${id}">` + translate(label) + `</label></dd>`);
dl.append(html);
if (storage.get(id) == true) {
const settingsBase = settings.extendedSettings[e.plugin.name];
if (settingsBase[p.id] == true) {
toggleCheckboxes.push(id);
}
}
+10 -4
View File
@@ -1,6 +1,5 @@
'use strict';
var moment = require('moment-timezone');
var _ = require('lodash');
var parse_duration = require('parse-duration'); // https://www.npmjs.com/package/parse-duration
var times = require('../times');
@@ -18,9 +17,9 @@ function init (client, $) {
var eventDate = $('#eventDateValue');
function setDateAndTime (time) {
time = time || moment();
eventTime.val(time.format('HH:mm'));
eventDate.val(time.format('YYYY-MM-DD'));
time = time || client.ctx.moment();
eventTime.val(time.hours() + ":" + time.minutes());
eventDate.val(time.toISOString().split('T')[0]);
}
function mergeDateAndTime () {
@@ -525,6 +524,11 @@ function init (client, $) {
careportal.dateTimeChange = function dateTimeChange (event) {
$('#othertime').prop('checked', true);
// Can't decipher why the following logic was in place
// and it's now bugging out and resetting any date set manually
// so I'm disabling this
/*
var ele = $(this);
var merged = mergeDateAndTime();
@@ -537,6 +541,8 @@ function init (client, $) {
setDateAndTime(merged);
updateTime(ele, merged);
*/
maybePrevent(event);
};
+1 -1
View File
@@ -85,7 +85,7 @@ hashauth.init = function init (client, $) {
client.browserUtils.reload();
}
// clear eveything just in case
// clear everything just in case
hashauth.apisecret = null;
hashauth.apisecrethash = null;
hashauth.authenticated = false;
+53 -9
View File
@@ -9,7 +9,6 @@ var Storages = require('js-storage');
var language = require('../language')();
var sandbox = require('../sandbox')();
var profile = require('../profilefunctions')();
var units = require('../units')();
var levels = require('../levels');
var times = require('../times');
@@ -18,6 +17,8 @@ var receiveDData = require('./receiveddata');
var brushing = false;
var browserSettings;
var moment = window.moment;
var timezones = moment.tz.names();
var client = {};
@@ -67,7 +68,7 @@ client.init = function init (callback) {
}).done(function success (serverSettings) {
if (serverSettings.runtimeState !== 'loaded') {
console.log('Server is still loading data');
$('#loadingMessageText').html('Server is starting and still loading data, retrying load in 5 seconds');
$('#loadingMessageText').html('Nightscout is still starting and should be available within about 15 seconds.');
window.setTimeout(window.Nightscout.client.init, 5000);
return;
}
@@ -152,6 +153,7 @@ client.load = function load (serverSettings, callback) {
var chart
, socket
, alarmSocket
, isInitialData = false
, opacity = { current: 1, DAY: 1, NIGHT: 0.5 }
, clientAlarms = {}
@@ -203,6 +205,7 @@ client.load = function load (serverSettings, callback) {
, extendedSettings: client.settings.extendedSettings
, language: language
, levels: levels
, moment: moment
}).registerClientDefaults();
browserSettings.loadPluginSettings(client);
@@ -210,6 +213,7 @@ client.load = function load (serverSettings, callback) {
client.utils = require('../utils')({
settings: client.settings
, language: language
, moment: moment
});
client.rawbg = client.plugins('rawbg');
@@ -223,6 +227,8 @@ client.load = function load (serverSettings, callback) {
, bus: require('../bus')(client.settings, client.ctx)
, settings: client.settings
, pluginBase: client.plugins.base(majorPills, minorPills, statusPills, bgStatus, client.tooltip, Storages.localStorage)
, moment: moment
, timezones: timezones
};
client.ctx.language = language;
@@ -298,6 +304,8 @@ client.load = function load (serverSettings, callback) {
client.careportal = require('./careportal')(client, $);
client.boluscalc = require('./boluscalc')(client, $);
var profile = require('../profilefunctions')(null, client.ctx);
client.profilefunctions = profile;
client.editMode = false;
@@ -804,7 +812,7 @@ client.load = function load (serverSettings, callback) {
// only emit ack if client invoke by button press
if (isClient && currentNotify) {
socket.emit('ack', currentNotify.level, currentNotify.group, silenceTime);
alarmSocket.emit('ack', currentNotify.level, currentNotify.group, silenceTime);
}
currentNotify = null;
@@ -1033,7 +1041,8 @@ client.load = function load (serverSettings, callback) {
// Client-side code to connect to server and handle incoming data
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* global io */
client.socket = socket = io.connect();
client.socket = socket = io.connect({ transports: ["polling"] });
client.alarmSocket = alarmSocket = io.connect("/alarm", { multiplex: true, transports: ["polling"] });
socket.on('dataUpdate', dataUpdate);
@@ -1120,6 +1129,41 @@ client.load = function load (serverSettings, callback) {
client.authorizeSocket();
});
client.subscribeForAlarms = function subscribeForAlarms () {
var auth_data = {
secret: client.authorized && client.authorized.token ? null : client.hashauth.hash()
, jwtToken: client.authorized && client.authorized.token
};
alarmSocket.emit(
'subscribe'
, auth_data
, function subscribeCallback (data) {
if (!data) {
console.log('Crashed!');
client.crashed();
}
console.log('Subscribed for alarms', data);
if (client.settings.authenticationPromptOnLoad && !data.success) {
client.hashauth.requestAuthentication(function afterRequest () {
client.hashauth.updateSocketAuth();
if (callback) {
callback();
}
});
} else if (callback) {
callback();
}
}
);
}
alarmSocket.on('connect', function() {
client.subscribeForAlarms();
});
function hasRequiredPermission () {
if (client.requiredPermission) {
if (client.hashauth && client.hashauth.isAuthenticated()) {
@@ -1144,7 +1188,7 @@ client.load = function load (serverSettings, callback) {
return client.latestSGV && client.latestSGV.mgdl <= client.settings.thresholds.bgTargetTop;
}
socket.on('notification', function(notify) {
alarmSocket.on('notification', function(notify) {
console.log('notification from server:', notify);
if (notify.timestamp && previousNotifyTimestamp !== notify.timestamp) {
previousNotifyTimestamp = notify.timestamp;
@@ -1154,14 +1198,14 @@ client.load = function load (serverSettings, callback) {
}
});
socket.on('announcement', function(notify) {
alarmSocket.on('announcement', function(notify) {
console.info('announcement received from server');
currentAnnouncement = notify;
currentAnnouncement.received = Date.now();
updateTitle();
});
socket.on('alarm', function(notify) {
alarmSocket.on('alarm', function(notify) {
console.info('alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmHigh) || (isAlarmForLow() && client.settings.alarmLow);
if (enabled) {
@@ -1173,7 +1217,7 @@ client.load = function load (serverSettings, callback) {
chart.update(false);
});
socket.on('urgent_alarm', function(notify) {
alarmSocket.on('urgent_alarm', function(notify) {
console.info('urgent alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmUrgentHigh) || (isAlarmForLow() && client.settings.alarmUrgentLow);
if (enabled) {
@@ -1185,7 +1229,7 @@ client.load = function load (serverSettings, callback) {
chart.update(false);
});
socket.on('clear_alarm', function(notify) {
alarmSocket.on('clear_alarm', function(notify) {
if (alarmInProgress) {
console.log('clearing alarm');
stopAlarm(false, null, notify);
-1
View File
@@ -139,7 +139,6 @@ function init(env, ctx) {
});
console.info('Load Complete:\n\t', counts.join(', '));
done(err, result);
}
+29 -20
View File
@@ -265,33 +265,42 @@ function init () {
ddata.tempbasalTreatments = ddata.processDurations(tempbasalTreatments, false);
// filter temp target
var tempTargetTreatments = ddata.treatments.filter(function filterTargets (tt) {
var tempTargetTreatments = ddata.treatments.filter(function filterTargets (t) {
return t.eventType && t.eventType.indexOf('Temporary Target') > -1;
});
// Clone the treatment before modifying it
let t = _.cloneDeep(tt);
function convertTempTargetTreatmentUnites (_treatments) {
//check for a units being sent
if (t.units) {
if (t.units == 'mmol') {
//convert to mgdl
let treatments = _.cloneDeep(_treatments);
for (let i = 0; i < treatments.length; i++) {
let t = treatments[i];
let converted = false;
// if treatment is in mmol, convert to mg/dl
if (Object.prototype.hasOwnProperty.call(t,'units')) {
if (t.units == 'mmol') {
//convert to mgdl
t.targetTop = t.targetTop * consts.MMOL_TO_MGDL;
t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL;
t.units = 'mg/dl';
converted = true;
}
}
//if we have a temp target thats below 20, assume its mmol and convert to mgdl for safety.
if (!converted && (t.targetTop < 20 || t.targetBottom < 20)) {
t.targetTop = t.targetTop * consts.MMOL_TO_MGDL;
t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL;
t.units = 'mg/dl';
}
}
//if we have a temp target thats below 20, assume its mmol and convert to mgdl for safety.
if (t.targetTop < 20) {
t.targetTop = t.targetTop * consts.MMOL_TO_MGDL;
t.units = 'mg/dl';
}
if (t.targetBottom < 20) {
t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL;
t.units = 'mg/dl';
}
return t.eventType && t.eventType.indexOf('Temporary Target') > -1;
});
if (preserveOrignalTreatments)
tempTargetTreatments = _.cloneDeep(tempTargetTreatments);
return treatments;
}
if (preserveOrignalTreatments) tempTargetTreatments = _.cloneDeep(tempTargetTreatments);
tempTargetTreatments = convertTempTargetTreatmentUnites(tempTargetTreatments);
ddata.tempTargetTreatments = ddata.processDurations(tempTargetTreatments, false);
};
+2
View File
@@ -64,6 +64,8 @@ function configure (app, ctx) {
next( );
});
api.use(ctx.authorization.isPermitted('api:entries:read'),
ctx.authorization.isPermitted('api:treatments:read'));
api.get('/at/:at?', ensure_at, get_ddata, format_result);
return api;
+4 -4
View File
@@ -243,13 +243,13 @@ client.init(function loaded () {
.append($('<img>').attr('title',translate('Edit record')).attr('src',icon_edit).attr('index',i).attr('class','fe_editimg'))
.append($('<img>').attr('title',translate('Delete record')).attr('src',icon_remove).attr('index',i).attr('class','fe_removeimg'))
)
.append($('<span>').addClass('width200px').append(foodlist[i].name))
.append($('<span>').addClass('width200px').text(foodlist[i].name))
.append($('<span>').addClass('width150px').css('text-align','center').append(foodlist[i].portion))
.append($('<span>').addClass('width50px').css('text-align','center').append(foodlist[i].unit))
.append($('<span>').addClass('width50px').css('text-align','center').text(foodlist[i].unit))
.append($('<span>').addClass('width100px').css('text-align','center').append(foodlist[i].carbs))
.append($('<span>').addClass('width100px').css('text-align','center').append(foodlist[i].gi))
.append($('<span>').addClass('width150px').append(foodlist[i].category))
.append($('<span>').addClass('width150px').append(foodlist[i].subcategory))
.append($('<span>').addClass('width150px').text(foodlist[i].category))
.append($('<span>').addClass('width150px').text(foodlist[i].subcategory))
.append($('<span>').addClass('width100px').append(foodlist[i].fat))
.append($('<span>').addClass('width100px').append(foodlist[i].protein))
.append($('<span>').addClass('width100px').append(foodlist[i].energy))
+1
View File
@@ -39,6 +39,7 @@ function init (fs) {
, { code: 'sl', file: 'sl_SL', language: 'Slovenščina', speechCode: 'sl-SL' }
, { code: 'sv', file: 'sv_SE', language: 'Svenska', speechCode: 'sv-SE' }
, { code: 'tr', file: 'tr_TR', language: 'Türkçe', speechCode: 'tr-TR' }
, { code: 'uk', file: 'uk_UA', language: 'українська', speechCode: 'uk-UA' }
, { code: 'zh_cn', file: 'zh_CN', language: '中文(简体)', speechCode: 'cmn-Hans-CN' }
// , { code: 'zh_tw', file: 'zh_TW', language: '中文(繁體)', speechCode: 'cmn-Hant-TW' }
];
+1 -1
View File
@@ -2,7 +2,6 @@
var _ = require('lodash');
var times = require('../times');
var moment = require('moment');
var BG_REF = 140; //Central tendency
var BG_MIN = 36; //Not 39, but why?
@@ -17,6 +16,7 @@ var AR2_COLOR = 'cyan';
function init (ctx) {
var translate = ctx.language.translate;
var moment = ctx.moment;
var ar2 = {
name: 'ar2'
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict';
var times = require('../times');
var moment = require('moment');
var consts = require('../constants');
var _ = require('lodash');
function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
+1 -1
View File
@@ -1,9 +1,9 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var levels = ctx.levels;
+1 -1
View File
@@ -1,7 +1,6 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
var times = require('../times');
var offset = times.mins(2.5).msecs;
@@ -9,6 +8,7 @@ var bucketFields = ['index', 'fromMills', 'toMills'];
function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var utils = require('../utils')(ctx);
+1 -1
View File
@@ -1,9 +1,9 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var levels = ctx.levels;
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict';
var _ = require('lodash')
, moment = require('moment')
, times = require('../times');
function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var iob = require('./iob')(ctx);
+1 -1
View File
@@ -1,9 +1,9 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var levels = ctx.levels;
+3 -3
View File
@@ -1,10 +1,10 @@
'use strict';
var _ = require('lodash')
, moment = require('moment')
, times = require('../times');
const _ = require('lodash')
const times = require('../times');
function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var utils = require('../utils')(ctx);
+2 -2
View File
@@ -1,12 +1,13 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
var times = require('../times');
// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'freq', 'rssi']; Unused variable
function init (ctx) {
var moment = ctx.moment;
var utils = require('../utils')(ctx);
var translate = ctx.language.translate;
var levels = ctx.levels;
@@ -231,7 +232,6 @@ function init (ctx) {
, split: false
, targets: false
, reasons: reasonconf
, otp: true
, submitHook: postLoopNotification
},
{
+1 -1
View File
@@ -1,13 +1,13 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
var times = require('../times');
var consts = require('../constants');
// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'meal-assist', 'freq', 'rssi']; Unused variable
function init (ctx) {
var moment = ctx.moment;
var utils = require('../utils')(ctx);
var openaps = {
name: 'openaps'
+1 -1
View File
@@ -1,12 +1,12 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
var times = require('../times');
var ALL_STATUS_FIELDS = ['reservoir', 'battery', 'clock', 'status', 'device'];
function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var timeago = require('./timeago')(ctx);
var openaps = require('./openaps')(ctx);
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
var times = require('../times');
function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate;
var levels = ctx.levels;
+5
View File
@@ -123,6 +123,11 @@ function init(ctx) {
message = '...';
}
if (!eventType && lastTreatment.carbs && lastTreatment.insulin) eventType = "Meal Bolus";
if (!eventType && lastTreatment.carbs) eventType = "Carb Correction";
if (!eventType && lastTreatment.insulin) eventType = "Correcton Bolus";
if (!eventType) eventType = "Note";
const hash = crypto.createHash('sha1');
const info = JSON.stringify({ eventType, timestamp});
hash.update(info);
+2 -1
View File
@@ -72,6 +72,7 @@ function init(ctx) {
var battery = uploaderStatus.battery;
var voltage = uploaderStatus.batteryVoltage;
var charging = status.isCharging ? status.isCharging : false;
var voltageDisplay;
if (voltage) {
@@ -93,7 +94,7 @@ function init(ctx) {
uploaderStatus.voltageDisplay = voltageDisplay;
}
uploaderStatus.display = battery ? battery + '%' : voltageDisplay;
uploaderStatus.display = (battery ? battery + '%' : voltageDisplay) + (charging ? "⚡" : "");
if (battery >= 95) {
uploaderStatus.level = 100;
+2 -1
View File
@@ -1,9 +1,10 @@
'use strict';
var moment = require('moment');
var _each = require('lodash/each');
function init(env, ctx) {
var moment = ctx.moment;
function virtAsstBase() {
return virtAsstBase;
}
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict';
var _ = require('lodash');
var moment = require('moment');
var times = require('../times');
function init(ctx) {
var moment = ctx.moment;
var levels = ctx.levels;
var utils = require('../utils')(ctx);
var firstPrefs = true;
+32 -10
View File
@@ -4,7 +4,6 @@ var init = function init () {
//for the tests window isn't the global object
var $ = window.$;
var _ = window._;
var moment = window.moment;
var Nightscout = window.Nightscout;
var client = Nightscout.client;
@@ -157,7 +156,7 @@ var init = function init () {
// Load timezones
timezoneInput.empty();
moment.tz.names().forEach(function addTz(tz) {
client.ctx.timezones.forEach(function addTz(tz) {
timezoneInput.append('<option value="' + tz + '">' + tz + '</option>');
});
@@ -198,8 +197,8 @@ var init = function init () {
}
databaseRecords.val(currentrecord);
timeInput.val(moment(mongorecords[currentrecord].startDate).format('HH:mm'));
dateInput.val(moment(mongorecords[currentrecord].startDate).format('YYYY-MM-DD'));
timeInput.val(client.ctx.moment(mongorecords[currentrecord].startDate).format('HH:mm'));
dateInput.val(client.ctx.moment(mongorecords[currentrecord].startDate).format('YYYY-MM-DD'));
initProfile();
}
@@ -313,11 +312,10 @@ var init = function init () {
profileSubmit();
}
GUIToObject();
mongorecords.push(_.cloneDeep(mongorecords[currentrecord]));
mongorecords.push(_.omit(mongorecords[currentrecord], ['_id', 'srvModified', 'srvCreated', 'identifier', 'mills']));
currentrecord = mongorecords.length - 1;
mongorecords[currentrecord].startDate = new Date().toISOString();
currentprofile = mongorecords[currentrecord].defaultProfile;
delete mongorecords[currentrecord]._id;
initRecord();
dirty = true;
@@ -390,6 +388,7 @@ var init = function init () {
newname += '1';
}
record.store[newname] = _.cloneDeep(record.store[currentprofile]);
currentprofile = newname;
dirty = true;
@@ -565,7 +564,20 @@ var init = function init () {
$('#pe_delay_high').val(c_profile.delay_high);
$('#pe_delay_medium').val(c_profile.delay_medium);
$('#pe_delay_low').val(c_profile.delay_low);
timezoneInput.val(c_profile.timezone);
// find the right zone regardless of string case
var foundCase = c_profile.timezone;
if (foundCase != "") {
var lcZone = c_profile.timezone.toLowerCase();
client.ctx.timezones.forEach(function testCase(tz) {
if (tz.toLowerCase() == lcZone) foundCase = tz;
});
}
timezoneInput.val(foundCase);
var index;
[ { prefix:'pe_basal', array:'basal' },
@@ -602,7 +614,15 @@ var init = function init () {
c_profile.delay_high = parseInt($('#pe_delay_high').val());
c_profile.delay_medium = parseInt($('#pe_delay_medium').val());
c_profile.delay_low = parseInt($('#pe_delay_low').val());
c_profile.timezone = timezoneInput.val();
// If the zone in the profile matches the editor profile
// but case is different, preserve case
var zone = timezoneInput.val();
if (c_profile.timezone.toLowerCase() == timezoneInput.val().toLowerCase()) zone = c_profile.timezone;
c_profile.timezone = zone;
var index;
[ { prefix:'pe_basal', array:'basal' },
@@ -635,11 +655,11 @@ var init = function init () {
}
function toTimeString(minfrommidnight) {
return moment.utc().startOf('day').add(minfrommidnight,'minutes').format('HH:mm'); // using utc to avoid daylight saving offset
return client.ctx.moment.utc().startOf('day').add(minfrommidnight,'minutes').format('HH:mm'); // using utc to avoid daylight saving offset
}
function toDisplayTime (minfrommidnight) {
var time = moment.utc().startOf('day').add(minfrommidnight,'minutes'); // using utc to avoid daylight saving offset
var time = client.ctx.moment.utc().startOf('day').add(minfrommidnight,'minutes'); // using utc to avoid daylight saving offset
return client.settings.timeFormat === 24 ? time.format('HH:mm') : time.format('h:mm A');
}
@@ -652,6 +672,8 @@ var init = function init () {
profileChange(event);
var record = mongorecords[currentrecord];
record.startDate = new Date(client.utils.mergeInputTime(timeInput.val(), dateInput.val())).toISOString( );
record.created_at = new Date().toISOString( );
record.srvModified = new Date().getTime(); // remove when switching to v3 API
var adjustedRecord = _.cloneDeep(record);
+7 -3
View File
@@ -1,14 +1,15 @@
'use strict';
var _ = require('lodash');
var moment = require('moment-timezone');
var c = require('memory-cache');
var times = require('./times');
var cacheTTL = 5000;
var prevBasalTreatment = null;
function init (profileData) {
function init (profileData, ctx) {
var moment = ctx.moment;
var cache = new c.Cache();
var profile = {};
@@ -174,7 +175,10 @@ function init (profileData) {
};
profile.getTimezone = function getTimezone (spec_profile) {
return profile.getCurrentProfile(null, spec_profile)['timezone'];
let rVal = profile.getCurrentProfile(null, spec_profile)['timezone'];
// Work around Loop uploading non-ISO compliant time zone string
if (rVal) rVal.replace('ETC','Etc');
return rVal;
};
profile.hasData = function hasData () {
+6 -3
View File
@@ -4,7 +4,6 @@ var init = function init () {
//for the tests window isn't the global object
var $ = window.$;
var _ = window._;
var moment = window.moment;
var Nightscout = window.Nightscout;
var client = Nightscout.client;
var report_plugins_preinit = Nightscout.report_plugins_preinit;
@@ -12,6 +11,8 @@ var init = function init () {
client.init(function loaded () {
var moment = client.ctx.moment;
report_plugins = report_plugins_preinit(client.ctx);
Nightscout.report_plugins = report_plugins;
@@ -257,9 +258,11 @@ var init = function init () {
function datefilter () {
if ($('#rp_enabledate').is(':checked')) {
matchesneeded++;
var from = moment.tz($('#rp_from').val().replace(/\//g, '-') + 'T00:00:00', zone);
var to = moment.tz($('#rp_to').val().replace(/\//g, '-') + 'T23:59:59', zone);
var from = moment.tz(moment($('#rp_from').val()).startOf('day'), zone).startOf('day');
var to = moment.tz(moment($('#rp_to').val()).endOf('day'), zone).endOf('day');
timerange = '&find[created_at][$gte]=' + from.toISOString() + '&find[created_at][$lt]=' + to.toISOString();
console.log("FROM", from.format( ), "TO", to.format( ), 'timerange', timerange);
//console.log($('#rp_from').val(),$('#rp_to').val(),zone,timerange);
while (from <= to) {
if (daystoshow[from.format('YYYY-MM-DD')]) {
+5 -3
View File
@@ -96,7 +96,9 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
var fatSum = 0;
daytoday.prepareHtml(sorteddaystoshow);
console.log('DAY2DAY', 'sorteddaystoshow', sorteddaystoshow);
sorteddaystoshow.forEach(function eachDay (day) {
drawChart(day, datastorage[day], options);
});
@@ -168,7 +170,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
// create svg and g to contain the chart contents
charts = d3.select('#daytodaychart-' + day).html(
'<b>' +
report_plugins.utils.localeDate(day) +
report_plugins.utils.localeDate(moment(day)) +
'</b><br>'
).append('svg');
@@ -432,8 +434,8 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
contextCircles.exit()
.remove();
var to = moment(day).add(1, 'days');
var from = moment(day);
var from = moment.tz(moment(day), profile.getTimezone( )).startOf('day');
var to = moment(from.clone( )).add(1, 'days');
var iobpolyline = ''
, cobpolyline = '';
+1 -1
View File
@@ -55,7 +55,7 @@ function init () {
sbx.language = ctx.language;
sbx.translate = ctx.language.translate;
var profile = require('./profilefunctions')();
var profile = require('./profilefunctions')(null, ctx);
//Plugins will expect the right profile based on time
profile.loadData(_.cloneDeep(ctx.ddata.profiles));
profile.updateTreatments(ctx.ddata.profileTreatments, ctx.ddata.tempbasalTreatments, ctx.ddata.combobolusTreatments);
+2 -2
View File
@@ -4,7 +4,7 @@ var find_options = require('./query');
function storage (env, ctx) {
var ObjectID = require('mongodb').ObjectID;
var ObjectID = require('mongodb-legacy').ObjectId;
function create (obj, fn) {
obj.created_at = (new Date( )).toISOString( );
@@ -62,7 +62,7 @@ function storage (env, ctx) {
function remove (_id, fn) {
var objId = new ObjectID(_id);
return api( ).remove({ '_id': objId }, fn);
return api( ).deleteOne({ '_id': objId }, fn);
}
function api ( ) {
+4 -13
View File
@@ -184,9 +184,8 @@ function create (env, ctx) {
///////////////////////////////////////////////////
const apiRoot = require('../api/root')(env, ctx);
var api = require('../api/')(env, ctx);
var api2 = require('../api2/')(env,ctx, api);
var api3 = require('../api3/')(env, ctx);
var ddata = require('../data/endpoints')(env, ctx);
var notificationsV2 = require('../api/notifications-v2')(app, ctx);
app.use(compression({
filter: function shouldCompress (req, res) {
@@ -245,16 +244,8 @@ function create (env, ctx) {
app.use("/clock", clockviews);
app.use('/api', apiRoot);
app.use('/api/v1', api);
app.use('/api/v2', api);
app.use('/api/v2/properties', ctx.properties);
app.use('/api/v2/authorization', ctx.authorization.endpoints);
app.use('/api/v2/ddata', ddata);
app.use('/api/v2/notifications', notificationsV2);
app.use('/api/v2', api2);
app.use('/api/v3', api3);
// pebble data
@@ -320,7 +311,7 @@ function create (env, ctx) {
}
// Production bundling
const tmpFiles = express.static(resolvePath('/tmp/public'), {
const tmpFiles = express.static(resolvePath('/node_modules/.cache/_ns_cache/public'), {
maxAge: maxAge
});
@@ -343,7 +334,7 @@ function create (env, ctx) {
, coffee_match: /coffeescript/
, json_match: /json/
, cssmin: myCssmin
, cache: resolvePath('/tmp/public')
, cache: resolvePath('/node_modules/.cache/_ns_cache/public')
, onerror: undefined
, }));
+53 -38
View File
@@ -1,14 +1,18 @@
'use strict';
const _ = require('lodash');
const UPDATE_THROTTLE = 5000;
const UPDATE_THROTTLE = 15000;
function boot (env, language) {
function startBoot(ctx, next) {
console.log('Executing startBoot');
console.log('++++++++++++++++++++++++++++++');
console.log('Nightscout Executing startBoot');
console.log('++++++++++++++++++++++++++++++');
ctx.bootErrors = [ ];
ctx.moment = require('moment-timezone');
ctx.runtimeState = 'booting';
ctx.settings = env.settings;
ctx.bus = require('../bus')(env.settings, ctx);
@@ -23,7 +27,8 @@ function boot (env, language) {
//////////////////////////////////////////////////
// Check Node version.
// Latest Node 10 to 14 LTS are recommended and supported.
// Latest Node LTS releases are recommended and supported.
// Current Node releases MAY work, but are not recommended. Will be tested in CI
// Older Node versions or Node versions with known security issues will not work.
///////////////////////////////////////////////////
function checkNodeVersion (ctx, next) {
@@ -34,10 +39,10 @@ function boot (env, language) {
var nodeVersion = process.version;
const isLTS = process.release.lts ? true : false;
if (isLTS && (semver.satisfies(nodeVersion, '^16.0.0') || semver.satisfies(nodeVersion, '^14.0.0') || semver.satisfies(nodeVersion, '^12.0.0') || semver.satisfies(nodeVersion, '^10.0.0'))) {
//Latest Node 10 LTS and Node 12 LTS are recommended and supported.
//Require at least Node 8 LTS and Node 10 LTS without known security issues
if (isLTS || (semver.satisfies(nodeVersion, '^20.0.0') || semver.satisfies(nodeVersion, '^18.0.0') || semver.satisfies(nodeVersion, '^16.0.0') || semver.satisfies(nodeVersion, '^14.0.0'))) {
//Latest Node 14 LTS and Node 16 LTS are recommended and supported.
//Require at least Node 14 without known security issues
console.debug('Node LTS version ' + nodeVersion + ' is supported');
next();
return;
@@ -71,29 +76,35 @@ function boot (env, language) {
var configURL = env.IMPORT_CONFIG || null;
var url = require('url');
var href = null;
try {
href = url.parse(configURL).href;
} catch (e) {
console.error('Parsing config URL from IMPORT_CONFIG failed');
if (configURL) {
try {
href = url.parse(configURL).href;
} catch (e) {
console.error('Parsing config URL from IMPORT_CONFIG failed');
}
}
if(configURL && href) {
var request = require('request');
var axios_default = { headers: { 'Accept': 'application/json' } };
var axios = require('axios').create(axios_default);
console.log('Getting settings from', href);
request.get({url: href, json: true}, function (err, resp, body) {
if (err) {
console.log('Attempt to fetch config', href, 'failed.');
console.error(err);
throw err;
} else {
var settings = body.settings || body;
console.log('extending settings with', settings);
_.merge(env.settings, settings);
if (body.extendedSettings) {
console.log('extending extendedSettings with', body.extendedSettings);
_.merge(env.extendedSettings, body.extendedSettings);
}
return axios.get(href).then(function (resp) {
var body = resp.data;
var settings = body.settings || body;
console.log('extending settings with', settings);
_.merge(env.settings, settings);
if (body.extendedSettings) {
console.log('extending extendedSettings with', body.extendedSettings);
_.merge(env.extendedSettings, body.extendedSettings);
}
next( );
}).catch(function (err) {
var synopsis = ['Attempt to fetch config', href, 'failed.'];
console.log('Attempt to fetch config', href, 'failed.', err.response);
ctx.bootErrors.push({desc: synopsis.join(' '), err});
next( );
});
} else {
next( );
@@ -139,16 +150,6 @@ function boot (env, language) {
}
try {
if (_.startsWith(env.storageURI, 'openaps://')) {
require('../storage/openaps-storage')(env, function ready (err, store) {
if (err) {
throw err;
}
ctx.store = store;
console.log('OpenAPS Storage system ready');
next();
});
} else {
//TODO assume mongo for now, when there are more storage options add a lookup
require('../storage/mongo-storage')(env, function ready(err, store) {
// FIXME, error is always null, if there is an error, the index.js will throw an exception
@@ -161,7 +162,6 @@ function boot (env, language) {
ctx.store = store;
next();
});
}
} catch (err) {
console.info('ERROR CONNECTING TO MONGO', err);
ctx.bootErrors = ctx.bootErrors || [ ];
@@ -179,6 +179,7 @@ function boot (env, language) {
}
ctx.authorization = require('../authorization')(env, ctx);
ctx.authorization.storage.ensureIndexes();
ctx.authorization.storage.reload(function loaded (err) {
if (err) {
ctx.bootErrors = ctx.bootErrors || [ ];
@@ -206,6 +207,7 @@ function boot (env, language) {
settings: env.settings
, language: ctx.language
, levels: ctx.levels
, moment: ctx.moment
}).registerServerDefaults();
ctx.wares = require('../middleware/')(env);
@@ -222,7 +224,7 @@ function boot (env, language) {
ctx.profile = require('./profile')(env.profile_collection, ctx);
ctx.food = require('./food')(env, ctx);
ctx.pebble = require('./pebble')(env, ctx);
ctx.properties = require('../api/properties')(env, ctx);
ctx.properties = require('../api2/properties')(env, ctx);
ctx.ddata = require('../data/ddata')();
ctx.cache = require('./cache')(env,ctx);
ctx.dataloader = require('../data/dataloader')(env, ctx);
@@ -284,6 +286,7 @@ function boot (env, language) {
ctx.bus.on('data-received', function forceReloadData ( ) {
console.info('got data-received event, requesting reload');
ctx.bus.emit('data-loaded'); // Since we update local sandbox instantly, process data-loaded right away in case this gets debounced
updateData();
});
@@ -294,7 +297,8 @@ function boot (env, language) {
ctx.notifications.initRequests();
ctx.plugins.checkNotifications(sbx);
ctx.notifications.process(sbx);
ctx.bus.emit('data-processed');
ctx.sbx = sbx;
ctx.bus.emit('data-processed', sbx);
});
ctx.bus.on('data-processed', function processed ( ) {
@@ -306,6 +310,13 @@ function boot (env, language) {
next( );
}
function setupConnect (ctx, next) {
console.log('Executing setupConnect');
ctx.nightscoutConnect = require('nightscout-connect')(env, ctx)
// ctx.nightscoutConnect.
return next( );
}
function setupBridge (ctx, next) {
console.log('Executing setupBridge');
@@ -317,6 +328,7 @@ function boot (env, language) {
ctx.bridge = require('../plugins/bridge')(env, ctx.bus);
if (ctx.bridge) {
ctx.bridge.startEngine(ctx.entries);
console.log("DEPRECATION WARNING", "PLEASE CONSIDER nightscout-connect instead.");
}
next( );
}
@@ -332,6 +344,7 @@ function boot (env, language) {
ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus, ctx.bus);
if (ctx.mmconnect) {
ctx.mmconnect.run();
console.log("DEPRECATION WARNING", "PLEASE CONSIDER nightscout-connect instead.");
}
next( );
}
@@ -343,6 +356,7 @@ function boot (env, language) {
if (hasBootErrors(ctx)) {
return next();
}
ctx.bus.emit('finishBoot');
ctx.runtimeState = 'booted';
ctx.bus.uptime( );
@@ -361,6 +375,7 @@ function boot (env, language) {
.acquire(setupInternals)
.acquire(ensureIndexes)
.acquire(setupListeners)
.acquire(setupConnect)
.acquire(setupBridge)
.acquire(setupMMConnect)
.acquire(finishBoot);
+11 -3
View File
@@ -29,23 +29,31 @@ function cache (env, ctx) {
, entries: constants.TWO_DAYS
};
function getObjectAge(object) {
let age = object.mills || object.date;
if (isNaN(age) && object.created_at) age = Date.parse(object.created_at).valueOf();
return age;
}
function mergeCacheArrays (oldData, newData, retentionPeriod) {
const ageLimit = Date.now() - retentionPeriod;
var filteredOld = filterForAge(oldData, ageLimit);
var filteredOld = filterForAge(oldData, ageLimit);
var filteredNew = filterForAge(newData, ageLimit);
const merged = ctx.ddata.idMergePreferNew(filteredOld, filteredNew);
return _.sortBy(merged, function(item) {
return -item.mills;
const age = getObjectAge(item);
return -age;
});
function filterForAge(data, ageLimit) {
return _.filter(data, function hasId(object) {
const hasId = !_.isEmpty(object._id);
const isFresh = object.mills >= ageLimit;
const age = getObjectAge(object);
const isFresh = age >= ageLimit;
return isFresh && hasId;
});
}
+13 -6
View File
@@ -24,16 +24,16 @@ function storage (collection, ctx) {
obj.utcOffset = d.utcOffset();
api().insertOne(obj, function(err, results) {
if (err !== null && err.message) {
if (err) {
console.log('Error inserting the device status object', err.message);
errorOccurred = true;
fn(err.message, null);
return;
}
if (!err) {
if (!obj._id) obj._id = results.insertedIds[0]._id;
if (results) {
if (!obj._id) obj._id = results.insertedId;
r.push(obj);
ctx.bus.emit('data-update', {
@@ -47,6 +47,11 @@ function storage (collection, ctx) {
fn(null, r);
ctx.bus.emit('data-received');
}
} else {
console.log('Error inserting the device status object', err.message);
errorOccurred = true;
fn(err.message, null);
return;
}
});
};
@@ -100,17 +105,19 @@ function storage (collection, ctx) {
function removed (err, stat) {
console.log('removed', err, stat);
ctx.bus.emit('data-update', {
type: 'devicestatus'
, op: 'remove'
, count: stat.result.n
, count: stat.deletedCount
, changes: opts.find._id
});
fn(err, stat);
}
return api().remove(
return api().deleteMany(
query_for(opts), removed);
}
+4 -4
View File
@@ -19,7 +19,7 @@ const init = function init () {
let apiKeySet = false;
function readKey (filename) {
let filePath = path.resolve(__dirname + '/../../tmp/' + filename);
let filePath = path.resolve(__dirname + '/../../node_modules/.cache/_ns_cache/' + filename);
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath).toString().trim();
}
@@ -32,7 +32,7 @@ const init = function init () {
function genHash(data, algorihtm) {
const hash = crypto.createHash(algorihtm);
data = hash.update(data, 'utf-8');
return data.digest('hex');
return data.digest('hex').toLowerCase();
}
enclave.setApiKey = function setApiKey (keyValue) {
@@ -48,7 +48,7 @@ const init = function init () {
}
enclave.isApiKey = function isApiKey (keyValue) {
return keyValue == secrets[apiKeySHA1] || keyValue == secrets[apiKeySHA512];
return keyValue.toLowerCase() == secrets[apiKeySHA1] || keyValue == secrets[apiKeySHA512];
}
enclave.setJWTKey = function setJWTKey (keyValue) {
@@ -72,7 +72,7 @@ const init = function init () {
var shasum = crypto.createHash('sha1');
shasum.update(secrets[apiKeySHA1]);
shasum.update(id);
return shasum.digest('hex');
return shasum.digest('hex').toLowerCase();
}
return enclave;
+13 -16
View File
@@ -2,7 +2,7 @@
var es = require('event-stream');
var find_options = require('./query');
var ObjectID = require('mongodb').ObjectID;
var ObjectId = require('mongodb-legacy').ObjectId;
var moment = require('moment');
/**********\
@@ -46,12 +46,12 @@ function storage (env, ctx) {
}
function remove (opts, fn) {
api().remove(query_for(opts), function(err, stat) {
api().deleteMany(query_for(opts), function(err, stat) {
ctx.bus.emit('data-update', {
type: 'entries'
, op: 'remove'
, count: stat.result.n
, count: stat.deletedCount
, changes: opts.find._id
});
@@ -99,20 +99,23 @@ function storage (env, ctx) {
// Normalize dates to be in UTC, store offset in utcOffset
var _sysTime = moment(doc.dateString).isValid() ? moment.parseZone(doc.dateString) : moment(doc.date);
_sysTime = _sysTime.isValid() ? _sysTime : moment();
var _sysTime;
if (doc.dateString) { _sysTime = moment.parseZone(doc.dateString); }
if (!_sysTime && doc.date) { _sysTime = moment(doc.date); }
if (!_sysTime) _sysTime = moment();
doc.utcOffset = _sysTime.utcOffset();
doc.sysTime = _sysTime.toISOString();
if (doc.dateString) doc.dateString = doc.sysTime;
var query = (doc.sysTime && doc.type) ? { sysTime: doc.sysTime, type: doc.type } : doc;
api().update(query, doc, { upsert: true }, function(err, updateResults) {
api().replaceOne(query, doc, { upsert: true }, function(err, updateResults) {
firstErr = firstErr || err;
if (!err) {
if (updateResults.result.upserted) {
doc._id = updateResults.result.upserted[0]._id
if (updateResults) {
if (updateResults.upsertedCount == 1) {
doc._id = updateResults.upsertedId
}
ctx.bus.emit('data-update', {
@@ -132,7 +135,7 @@ function storage (env, ctx) {
}
function getEntry (id, fn) {
api().findOne({ _id: ObjectID(id) }, function(err, entry) {
api().findOne({ "_id": new ObjectId(id) }, function(err, entry) {
if (err) {
fn(err);
} else {
@@ -163,17 +166,11 @@ function storage (env, ctx) {
api.aggregate = require('./aggregate')({}, api);
api.indexedFields = [
'date'
, 'type'
, 'sgv'
, 'mbg'
, 'sysTime'
, 'dateString'
, { 'type': 1, 'date': -1, 'dateString': 1 }
];
return api;
+2 -2
View File
@@ -1,7 +1,7 @@
'use strict';
function storage (env, ctx) {
var ObjectID = require('mongodb').ObjectID;
var ObjectID = require('mongodb-legacy').ObjectId;
function create (obj, fn) {
obj.created_at = (new Date( )).toISOString( );
@@ -42,7 +42,7 @@ function storage (env, ctx) {
function remove (_id, fn) {
var objId = new ObjectID(_id);
return api( ).remove({ '_id': objId }, fn);
return api( ).deleteOne({ '_id': objId }, fn);
}
+2 -1
View File
@@ -1,6 +1,6 @@
//'use strict';
const apn = require('apn');
const apn = require('@parse/node-apn');
function init (env, ctx) {
@@ -128,6 +128,7 @@ function init (env, ctx) {
notification.topic = loopSettings.bundleIdentifier;
notification.contentAvailable = 1;
notification.payload = payload;
notification.interruptionLevel = "time-sensitive"
provider.send(notification, [loopSettings.deviceToken]).then( (response) => {
if (response.sent && response.sent.length > 0) {
+2 -2
View File
@@ -4,7 +4,7 @@ var find_options = require('./query');
var consts = require('../constants');
function storage (collection, ctx) {
var ObjectID = require('mongodb').ObjectID;
var ObjectID = require('mongodb-legacy').ObjectId;
function create (obj, fn) {
obj.created_at = (new Date( )).toISOString( );
@@ -79,7 +79,7 @@ function storage (collection, ctx) {
function remove (_id, fn) {
var objId = new ObjectID(_id);
api( ).remove({ '_id': objId }, fn);
api( ).deleteOne({ '_id': objId }, fn);
ctx.bus.emit('data-received');
}
+1 -1
View File
@@ -1,7 +1,7 @@
'use strict';
const traverse = require('traverse');
const ObjectID = require('mongodb').ObjectID;
const ObjectID = require('mongodb-legacy').ObjectId;
const moment = require('moment');
const TWO_DAYS = 172800000;
-8
View File
@@ -70,14 +70,6 @@ require('./bootevent')(env, language).boot(function booted (ctx) {
///////////////////////////////////////////////////
var websocket = require('./websocket')(env, ctx, server);
ctx.bus.on('data-processed', function() {
websocket.update();
});
ctx.bus.on('notification', function(notify) {
websocket.emitNotification(notify);
});
//after startup if there are no alarms send all clear
let sendStartupAllClearTimer = setTimeout(function sendStartupAllClear () {
var alarm = ctx.notifications.findHighestAlarm();
+12 -12
View File
@@ -6,7 +6,7 @@ var moment = require('moment');
var find_options = require('./query');
function storage (env, ctx) {
var ObjectID = require('mongodb').ObjectID;
var ObjectID = require('mongodb-legacy').ObjectId;
function create (objOrArray, fn) {
@@ -46,18 +46,18 @@ function storage (env, ctx) {
, eventType: obj.eventType
};
api( ).update(query, obj, {upsert: true}, function complete (err, updateResults) {
api( ).replaceOne(query, obj, {upsert: true}, function complete (err, updateResults) {
if (err) console.error('Problem upserting treatment', err);
if (!err) {
if (updateResults.result.upserted) {
obj._id = updateResults.result.upserted[0]._id
if (updateResults) {
if (updateResults.upsertedCount == 1) {
obj._id = updateResults.upsertedId
}
}
// TODO document this feature
if (!err && obj.preBolus) {
if (updateResults && obj.preBolus) {
//create a new object to insert copying only the needed fields
var pbTreat = {
created_at: (new Date(new Date(results.created_at).getTime() + (obj.preBolus * 60000))).toISOString(),
@@ -70,11 +70,11 @@ function storage (env, ctx) {
}
query.created_at = pbTreat.created_at;
api( ).update(query, pbTreat, {upsert: true}, function pbComplete (err, updateResults) {
api( ).replaceOne(query, pbTreat, {upsert: true}, function pbComplete (err, updateResults) {
if (!err) {
if (updateResults.result.upserted) {
pbTreat._id = updateResults.result.upserted[0]._id
if (updateResults) {
if (updateResults.upsertedCount == 1) {
pbTreat._id = updateResults.upsertedId
}
}
@@ -122,14 +122,14 @@ function storage (env, ctx) {
}
function remove (opts, fn) {
return api( ).remove(query_for(opts), function (err, stat) {
return api( ).deleteMany(query_for(opts), {}, function (err, stat) {
//TODO: this is triggering a read from Mongo, we can do better
//console.log('Treatment removed', opts); // , stat);
ctx.bus.emit('data-update', {
type: 'treatments',
op: 'remove',
count: stat.result.n,
count: stat.deletedCount,
changes: opts.find._id
});
+33 -52
View File
@@ -2,17 +2,20 @@
var times = require('../times');
var calcData = require('../data/calcdelta');
var ObjectID = require('mongodb').ObjectID;
var ObjectID = require('mongodb-legacy').ObjectId;
const forwarded = require('forwarded-for');
function getRemoteIP (req) {
const address = forwarded(req, req.headers);
return address.ip;
}
function init (env, ctx, server) {
function websocket () {
return websocket;
}
var levels = ctx.levels;
//var log_yellow = '\x1B[33m';
var log_green = '\x1B[32m';
var log_magenta = '\x1B[35m';
@@ -69,13 +72,19 @@ function init (env, ctx, server) {
function start () {
io = require('socket.io')({
'transports': ['xhr-polling']
, 'log level': 0
'log level': 0
}).listen(server, {
//these only effect the socket.io.js file that is sent to the client, but better than nothing
'browser client minification': true
, 'browser client etag': true
, 'browser client gzip': false
, 'perMessageDeflate': {
threshold: 512
}
, transports: ["polling", "websocket"]
, httpCompression: {
threshold: 512
}
});
ctx.bus.on('teardown', function serverTeardown () {
@@ -84,6 +93,11 @@ function init (env, ctx, server) {
});
io.close();
});
ctx.bus.on('data-processed', function() {
update();
});
}
function verifyAuthorization (message, ip, callback) {
@@ -117,7 +131,7 @@ function init (env, ctx, server) {
delta.status = status(ctx.ddata.profiles);
lastProfileSwitch = ctx.ddata.lastProfileFromSwitch;
}
io.to('DataReceivers').emit('dataUpdate', delta);
io.to('DataReceivers').compress(true).emit('dataUpdate', delta);
}
}
@@ -128,15 +142,10 @@ function init (env, ctx, server) {
var timeDiff;
var history;
const address = forwarded(socket.request, socket.request.headers);
const remoteIP = address.ip;
const remoteIP = getRemoteIP(socket.request);
console.log(LOG_WS + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP);
io.emit('clients', ++watchers);
socket.on('ack', function onAck (level, group, silenceTime) {
ctx.notifications.ack(level, group, silenceTime, true);
});
socket.on('disconnect', function onDisconnect () {
io.emit('clients', --watchers);
console.log(LOG_WS + 'Disconnected client ID: ', socket.client.id);
@@ -179,7 +188,7 @@ function init (env, ctx, server) {
callback({ result: 'success' });
}
//TODO: use opts to only send delta for retro data
socket.emit('retroUpdate', { devicestatus: lastData.devicestatus });
socket.compress(true).emit('retroUpdate', { devicestatus: lastData.devicestatus });
console.info('sent retroUpdate', opts);
});
@@ -211,7 +220,7 @@ function init (env, ctx, server) {
id = new ObjectID();
}
ctx.store.collection(collection).update({ '_id': id }
ctx.store.collection(collection).updateOne({ '_id': id }
, { $set: data.data }
, function(err, results) {
@@ -259,7 +268,7 @@ function init (env, ctx, server) {
}
var objId = new ObjectID(data._id);
ctx.store.collection(collection).update({ '_id': objId }, { $unset: data.data }
ctx.store.collection(collection).updateOne({ '_id': objId }, { $unset: data.data }
, function(err, results) {
if (!err) {
@@ -385,7 +394,7 @@ function init (env, ctx, server) {
console.log(LOG_DEDUP + 'Found similiar', array[0]);
array[0].created_at = data.data.created_at;
var objId = new ObjectID(array[0]._id);
ctx.store.collection(collection).update({ '_id': objId }, { $set: { created_at: data.data.created_at } });
ctx.store.collection(collection).updateOne({ '_id': objId }, { $set: { created_at: data.data.created_at } });
if (callback) {
callback([array[0]]);
}
@@ -497,14 +506,14 @@ function init (env, ctx, server) {
}
var objId = new ObjectID(data._id);
ctx.store.collection(collection).remove({ '_id': objId }
ctx.store.collection(collection).deleteOne({ '_id': objId }
, function(err, stat) {
if (!err) {
ctx.bus.emit('data-update', {
type: data.collection
, op: 'remove'
, count: stat.result.n
, count: stat.deletedCount
, changes: data._id
});
@@ -525,7 +534,7 @@ function init (env, ctx, server) {
// [, status : true ]
// }
socket.on('authorize', function authorize (message, callback) {
const remoteIP = socket.request.connection.remoteAddress;
const remoteIP = getRemoteIP(socket.request);
verifyAuthorization(message, remoteIP, function verified (err, authorization) {
if (err) {
@@ -559,23 +568,10 @@ function init (env, ctx, server) {
}
});
});
// Pind message
// {
// mills: <local_time_in_milliseconds>
// }
socket.on('nsping', function ping (message, callback) {
var clientTime = message.mills;
timeDiff = new Date().getTime() - clientTime;
// console.log(LOG_WS + 'Ping from client ID: ',socket.client.id, ' client: ', clientType, ' timeDiff: ', (timeDiff/1000).toFixed(1) + 'sec');
if (callback) {
callback({ result: 'pong', mills: new Date().getTime(), authorization: socketAuthorization });
}
});
});
}
websocket.update = function update () {
function update () {
// console.log(LOG_WS + 'running websocket.update');
if (lastData.sgvs) {
var delta = calcData(lastData, ctx.ddata);
@@ -588,25 +584,6 @@ function init (env, ctx, server) {
lastData = ctx.ddata.clone();
};
websocket.emitNotification = function emitNotification (notify) {
if (notify.clear) {
io.emit('clear_alarm', notify);
console.info(LOG_WS + 'emitted clear_alarm to all clients');
} else if (notify.level === levels.WARN) {
io.emit('alarm', notify);
console.info(LOG_WS + 'emitted alarm to all clients');
} else if (notify.level === levels.URGENT) {
io.emit('urgent_alarm', notify);
console.info(LOG_WS + 'emitted urgent_alarm to all clients');
} else if (notify.isAnnouncement) {
io.emit('announcement', notify);
console.info(LOG_WS + 'emitted announcement to all clients');
} else {
io.emit('notification', notify);
console.info(LOG_WS + 'emitted notification to all clients');
}
};
start();
listeners();
@@ -614,6 +591,10 @@ function init (env, ctx, server) {
ctx.storageSocket.init(io);
}
if (ctx.alarmSocket) {
ctx.alarmSocket.init(io);
}
return websocket();
}
+2
View File
@@ -72,6 +72,7 @@ function init () {
, adminNotifiesEnabled: true
, obscured: ''
, obscureDeviceProvenance: ''
, authenticationPromptOnLoad: false
};
var secureSettings = [
@@ -111,6 +112,7 @@ function init () {
, bgTargetBottom: mapNumber
, authFailDelay: mapNumber
, adminNotifiesEnabled: mapTruthy
, authenticationPromptOnLoad: mapTruthy
};
function filterObj(obj, secureKeys) {
+4 -3
View File
@@ -1,6 +1,6 @@
'use strict';
const MongoClient = require('mongodb').MongoClient;
const MongoClient = require('mongodb-legacy').MongoClient;
const mongo = {
client: null,
@@ -82,10 +82,11 @@ function init(env, cb, forceNewConnection) {
mongo.ensureIndexes = function ensureIndexes(collection, fields) {
fields.forEach(function (field) {
console.info('ensuring index for: ' + field);
const name = collection.collectionName + "." + field;
console.info('ensuring index for: ' + name);
collection.createIndex(field, { 'background': true }, function (err) {
if (err) {
console.error('unable to ensureIndex for: ' + field + ' - ' + err);
console.error('unable to ensureIndex for: ' + name + ' - ' + err);
}
});
});
-191
View File
@@ -1,191 +0,0 @@
'use strict';
var _ = require('lodash');
var fs = require('fs');
var crypto = require('crypto');
var MongoMock = require('mongomock');
var config = {
collections: {}
};
function init (env, callback) {
if (!env.storageURI || !_.isString(env.storageURI)) {
throw new Error('openaps config uri is missing or invalid');
}
var configPath = env.storageURI.split('openaps://').pop();
function addId (data) {
var shasum = crypto.createHash('sha1');
shasum.update(JSON.stringify(data));
data._id = shasum.digest('hex');
}
function loadData (path) {
if (!path || !_.isString(path)) {
return [ ];
}
try {
purgeCache(path);
var inputData = require(path);
if (_.isArray(inputData)) {
//console.info('>>>input is an array', path);
_.forEach(inputData, addId);
} else if (!_.isEmpty(inputData) && _.isObject(inputData)) {
//console.info('>>>input is an object', path);
inputData.created_at = new Date(fs.statSync(path).mtime).toISOString();
addId(inputData);
inputData = [ inputData ];
} else {
//console.info('>>>input is something else', path, inputData);
inputData = [ ];
}
return inputData;
} catch (err) {
console.error('unable to find input data for', path, err);
return [ ];
}
}
function reportAsCollection (name) {
var data = { };
var input = _.get(config, 'collections.' + name + '.input');
if (_.isArray(input)) {
//console.info('>>>input is an array', input);
data[name] = _.flatten(_.map(input, loadData));
} else {
data[name] = loadData(input);
}
var mock = new MongoMock(data);
var collection = mock.collection(name);
var wrapper = {
findQuery: null
, sortQuery: null
, limitCount: null
, find: function find (query) {
query = _.cloneDeepWith(query, function booleanize (value) {
//TODO: for some reason we're getting {$exists: NaN} instead of true/false
if (value && _.isObject(value) && '$exists' in value) {
return {$exists: true};
}
});
wrapper.findQuery = query;
return wrapper;
}
, limit: function limit (count) {
wrapper.limitCount = count;
return wrapper;
}
, sort: function sort (query) {
wrapper.sortQuery = query;
return wrapper;
}
, toArray: function toArray(callback) {
collection.find(wrapper.findQuery).toArray(function intercept (err, results) {
if (err) {
return callback(err, results);
}
if (wrapper.sortQuery) {
var field = _.keys(wrapper.sortQuery).pop();
//console.info('>>>sortField', field);
if (field) {
results = _.sortBy(results, field);
if (-1 === wrapper.sortQuery[field]) {
//console.info('>>>sort reverse');
results = _.reverse(results);
}
}
}
if (wrapper.limitCount !== null && _.isNumber(wrapper.limitCount)) {
//console.info('>>>limit count', wrapper.limitCount);
results = _.take(results, wrapper.limitCount);
}
//console.info('>>>toArray', name, wrapper.findQuery, wrapper.sortQuery, wrapper.limitCount, results.length);
callback(null, results);
});
return wrapper;
}
};
return wrapper;
}
try {
var customConfig = require(configPath);
config = _.merge({}, customConfig, config);
callback(null, {
collection: reportAsCollection
, ensureIndexes: _.noop
});
} catch (err) {
callback(err);
}
}
/**
* Removes a module from the cache
*
* see http://stackoverflow.com/a/14801711
*/
function purgeCache(moduleName) {
// Traverse the cache looking for the files
// loaded by the specified module name
searchCache(moduleName, function (mod) {
delete require.cache[mod.id];
});
// Remove cached paths to the module.
// Thanks to @bentael for pointing this out.
Object.keys(module.constructor._pathCache).forEach(function(cacheKey) {
if (cacheKey.indexOf(moduleName)>0) {
delete module.constructor._pathCache[cacheKey];
}
});
}
/**
* Traverses the cache to search for all the cached
* files of the specified module name
*
* see http://stackoverflow.com/a/14801711
*/
function searchCache(moduleName, callback) {
// Resolve the module identified by the specified name
var mod = require.resolve(moduleName);
// Check if the module has been resolved and found within
// the cache
if (mod && ((mod = require.cache[mod]) !== undefined)) {
// Recursively go over the results
(function traverse(mod) {
// Go over each of the module's children and
// traverse them
mod.children.forEach(function (child) {
traverse(child);
});
// Call the specified callback providing the
// found cached module
callback(mod);
}(mod));
}
}
module.exports = init;
+1 -2
View File
@@ -1,12 +1,11 @@
'use strict';
var _ = require('lodash');
var moment = require('moment-timezone');
var units = require('./units')();
function init(ctx) {
var moment = ctx.moment;
var settings = ctx.settings;
var translate = ctx.language.translate;
var timeago = require('./plugins/timeago')(ctx);
+3386 -6663
View File
File diff suppressed because it is too large Load Diff
+33 -34
View File
@@ -1,6 +1,6 @@
{
"name": "nightscout",
"version": "14.2.6",
"version": "15.0.0",
"description": "Nightscout acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime.",
"license": "AGPL-3.0",
"author": "Nightscout Team",
@@ -31,11 +31,11 @@
"test-single": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js --exit ./tests/$TEST.test.js",
"test-ci": "env-cmd -f ./tests/ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --timeout 5000 --require ./tests/hooks.js --exit ./tests/*.test.js",
"env": "env",
"postinstall": "webpack --mode production --config webpack/webpack.config.js && npm run-script generate-keys",
"bundle": "webpack --mode production --config webpack/webpack.config.js && npm run-script generate-keys",
"bundle-dev": "webpack --mode development --config webpack/webpack.config.js && npm run-script generate-keys",
"postinstall": "webpack --mode production --config webpack/webpack.config.js && npm run-script post-generate-keys",
"bundle": "webpack --mode production --config webpack/webpack.config.js && npm run-script post-generate-keys",
"bundle-dev": "webpack --mode development --config webpack/webpack.config.js && npm run-script post-generate-keys",
"bundle-analyzer": "webpack --mode development --config webpack/webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json",
"generate-keys": "node bin/generateRandomString.js >tmp/randomString",
"post-generate-keys": "node bin/generateRandomString.js >node_modules/.cache/_ns_cache/randomString",
"coverage": "cat ./coverage/lcov.info | env-cmd -f ./tests/ci.test.env codacy-coverage || echo NO COVERAGE",
"dev": "env-cmd -f ./my.env nodemon --inspect lib/server/server.js 0.0.0.0",
"dev-test": "env-cmd -f ./my.devtest.env nodemon --inspect lib/server/server.js 0.0.0.0",
@@ -65,17 +65,17 @@
}
},
"engines": {
"node": "^16.x || ^14.x || ^12.x"
"node": "^16.x || ^14.x",
"npm": "^6.x"
},
"dependencies": {
"@babel/core": "^7.11.1",
"@babel/preset-env": "^7.12.11",
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@parse/node-apn": "^5.1.3",
"acorn": "^8.0.5",
"acorn-jsx": "^5.3.1",
"apn": "^2.2.0",
"async": "^0.9.2",
"babel-loader": "^8.1.0",
"base64url": "^3.0.1",
"babel-loader": "^8.2.5",
"body-parser": "^1.19.0",
"bootevent": "0.0.1",
"braces": "^3.0.2",
@@ -88,11 +88,11 @@
"d3": "^5.16.0",
"dompurify": "^2.2.6",
"easyxml": "^2.0.1",
"ejs": "^2.7.4",
"ejs": "^3.1.8",
"errorhandler": "^1.5.1",
"event-stream": "3.3.4",
"expose-loader": "^2.0.0",
"express": "^4.17.1",
"express": "4.17.1",
"express-minify": "^1.0.0",
"fast-password-entropy": "^1.1.1",
"file-loader": "^6.2.0",
@@ -103,22 +103,22 @@
"jquery-ui-bundle": "^1.12.1-migrate",
"jquery.tooltips": "^1.0.0",
"js-storage": "^1.1.0",
"jsdom": "^11.11.0",
"jsdom": "=11.11.0",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.20",
"memory-cache": "^0.2.0",
"mime": "^2.4.6",
"minimed-connect-to-nightscout": "^1.5.3",
"minimed-connect-to-nightscout": "^1.5.5",
"moment": "^2.27.0",
"moment-locales-webpack-plugin": "^1.2.0",
"moment-timezone": "^0.5.31",
"moment-timezone-data-webpack-plugin": "^1.3.0",
"mongo-url-parser": "^1.0.1",
"mongodb": "^3.6.0",
"moment-timezone-data-webpack-plugin": "^1.5.0",
"mongo-url-parser": "^1.0.2",
"mongodb-legacy": "^5.0.0",
"mongomock": "^0.1.2",
"nightscout-connect": "^0.0.12",
"node-cache": "^4.2.1",
"parse-duration": "^0.1.3",
"pem": "^1.14.4",
"process": "^0.11.10",
"pushover-notifications": "^1.2.2",
"random-token": "0.0.8",
@@ -127,36 +127,35 @@
"share2nightscout-bridge": "^0.2.9",
"shiro-trie": "^0.4.9",
"simple-statistics": "^0.7.0",
"socket.io": "~2.4.0",
"socket.io": "~4.5.4",
"socket.io-client": "^4.5.4",
"stream-browserify": "^3.0.0",
"style-loader": "^0.23.1",
"swagger-ui-dist": "^3.32.1",
"swagger-ui-express": "^4.1.4",
"swagger-ui-dist": "^4.13.2",
"swagger-ui-express": "^4.5.0",
"traverse": "^0.6.6",
"uuid": "^3.4.0",
"webpack": "^5.20.2",
"webpack-cli": "^4.5.0"
"uuid": "^9.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"devDependencies": {
"@types/tough-cookie": "^4.0.0",
"axios": "^0.21.1",
"babel-eslint": "^10.1.0",
"benv": "^3.3.0",
"codacy-coverage": "^3.4.0",
"csv-parse": "^4.12.0",
"env-cmd": "^10.1.0",
"eslint": "^7.19.0",
"eslint-plugin-security": "^1.4.0",
"eslint-webpack-plugin": "^2.4.3",
"mocha": "^8.1.1",
"nodemon": "^1.19.4",
"eslint-webpack-plugin": "^2.7.0",
"mocha": "^8.4.0",
"nodemon": "^2.0.19",
"nyc": "^14.1.1",
"should": "^13.2.3",
"supertest": "^3.4.2",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-dev-middleware": "^4.1.0",
"webpack-hot-middleware": "^2.25.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-dev-middleware": "^4.3.0",
"webpack-hot-middleware": "^2.25.2",
"xml2js": "^0.4.23"
},
"browserslist": "> 0.25%, not dead, ios_saf 10"
}
}
+2 -2
View File
@@ -25,13 +25,13 @@ describe('Clean MONGO after tests', function ( ) {
});
it('wipe treatment data', function (done) {
self.ctx.treatments().remove({ }, function ( ) {
self.ctx.treatments().deleteMany({ }, function ( ) {
done();
});
});
it('wipe entries data', function (done) {
self.ctx.entries().remove({ }, function ( ) {
self.ctx.entries().deleteMany({ }, function ( ) {
done();
});
});
+1 -1
View File
@@ -70,7 +70,7 @@ describe('admintools', function ( ) {
before(function (done) {
benv.setup(function() {
benv.require(__dirname + '/../tmp/public/js/bundle.app.js');
benv.require(__dirname + '/../node_modules/.cache/_ns_cache/public/js/bundle.app.js');
self.$ = $;
+51 -9
View File
@@ -4,8 +4,12 @@ var request = require('supertest');
var load = require('./fixtures/load');
var bootevent = require('../lib/server/bootevent');
var language = require('../lib/language')();
const _ = require('lodash');
require('should');
const FIVE_MINUTES=1000*60*5;
describe('Entries REST api', function ( ) {
var entries = require('../lib/api/entries/');
var self = this;
@@ -24,25 +28,46 @@ describe('Entries REST api', function ( ) {
bootevent(self.env, language).boot(function booted (ctx) {
self.app.use('/', entries(self.app, self.wares, ctx, self.env));
self.archive = require('../lib/server/entries')(self.env, ctx);
var creating = load('json');
creating.push({type: 'sgv', sgv: 100, date: Date.now()});
self.archive.create(creating, done);
self.ctx = ctx;
done();
});
});
beforeEach(function (done) {
var creating = load('json');
creating.push({type: 'sgv', sgv: 100, date: Date.now()});
self.archive.create(creating, done);
for (let i = 0; i < 20; i++) {
const e = {type: 'sgv', sgv: 100, date: Date.now()};
e.date = e.date - FIVE_MINUTES * i;
creating.push(e);
}
creating = _.sortBy(creating, function(item) {
return item.date;
});
function setupDone() {
console.log('Setup complete');
done();
}
function waitForASecond() {
// wait for event processing of cache entries to actually finish
setTimeout(function() {
setupDone();
}, 100);
}
self.archive.create(creating, waitForASecond);
});
afterEach(function (done) {
self.archive( ).remove({ }, done);
self.archive( ).deleteMany({ }, done);
});
after(function (done) {
self.archive( ).remove({ }, done);
self.archive( ).deleteMany({ }, done);
});
// keep this test pinned at or near the top in order to validate all
@@ -89,6 +114,23 @@ describe('Entries REST api', function ( ) {
});
});
it('gets entries in right order without type specifier', function (done) {
var defaultCount = 10;
request(self.app)
.get('/entries.json')
.expect(200)
.end(function (err, res) {
res.body.should.be.instanceof(Array).and.have.lengthOf(defaultCount);
var array = res.body;
var firstEntry = array[0];
var secondEntry = array[1];
firstEntry.date.should.be.above(secondEntry.date);
done( );
});
});
it('/echo/ api shows query', function (done) {
request(self.app)
@@ -196,7 +238,7 @@ describe('Entries REST api', function ( ) {
it('/entries/:id', function (done) {
var app = self.app;
self.archive.list({count: 1}, function(err, records) {
self.archive.list({count: 10}, function(err, records) {
var currentId = records.pop()._id.toString();
request(app)
.get('/entries/'+currentId+'.json')
+16 -2
View File
@@ -29,7 +29,7 @@ describe('Security of REST API V1', function() {
self.app.use('/api/v2/authorization', ctx.authorization.endpoints);
let authResult = await authSubject(ctx.authorization.storage);
self.subject = authResult.subject;
self.token = authResult.token;
self.token = authResult.accessToken;
done();
});
@@ -71,6 +71,20 @@ describe('Security of REST API V1', function() {
});
});
it('Should return a JWT with default roles on broken role token', function(done) {
const now = Math.round(Date.now() / 1000) - 1;
request(self.app)
.get('/api/v2/authorization/request/' + self.token.noneSubject)
.expect(200)
.end(function(err, res) {
const decodedToken = jwt.decode(res.body.token);
decodedToken.accessToken.should.equal(self.token.noneSubject);
decodedToken.iat.should.be.aboveOrEqual(now);
decodedToken.exp.should.be.above(decodedToken.iat);
done();
});
});
it('Data load should succeed with API SECRET', function(done) {
request(self.app)
.get('/api/v1/entries.json')
@@ -138,7 +152,7 @@ describe('Security of REST API V1', function() {
.expect(200)
.end(function(err, res) {
res.body.message.message.should.equal('OK');
res.body.message.isAdmin.should.equal(true);
res.body.message.isAdmin.should.equal(true);
done();
});
});
+5 -5
View File
@@ -35,7 +35,7 @@ describe('Treatment API', function ( ) {
it('post single treatments', function (done) {
self.ctx.treatments().remove({ }, function ( ) {
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
var now = (new Date()).toISOString();
request(self.app)
.post('/api/treatments/')
@@ -88,8 +88,8 @@ describe('Treatment API', function ( ) {
var current_time = Date.now();
console.log('Testing date with local format: ', _moment(current_time).format("YYYY-MM-DDTHH:mm:ss.SSSZZ"));
self.ctx.treatments().remove({ }, function ( ) {
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash || '')
@@ -122,7 +122,7 @@ describe('Treatment API', function ( ) {
it('post a treatment array', function (done) {
self.ctx.treatments().remove({ }, function ( ) {
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
var now = (new Date()).toISOString();
request(self.app)
.post('/api/treatments/')
@@ -149,7 +149,7 @@ describe('Treatment API', function ( ) {
});
it('post a treatment array and dedupe', function (done) {
self.ctx.treatments().remove({ }, function ( ) {
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
var now = (new Date()).toISOString();
request(self.app)
.post('/api/treatments/')
+2 -2
View File
@@ -39,11 +39,11 @@ describe('authed REST api', function ( ) {
});
afterEach(function (done) {
this.archive( ).remove({ }, done);
this.archive( ).deleteMany({ }, done);
});
after(function (done) {
this.archive( ).remove({ }, done);
this.archive( ).deleteMany({ }, done);
});
it('disallow unauthorized POST', function (done) {

Some files were not shown because too many files have changed in this diff Show More