Merge branch 'dev' into docker-compose-restarts

This commit is contained in:
Ben West
2025-05-23 09:40:59 -07:00
committed by GitHub
153 changed files with 9887 additions and 10223 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
+5 -5
View File
@@ -17,7 +17,7 @@ on:
branches: [ dev ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
branches: [ dev ]
schedule:
- cron: '43 23 * * 3'
@@ -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
+35 -38
View File
@@ -16,15 +16,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x]
mongodb-version: [4.2, 4.4]
node-version: [14.x, 16.x, 20, lts/*]
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 }}
@@ -40,14 +40,19 @@ jobs:
- name: Send Coverage
run: npm run-script coverage
publish_dev:
name: Publish dev branch to Docker Hub
publish:
name: Publish to Docker Hub
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/dev' && github.repository_owner == 'nightscout'
if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev') && github.repository_owner == 'nightscout'
env:
DOCKER_IMAGE: nightscout/cgm-remote-monitor
PLATFORMS: linux/amd64,linux/arm64
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
@@ -55,39 +60,31 @@ 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()
run: |
docker build --no-cache=true -t ${{ env.DOCKER_IMAGE }}:dev_${{ github.sha }} .
docker image push ${{ env.DOCKER_IMAGE }}:dev_${{ github.sha }}
docker tag ${{ env.DOCKER_IMAGE }}:dev_${{ github.sha }} ${{ env.DOCKER_IMAGE }}:latest_dev
docker image push ${{ env.DOCKER_IMAGE }}:latest_dev
publish_master:
name: Publish master branch to Docker Hub
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && github.repository_owner == 'nightscout'
env:
DOCKER_IMAGE: nightscout/cgm-remote-monitor
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
if: success() && github.ref == 'refs/heads/dev'
uses: docker/build-push-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Clean git Checkout
if: success()
uses: actions/checkout@v2
- name: get-npm-version
if: success()
context: .
push: true
no-cache: true
platforms: ${{ env.PLATFORMS }}
tags: |
${{ env.DOCKER_IMAGE }}:dev_${{ github.sha }}
${{ env.DOCKER_IMAGE }}:latest_dev
- name: Get Nightscout release version
if: success() && github.ref == 'refs/heads/master'
id: package-version
uses: martinbeentjes/npm-get-version-action@master
- name: Build, tag and push the master Docker image
if: success()
run: |
docker build --no-cache=true -t ${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }} .
docker image push ${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }}
docker tag ${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }} ${{ env.DOCKER_IMAGE }}:latest
docker image push ${{ env.DOCKER_IMAGE }}:latest
if: success() && github.ref == 'refs/heads/master'
uses: docker/build-push-action@v2
with:
context: .
push: true
no-cache: true
platforms: ${{ env.PLATFORMS }}
tags: |
${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }}
${{ env.DOCKER_IMAGE }}:latest
+1 -1
View File
@@ -1 +1 @@
14.15.3
16.16.0
+12 -7
View File
@@ -1,18 +1,23 @@
FROM node:14.15.3-alpine
FROM node:16.16.0-alpine
LABEL maintainer="Nightscout Contributors"
RUN mkdir -p /opt/app
ADD . /opt/app
WORKDIR /opt/app
RUN chown -R node:node /opt/app
USER node
ADD . /opt/app
RUN npm install && \
# TODO: We should be able to do `RUN npm install --only=production`.
# For this to work, we need to copy only package.json and things needed for `npm`'s to succeed.
# TODO: Do we need to re-add `npm audit fix`? Or should that be part of a development process/stage?
RUN npm install --cache /tmp/empty-cache && \
npm run postinstall && \
npm run env && \
npm audit fix
rm -rf /tmp/*
# TODO: These should be added in the future to correctly cache express-minify content to disk
# Currently, doing this breaks the browser cache.
# mkdir /tmp/public && \
# chown node:node /tmp/public
USER node
EXPOSE 1337
CMD ["node", "lib/server/server.js"]
+133 -33
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,15 +103,16 @@ 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)
- [`override` (Override Mode)](#override-override-mode)
- [`xdripjs` (xDrip-js)](#xdripjs-xdrip-js)
- [`alexa` (Amazon Alexa)](#alexa-amazon-alexa)
- [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow)
- [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) [broken]
- [`speech` (Speech)](#speech-speech)
- [`cors` (CORS)](#cors-cors)
- [Extended Settings](#extended-settings)
@@ -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.
@@ -568,9 +666,11 @@ For remote overrides, the following extended settings must be configured:
##### `alexa` (Amazon Alexa)
Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md)
##### `googlehome` (Google Home/DialogFLow)
##### `googlehome` (Google Home/DialogFLow) [broken]
Integration with Google Home (via DialogFlow), [detailed setup instructions](docs/plugins/googlehome-plugin.md)
Unfortunately this integration broke when [Google discontinued conversational actions](https://developers.google.com/assistant/ca-sunset). We'll keep this here for reference, in case it can be revived at some point in the future.
##### `speech` (Speech)
Speech synthesis plugin. When enabled, speaks out the blood glucose values, IOB and alarms. Note you have to set the LANGUAGE setting on the server to get all translated alarms.
+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,11 +1,19 @@
version: '3'
x-logging:
&default-logging
options:
max-size: '10m'
max-file: '5'
driver: json-file
services:
mongo:
image: mongo:4.4
restart: always
volumes:
- ${NS_MONGO_DATA_DIR:-./mongo-data}:/data/db:cached
logging: *default-logging
nightscout:
image: nightscout/cgm-remote-monitor:latest
@@ -20,6 +28,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
@@ -76,3 +85,4 @@ services:
volumes:
- './letsencrypt:/letsencrypt'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
logging: *default-logging
+3
View File
@@ -1,3 +1,6 @@
# The Google Assistant integration is broken!
Unfortunately this integration broke when [Google discontinued conversational actions](https://developers.google.com/assistant/ca-sunset). We'll keep this here for reference, in case it can be revived at some point in the future.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+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) {
+4 -15
View File
@@ -12,23 +12,14 @@ function configure(app, wares, ctx) {
, api = express.Router();
api.use(wares.compression());
api.use(wares.bodyParser({
limit: 1048576 * 50
}));
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw({
limit: 1048576
}));
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
limit: '50Mb'
}));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({
limit: 1048576
, extended: true
}));
api.use(wares.urlencodedParser);
// invoke common middleware
api.use(wares.sendJSONStatus);
@@ -94,9 +85,7 @@ function configure(app, wares, ctx) {
});
}
api.post('/activity/', wares.bodyParser({
limit: 1048576 * 50
}), ctx.authorization.isPermitted('api:activity:create'), post_response);
api.post('/activity/', ctx.authorization.isPermitted('api:activity:create'), post_response);
api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) {
ctx.activity.remove(req.params._id, function() {
+6 -13
View File
@@ -11,12 +11,12 @@ function configure (app, wares, ctx, env) {
// invoke common middleware
api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw());
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
api.use(wares.jsonParser);
// also support url-encoded content-type
api.use(wares.urlencodedParser);
// text body types get handled as raw buffer stream
ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa);
@@ -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;
}
+4 -6
View File
@@ -13,14 +13,12 @@ function configure (app, wares, ctx, env) {
// invoke common middleware
api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw());
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
api.use(wares.jsonParser);
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ extended: true }));
api.use(wares.urlencodedParser);
// text body types get handled as raw buffer stream
api.use(ctx.authorization.isPermitted('api:devicestatus:read'));
+18 -18
View File
@@ -41,20 +41,18 @@ function configure (app, wares, ctx, env) {
// invoke common middleware
api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw());
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
limit: '50Mb'
}));
// also support url-encoded content-type
api.use(wares.urlencodedParser);
// text body types get handled as raw buffer stream
// shortcut to use extension to specify output content-type
api.use(wares.extensions([
'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv'
]));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({
extended: true
}));
api.use(ctx.authorization.isPermitted('api:entries:read'));
/**
@@ -357,7 +355,7 @@ function configure (app, wares, ctx, env) {
res.entries_err = err;
return next();
});
}, format_entries);
}, wares.obscure_device, format_entries);
/**
* @module get#/entries/:spec
@@ -391,7 +389,7 @@ function configure (app, wares, ctx, env) {
prepReqModel(req, req.params.model);
query_models(req, res, next);
}
}, format_entries);
}, wares.obscure_device, format_entries);
/**
* @module get#/entries
@@ -402,7 +400,7 @@ function configure (app, wares, ctx, env) {
* `find[date]`.
*
*/
api.get('/entries', ifModifiedSinceCTX, query_models, format_entries);
api.get('/entries', ifModifiedSinceCTX, query_models, wares.obscure_device, format_entries);
/**
* @function echo_query
@@ -474,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();
}
@@ -740,7 +740,7 @@ function configure (app, wares, ctx, env) {
* @routed
* @response 200 /definitions/Entries
*/
api.get('/times/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, prep_patterns, query_models, format_entries);
api.get('/times/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, prep_patterns, query_models, wares.obscure_device, format_entries);
api.get('/count/:storage/where', prep_storage, count_records, format_results);
@@ -755,7 +755,7 @@ function configure (app, wares, ctx, env) {
/api/v1/slice/entries/dateString/mbg/2015.json
```
*/
api.get('/slice/:storage/:field/:type?/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, query_models, format_entries);
api.get('/slice/:storage/:field/:type?/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, query_models, wares.obscure_device, format_entries);
/**
* @module post#/entries/preview
@@ -767,7 +767,7 @@ function configure (app, wares, ctx, env) {
// setting this flag tells insert_entries to not actually store the results
req.persist_entries = false;
next();
}, insert_entries, format_entries);
}, insert_entries, wares.obscure_device, format_entries);
// Protect endpoints with authenticated api.
if (app.enabled('api')) {
@@ -782,7 +782,7 @@ function configure (app, wares, ctx, env) {
// setting this flag tells insert_entries to store the results
req.persist_entries = true;
next();
}, insert_entries, format_entries);
}, insert_entries, wares.obscure_device, format_entries);
/**
* @module delete#/entries/:spec
+5 -6
View File
@@ -9,14 +9,13 @@ function configure (app, wares, ctx) {
// invoke common middleware
api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw( ));
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
api.use(wares.jsonParser);
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ extended: true }));
api.use(wares.urlencodedParser);
// text body types get handled as raw buffer stream
// shortcut to use extension to specify output content-type
api.use(ctx.authorization.isPermitted('api:food:read'));
+4 -10
View File
@@ -11,9 +11,10 @@ function configure (app, wares, ctx, env) {
// invoke common middleware
api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw());
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json());
api.use(wares.jsonParser);
ctx.virtAsstBase.setupVirtAsstHandlers(ctx.googleHome);
@@ -30,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( );
@@ -45,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;
}
+1 -1
View File
@@ -6,7 +6,7 @@ function create (env, ctx) {
, app = express( )
;
var wares = require('../middleware/')(env);
const wares = ctx.wares;
// set up express app with our options
app.set('name', env.name);
+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);
+4 -6
View File
@@ -9,14 +9,12 @@ function configure (app, wares, ctx) {
// invoke common middleware
api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw( ));
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
api.use(wares.jsonParser);
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ extended: true }));
api.use(wares.urlencodedParser);
// text body types get handled as raw buffer stream
api.use(ctx.authorization.isPermitted('api:profile:read'));
+3 -1
View File
@@ -2,6 +2,7 @@
function configure (app, wares, env, ctx) {
var express = require('express'),
forwarded = require('forwarded-for'),
api = express.Router( )
;
@@ -21,7 +22,8 @@ function configure (app, wares, env, ctx) {
var authToken = req.query.token || req.query.secret || '';
function getRemoteIP (req) {
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const address = forwarded(req, req.headers);
return address.ip;
}
var date = new Date();
+11 -19
View File
@@ -13,24 +13,16 @@ function configure (app, wares, ctx, env) {
, api = express.Router();
api.use(wares.compression());
api.use(wares.bodyParser({
limit: 1048576 * 50
, extended: true
}));
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw({
limit: 1048576
}));
api.use(wares.rawParser);
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
limit: '50Mb'
}));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({
limit: 1048576
, extended: true
}));
api.use(wares.urlencodedParser);
// invoke common middleware
api.use(wares.sendJSONStatus);
@@ -71,7 +63,8 @@ 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({
@@ -80,11 +73,12 @@ function configure (app, wares, ctx, env) {
, type: 'internal'
});
return;
} else {
return res.json(results);
}
}
return res.json(results);
}
// List treatments available
api.get('/treatments', function(req, res) {
var query = req.query;
@@ -150,9 +144,7 @@ function configure (app, wares, ctx, env) {
});
}
api.post('/treatments/', wares.bodyParser({
limit: 1048576 * 50
}), ctx.authorization.isPermitted('api:treatments:create'), post_response);
api.post('/treatments/', ctx.authorization.isPermitted('api:treatments:create'), post_response);
/**
* @function delete_records
+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;
@@ -7,6 +7,13 @@ function configure (app, ctx) {
, api = express.Router( )
;
api.use(ctx.wares.compression());
api.use(ctx.wares.rawParser);
api.use(ctx.wares.bodyParser.json({
limit: '50Mb'
}));
api.use(ctx.wares.urlencodedParser);
api.post('/loop', ctx.authorization.isPermitted('notifications:loop:push'), function (req, res) {
ctx.loop.sendNotification(req.body, req.connection.remoteAddress, function (error) {
if (error) {
@@ -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);
+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;
+198
View File
@@ -0,0 +1,198 @@
'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);
});
});
// Turns all notifications on the event bus back into events to be
// broadcast to clients.
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
// Client sends ack, which sends a notificaiton through our internal bus
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;
}
});
}
if (!message) { message = {}; }
// Web client (jwt access token or api_hash)
/*
* On the web: a client may have saved a secret or using a jwtToken, or may have none.
* Some pages will automatically prompt for authorization, when needed.
* To make the main homepage require authorization as well, set
* AUTHENTICATION_PROMPT_ON_LOAD=true.
*
* If there is missing authorization when authorization is required,
* rejecting the attempt in order to trigger a prompt on the client.
* If there is no authorization required, or there are available
* credentials, attempt to resolve the available permissions.
* When processing ACK messages that dismiss alarms, Authorization should be
* required.
*/
var shouldTry = true;
if (env.settings.authenticationPromptOnLoad) {
if (!message.jwtToken && !message.secret) {
shouldTry = false;
}
}
if (message && shouldTry) {
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 {
var perms = {
read: ctx.authorization.checkMultiple('api:*:read', auth.shiros)
, ack: ctx.authorization.checkMultiple('notifications:*:ack', auth.shiros)
};
// Subscribe for acking alarms
// TODO: does this produce double ACK after the authorizing? Only if reconnecting?
// TODO: how will perms get updated after authorizing?
socket.on('ack', function onAck (level, group, silenceTime) {
if (perms.ack) {
// This goes through the server-wide event bus.
ctx.notifications.ack(level, group, silenceTime, true);
console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime);
} else {
// TODO: send a message to client to silence locally, but not
// globally, and request authorization.
// This won't go through th event bus.
// var acked = { silenceTime, group, level };
// socket.emit('authorization_needed', acked);
}
});
/* TODO: need to know when to update the permissions.
// Can we use
socket.on('resubscribe', function update_permissions ( ) {
// perms = { ... };
});
*/
var okResponse = { success: true, message: 'Subscribed for alarms', ...perms };
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)
+185 -116
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,11 +96,21 @@ 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
@@ -97,19 +118,19 @@ Sample result:
"status": 200,
"result": [
{
"dateString": "2019-07-30T02:24:50.434+0200",
"sgv": 115,
"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({
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',
body: doc,
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}`
},
(error, response, body) => console.log(body));
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,
"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,
"srvDate": 1613057924021,
"collections": {
"devicestatus": 1564591490074,
"entries": 1564591486801,
"profile": 1548524042744,
"treatments": 1564591627732
"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({
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',
body: doc,
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(body));
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({
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',
body: doc,
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(body));
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({
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',
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(body));
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
};
}
+3 -1
View File
@@ -23,7 +23,9 @@ 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;
}
+11 -2
View File
@@ -4,11 +4,13 @@ const apiConst = require('./const.json')
, _ = require('lodash')
, shiroTrie = require('shiro-trie')
, opTools = require('./shared/operationTools')
, forwarded = require('forwarded-for')
;
function getRemoteIP (req) {
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const address = forwarded(req, req.headers);
return address.ip;
}
@@ -23,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);
}
+7 -1
View File
@@ -1,6 +1,12 @@
'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 any storage change
@@ -28,7 +34,7 @@ function StorageSocket (app, env, ctx) {
self.namespace = io.of(NAMESPACE);
self.namespace.on('connection', function onConnected (socket) {
const remoteIP = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress;
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
+4 -2
View File
@@ -6,9 +6,11 @@ const shiroTrie = require('shiro-trie');
const consts = require('./../constants');
const sleep = require('util').promisify(setTimeout);
const forwarded = require('forwarded-for');
function getRemoteIP (req) {
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const address = forwarded(req, req.headers);
return address.ip;
}
function init (env, ctx) {
@@ -296,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
+5
View File
@@ -122,6 +122,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);
+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;
+72 -8
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 = {};
@@ -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,39 @@ 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);
var shouldAuthenticationPromptOnLoad = client.settings.authenticationPromptOnLoad ;
if (!data.success) {
if (!data.read || !hasRequiredPermission() || shouldAuthenticationPromptOnLoad) {
return client.hashauth.requestAuthentication(function afterRequest () {
return client.hashauth.updateSocketAuth();
});
}
}
}
);
}
alarmSocket.on('connect', function() {
client.subscribeForAlarms();
});
function hasRequiredPermission () {
if (client.requiredPermission) {
if (client.hashauth && client.hashauth.isAuthenticated()) {
@@ -1144,7 +1186,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 +1196,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 +1215,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,12 +1227,34 @@ 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);
}
});
/*
*
// TODO: When an unauthorized client attempts to silence an alarm, we should
// allow silencing locally, request for authorization, and if the
// authorization succeeds even republish the ACK notification. something like...
alarmSocket.on('authorization_needed', function(details) {
if (alarmInProgress) {
console.log('clearing alarm');
stopAlarm(true, details.silenceTime, currentNotify);
}
client.hashauth.requestAuthentication(function afterRequest () {
console.log("SUCCESSFULLY AUTHORIZED, REPUBLISHED ACK?");
// easiest way to update permission set on server side is to send another message.
alarmSocket.emit('resubscribe', currentNotify, details);
if (isClient && currentNotify) {
alarmSocket.emit('ack', currentNotify.level, currentNotify.group, details.silenceTime);
}
});
});
*/
$('#testAlarms').click(function(event) {
+1 -1
View File
@@ -7,7 +7,7 @@
"HTTP_BAD_REQUEST": 400,
"ENTRIES_DEFAULT_COUNT" : 10,
"PROFILES_DEFAULT_COUNT" : 10,
"MMOL_TO_MGDL": 18,
"MMOL_TO_MGDL": 18.018018018,
"ONE_DAY" : 86400000,
"TWO_DAYS" : 172800000,
"FIFTEEN_MINUTES": 900000,
+3 -3
View File
@@ -139,7 +139,6 @@ function init(env, ctx) {
});
console.info('Load Complete:\n\t', counts.join(', '));
done(err, result);
}
@@ -190,6 +189,7 @@ function loadEntries(ddata, ctx, callback) {
}
};
var obscureDeviceProvenance = ctx.settings.obscureDeviceProvenance;
ctx.entries.list(q, function(err, results) {
if (err) {
@@ -213,7 +213,7 @@ function loadEntries(ddata, ctx, callback) {
_id: element._id,
mgdl: Number(element.mbg),
mills: element.date,
device: element.device,
device: obscureDeviceProvenance || element.device,
type: 'mbg'
});
} else if (element.sgv) {
@@ -221,7 +221,7 @@ function loadEntries(ddata, ctx, callback) {
_id: element._id,
mgdl: Number(element.sgv),
mills: element.date,
device: element.device,
device: obscureDeviceProvenance || element.device,
direction: element.direction,
filtered: element.filtered,
unfiltered: element.unfiltered,
+23 -10
View File
@@ -266,28 +266,41 @@ function init () {
// filter temp target
var tempTargetTreatments = ddata.treatments.filter(function filterTargets (t) {
//check for a units being sent
if (t.units) {
return t.eventType && t.eventType.indexOf('Temporary Target') > -1;
});
function convertTempTargetTreatmentUnites (_treatments) {
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 (t.targetTop < 20) {
if (!converted && (t.targetTop < 20 || t.targetBottom < 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))
+3 -1
View File
@@ -12,7 +12,8 @@ function init (fs) {
language.lang = 'en';
language.languages = [
{ code: 'bg', file: 'bg_BG', language: 'Български', speechCode: 'bg-BG' }
{ code: 'ar', file: 'ar_SA', language: 'اللغة العربية', speechCode: 'ar-SA' }
, { code: 'bg', file: 'bg_BG', language: 'Български', speechCode: 'bg-BG' }
, { code: 'cs', file: 'cs_CZ', language: 'Čeština', speechCode: 'cs-CZ' }
, { code: 'de', file: 'de_DE', language: 'Deutsch', speechCode: 'de-DE' }
, { code: 'dk', file: 'da_DK', language: 'Dansk', speechCode: 'dk-DK' }
@@ -38,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' }
];
+16 -3
View File
@@ -3,19 +3,32 @@
var wares = {
sendJSONStatus : require('./send-json-status'),
bodyParser : require('body-parser'),
compression : require('compression')
compression : require('compression'),
obscureDeviceProvenance: require('./obscure-provenance')
};
function extensions (list) {
return require('./express-extension-to-accept')(list);
}
function configure () {
function configure (env) {
return {
sendJSONStatus: wares.sendJSONStatus( ),
bodyParser: wares.bodyParser,
jsonParser: wares.bodyParser.json({
limit: '1Mb',
}),
urlencodedParser: wares.bodyParser.urlencoded({
limit: '1Mb',
extended: true,
parameterLimit: 50000
}),
rawParser: wares.bodyParser.raw({
limit: '1Mb'
}),
compression: wares.compression,
extensions: extensions
extensions: extensions,
obscure_device: wares.obscureDeviceProvenance(env)
};
}
+15
View File
@@ -0,0 +1,15 @@
var _ = require('lodash');
module.exports = function create_device_obscurity (env) {
function obscure_device (req, res, next) {
if (res.entries && env.settings.obscureDeviceProvenance) {
var entries = _.cloneDeep(res.entries);
for (var i = 0; i < entries.length; i++) {
entries[i].device = env.settings.obscureDeviceProvenance;
}
res.entries = entries;
}
next( );
}
return obscure_device;
}
+6
View File
@@ -185,6 +185,10 @@ function init (env, ctx) {
notifications.ack(1, group, time);
}
/*
* TODO: modify with a local clear, this will clear all connected clients,
* globally
*/
if (sendClear) {
var notify = {
clear: true
@@ -192,6 +196,8 @@ function init (env, ctx) {
, message: group + ' - ' + ctx.levels.toDisplay(level) + ' was ack\'d'
, group: group
};
// When web client sends ack, this translates the websocket message into
// an event on our internal bus.
ctx.bus.emit('notification', notify);
logEmitEvent(notify);
}
+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);
+43 -4
View File
@@ -24,6 +24,7 @@ function bridged (entries) {
mostRecentRecord = glucose[i].date;
}
}
//console.log("DEXCOM: Most recent entry received; "+new Date(mostRecentRecord).toString());
}
entries.create(glucose, function stored (err) {
if (err) {
@@ -46,12 +47,12 @@ function options (env) {
, minutes: env.extendedSettings.bridge.minutes || 1440
};
var interval = env.extendedSettings.bridge.interval || 60000 * 2.5; // Default: 2.5 minutes
var interval = env.extendedSettings.bridge.interval || 60000 * 2.6; // Default: 2.6 minutes
if (interval < 1000 || interval > 300000) {
// Invalid interval range. Revert to default
console.error("Invalid interval set: [" + interval + "ms]. Defaulting to 2.5 minutes.")
interval = 60000 * 2.5 // 2.5 minutes
console.error("Invalid interval set: [" + interval + "ms]. Defaulting to 2.6 minutes.")
interval = 60000 * 2.6 // 2.6 minutes
}
return {
@@ -75,15 +76,53 @@ function create (env, bus) {
bridge.startEngine = function startEngine (entries) {
opts.callback = bridged(entries);
let last_run = new Date(0).getTime();
let last_ondemand = new Date(0).getTime();
function should_run() {
// Time we expect to have to collect again
const msRUN_AFTER = (300+20) * 1000;
const msNow = new Date().getTime();
const next_entry_expected = mostRecentRecord + msRUN_AFTER;
if (next_entry_expected > msNow) {
// we're not due to collect a new slot yet. Use interval
const ms_since_last_run = msNow - last_run;
if (ms_since_last_run < interval) {
return false;
}
last_run = msNow;
last_ondemand = new Date(0).getTime();
console.log("DEXCOM: Running poll");
return true;
}
const ms_since_last_run = msNow - last_ondemand;
if (ms_since_last_run < interval) {
return false;
}
last_run = msNow;
last_ondemand = msNow;
console.log("DEXCOM: Data due, running extra poll");
return true;
}
let timer = setInterval(function () {
if (!should_run()) return;
opts.fetch.minutes = parseInt((new Date() - mostRecentRecord) / 60000);
opts.fetch.maxCount = parseInt((opts.fetch.minutes / 5) + 1);
opts.firstFetchCount = opts.fetch.maxCount;
console.log("Fetching Share Data: ", 'minutes', opts.fetch.minutes, 'maxCount', opts.fetch.maxCount);
engine(opts);
}, interval);
}, 1000 /*interval*/);
if (bus) {
bus.on('teardown', function serverTeardown () {
+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);
+16 -8
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
},
{
@@ -356,13 +356,21 @@ function init (ctx) {
function addLastEnacted () {
if (prop.lastEnacted) {
var canceled = prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0;
var valueParts = []
var valueParts = [
'<b>Temp Basal' + (canceled ? ' Canceled' : ' Started') + '</b>'
, canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + 'U/hour for ' + prop.lastEnacted.duration + 'm'
, valueString(', ', prop.lastEnacted.reason)
];
if (prop.lastEnacted.bolusVolume) {
valueParts.push('<b>Automatic Bolus</b>')
valueParts.push(' ' + prop.lastEnacted.bolusVolume + 'U')
if (prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0) {
valueParts.push(' (Temp Basal Canceled)')
}
} else if (prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0) {
valueParts.push('<b>Temp Basal Canceled</b>')
} else if (prop.lastEnacted.rate != null) {
valueParts.push('<b>Temp Basal Started</b>')
valueParts.push(' ' + prop.lastEnacted.rate.toFixed(2) + 'U/hour for ' + prop.lastEnacted.duration + 'm')
}
valueParts.push(valueString(', ', prop.lastEnacted.reason))
valueParts = concatIOB(valueParts);
valueParts = concatCOB(valueParts);
+9 -3
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'
@@ -392,7 +392,7 @@ function init (ctx) {
function addSuggestion () {
if (prop.lastSuggested) {
var bg = prop.lastSuggested.bg;
var units = sbx.data.profile.getUnits();
var units = sbx.settings.units;
if (units === 'mmol') {
bg = Math.round(bg / consts.MMOL_TO_MGDL * 10) / 10;
@@ -478,9 +478,15 @@ function init (ctx) {
if ('enacted' === prop.status.code) {
var canceled = prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0;
var bg = prop.lastEnacted.bg;
var units = sbx.settings.units;
if (units === 'mmol') {
bg = Math.round(bg / consts.MMOL_TO_MGDL * 10) / 10;
}
var valueParts = [
valueString('BG: ', prop.lastEnacted.bg)
valueString('BG: ', bg)
, ', <b>Temp Basal' + (canceled ? ' Canceled' : ' Started') + '</b>'
, canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + ' for ' + prop.lastEnacted.duration + 'm'
, valueString(', ', prop.lastEnacted.reason)
+9 -6
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);
@@ -236,11 +236,7 @@ function init (ctx) {
function updateReservoir (prefs, result) {
if (result.reservoir) {
result.reservoir.label = 'Reservoir';
if (result.reservoir_display_override) {
result.reservoir.display = result.reservoir_display_override;
} else {
result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
}
if (result.reservoir.value < prefs.urgentRes) {
result.reservoir.level = levels.URGENT;
result.reservoir.message = 'URGENT: Pump Reservoir Low';
@@ -250,11 +246,17 @@ function init (ctx) {
} else {
result.reservoir.level = levels.NONE;
}
} else if (result.manufacturer === 'Insulet' && result.model === 'Eros') {
} else if (result.manufacturer === 'Insulet') {
result.reservoir = {
label: 'Reservoir', display: '50+ U'
}
}
if (result.reservoir_display_override) {
result.reservoir.display = result.reservoir_display_override;
}
if (result.reservoir_level_override) {
result.reservoir.level = result.reservoir_level_override;
}
}
function updateBattery (type, prefs, result, batteryWarn) {
@@ -319,6 +321,7 @@ function init (ctx) {
, clock: pump.clock ? { value: moment(pump.clock) } : null
, reservoir: pump.reservoir || pump.reservoir === 0 ? { value: pump.reservoir } : null
, reservoir_display_override: pump.reservoir_display_override || null
, reservoir_level_override: pump.reservoir_level_override || null
, manufacturer: pump.manufacturer
, model: pump.model
, extended: pump.extended || null
+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;
+33 -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;
@@ -19,6 +18,7 @@ var init = function init () {
client.init(function loaded () {
console.log("LOADING CLIENT INIT");
if (c_profile !== null) {
return; // already loaded so don't load again
}
@@ -157,7 +157,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 +198,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 +313,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 +389,7 @@ var init = function init () {
newname += '1';
}
record.store[newname] = _.cloneDeep(record.store[currentprofile]);
currentprofile = newname;
dirty = true;
@@ -565,7 +565,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 +615,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 +656,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 +673,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 = '';
+3 -4
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);
@@ -235,10 +235,9 @@ function init () {
};
sbx.displayBg = function displayBg (entry) {
var isDex = entry && (!entry.device || entry.device === 'dexcom');
if (isDex && Number(entry.mgdl) === 39) {
if (Number(entry.mgdl) === 39) {
return 'LOW';
} else if (isDex && Number(entry.mgdl) === 401) {
} else if (Number(entry.mgdl) === 401) {
return 'HIGH';
} else {
return sbx.scaleEntry(entry);
+4 -15
View File
@@ -26,8 +26,6 @@ function create (env, ctx) {
var appInfo = env.name + ' ' + env.version;
app.set('title', appInfo);
app.enable('trust proxy'); // Allows req.secure test on heroku https connections.
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
var insecureUseHttp = env.insecureUseHttp;
var secureHstsHeader = env.secureHstsHeader;
if (!insecureUseHttp) {
@@ -186,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) {
@@ -247,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
@@ -322,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
});
@@ -345,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
, }));
+1 -1
View File
@@ -29,7 +29,7 @@ function bootError(env, ctx) {
return '<dt><b>' + obj.desc + '</b></dt><dd>' + message.replace(/\\n/g, '<br/>') + '</dd>';
}).join(' ');
res.render('error.html', {
res.status(500).render('error.html', {
errors,
locals
});
+39 -14
View File
@@ -9,6 +9,8 @@ function boot (env, language) {
console.log('Executing startBoot');
ctx.bootErrors = [ ];
ctx.moment = require('moment-timezone');
ctx.runtimeState = 'booting';
ctx.settings = env.settings;
ctx.bus = require('../bus')(env.settings, ctx);
@@ -23,7 +25,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) {
@@ -35,9 +38,9 @@ function boot (env, language) {
const isLTS = process.release.lts ? true : false;
if (isLTS && (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,20 +74,21 @@ function boot (env, language) {
var configURL = env.IMPORT_CONFIG || null;
var url = require('url');
var href = null;
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 {
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);
@@ -92,8 +96,13 @@ function boot (env, language) {
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( );
@@ -179,6 +188,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,8 +216,11 @@ function boot (env, language) {
settings: env.settings
, language: ctx.language
, levels: ctx.levels
, moment: ctx.moment
}).registerServerDefaults();
ctx.wares = require('../middleware/')(env);
ctx.pushover = require('../plugins/pushover')(env, ctx);
ctx.maker = require('../plugins/maker')(env);
ctx.pushnotify = require('./pushnotify')(env, ctx);
@@ -220,7 +233,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);
@@ -292,7 +305,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 ( ) {
@@ -304,6 +318,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');
@@ -315,6 +336,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( );
}
@@ -330,6 +352,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( );
}
@@ -341,6 +364,7 @@ function boot (env, language) {
if (hasBootErrors(ctx)) {
return next();
}
ctx.bus.emit('finishBoot');
ctx.runtimeState = 'booted';
ctx.bus.uptime( );
@@ -359,6 +383,7 @@ function boot (env, language) {
.acquire(setupInternals)
.acquire(ensureIndexes)
.acquire(setupListeners)
.acquire(setupConnect)
.acquire(setupBridge)
.acquire(setupMMConnect)
.acquire(finishBoot);
+10 -2
View File
@@ -29,6 +29,12 @@ 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;
@@ -39,13 +45,15 @@ function cache (env, ctx) {
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;
});
}
+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;
+5 -8
View File
@@ -99,8 +99,11 @@ 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();
@@ -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;
+38 -5
View File
@@ -1,6 +1,6 @@
//'use strict';
const apn = require('apn');
const apn = require('@parse/node-apn');
function init (env, ctx) {
@@ -81,6 +81,9 @@ function init (env, ctx) {
if (data.otp !== undefined && data.otp.length > 0) {
payload["otp"] = ""+data.otp
}
if (data.created_at !== undefined) {
payload['start-time'] = data.created_at;
}
alert = "Remote Carbs Entry: "+payload["carbs-entry"]+" grams\n";
alert += "Absorption Time: "+payload["absorption-time"]+" hours";
} else {
@@ -112,21 +115,51 @@ function init (env, ctx) {
alert += " - " + data.enteredBy
}
// Track time notification was sent
let now = new Date()
payload['sent-at'] = now.toISOString();
// Expire after 5 minutes.
let expiration = new Date(now.getTime() + 5 * 60 * 1000)
payload['expiration'] = expiration.toISOString();
let notification = new apn.Notification();
notification.alert = alert;
notification.topic = loopSettings.bundleIdentifier;
notification.contentAvailable = 1;
notification.expiry = Math.round((Date.now() / 1000)) + 60 * 5; // Allow this to enact within 5 minutes.
notification.payload = payload;
notification.interruptionLevel = "time-sensitive"
provider.send(notification, [loopSettings.deviceToken]).then( (response) => {
provider.send(notification, [loopSettings.deviceToken]).then((response) => {
if (response.sent && response.sent.length > 0) {
completion();
} else {
console.log("APNs delivery failed:", response.failed)
completion("APNs delivery failed: " + response.failed[0].response.reason);
console.log("APNs delivery failed:", response.failed);
// Check if response.failed and response.failed[0] are defined
if (response.failed && response.failed.length > 0) {
const failedResponse = response.failed[0];
const reason = failedResponse.response && failedResponse.response.reason
? failedResponse.response.reason
: 'Unknown reason';
// Provide detailed debugging information
const errorMessage = `APNs delivery failed: ${reason}`;
console.error(errorMessage, failedResponse);
completion(errorMessage);
} else {
// Handle the case where response.failed is undefined or empty
const errorMessage = 'APNs delivery failed: No failure details available.';
console.error(errorMessage, response);
completion(errorMessage);
}
}
}).catch((error) => {
// Catch any other unexpected errors
console.error('Unexpected error during APNs delivery:', error);
completion(`APNs delivery failed: ${error.message || 'Unknown error'}`);
});
};
return loop();
-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();
+1 -1
View File
@@ -881,7 +881,7 @@
"securitySchemes": {
"api_secret": {
"type": "apiKey",
"name": "api_secret",
"name": "api-secret",
"in": "header",
"description": "The hash of the API_SECRET env var"
},
+1 -1
View File
@@ -656,7 +656,7 @@ components:
securitySchemes:
api_secret:
type: apiKey
name: api_secret
name: api-secret
in: header
description: The hash of the API_SECRET env var
token_in_url:
+34 -49
View File
@@ -3,6 +3,12 @@
var times = require('../times');
var calcData = require('../data/calcdelta');
var ObjectID = require('mongodb').ObjectID;
const forwarded = require('forwarded-for');
function getRemoteIP (req) {
const address = forwarded(req, req.headers);
return address.ip;
}
function init (env, ctx, server) {
@@ -10,8 +16,6 @@ function init (env, ctx, server) {
return websocket;
}
var levels = ctx.levels;
//var log_yellow = '\x1B[33m';
var log_green = '\x1B[32m';
var log_magenta = '\x1B[35m';
@@ -68,13 +72,21 @@ 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
// compat with v2 client
allowEIO3: true
, '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 () {
@@ -83,6 +95,11 @@ function init (env, ctx, server) {
});
io.close();
});
ctx.bus.on('data-processed', function() {
update();
});
}
function verifyAuthorization (message, ip, callback) {
@@ -116,7 +133,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);
}
}
@@ -127,14 +144,10 @@ function init (env, ctx, server) {
var timeDiff;
var history;
var remoteIP = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress;
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);
@@ -177,7 +190,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);
});
@@ -217,7 +230,7 @@ function init (env, ctx, server) {
ctx.store.collection(collection).findOne({ '_id': id }
, function(err, results) {
console.log('Got results', results);
if (!err) {
if (!err && results !== null) {
ctx.bus.emit('data-update', {
type: data.collection
, op: 'update'
@@ -264,7 +277,7 @@ function init (env, ctx, server) {
ctx.store.collection(collection).findOne({ '_id': objId }
, function(err, results) {
console.log('Got results', results);
if (!err) {
if (!err && results !== null) {
ctx.bus.emit('data-update', {
type: data.collection
, op: 'update'
@@ -292,7 +305,7 @@ function init (env, ctx, server) {
socket.on('dbAdd', function dbAdd (data, callback) {
console.log(LOG_WS + 'dbAdd client ID: ', socket.client.id, ' data: ', data);
var collection = supportedCollections[data.collection];
var maxtimediff = times.mins(1).msecs;
var maxtimediff = times.secs(2).msecs;
var check = checkConditions('dbAdd', data);
if (check) {
@@ -523,7 +536,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) {
@@ -557,23 +570,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);
@@ -586,25 +586,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();
@@ -612,6 +593,10 @@ function init (env, ctx, server) {
ctx.storageSocket.init(io);
}
if (ctx.alarmSocket) {
ctx.alarmSocket.init(io);
}
return websocket();
}
+11
View File
@@ -70,6 +70,9 @@ function init () {
, frameName8: ''
, authFailDelay: 5000
, adminNotifiesEnabled: true
, obscured: ''
, obscureDeviceProvenance: ''
, authenticationPromptOnLoad: false
};
var secureSettings = [
@@ -78,6 +81,8 @@ function init () {
, 'developerTeamId'
, 'userName'
, 'password'
, 'obscured'
, 'obscureDeviceProvenance'
];
var valueMappers = {
@@ -107,6 +112,7 @@ function init () {
, bgTargetBottom: mapNumber
, authFailDelay: mapNumber
, adminNotifiesEnabled: mapTruthy
, authenticationPromptOnLoad: mapTruthy
};
function filterObj(obj, secureKeys) {
@@ -129,6 +135,9 @@ function init () {
function filteredSettings(settingsObject) {
let so = _.cloneDeep(settingsObject);
if (so.obscured) {
so.enable = _.difference(so.enable, so.obscured);
}
return filterObj(so, secureSettings);
}
@@ -244,6 +253,7 @@ function init () {
var enable = getAndPrepare('enable');
var disable = getAndPrepare('disable');
var obscured = getAndPrepare('obscured');
settings.alarmTypes = prepareAlarmTypes();
@@ -266,6 +276,7 @@ function init () {
//all enabled feature, without any that have been disabled
settings.enable = _.difference(enable, disable);
settings.obscured = obscured;
var thresholds = settings.thresholds;
+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);
+4232 -7192
View File
File diff suppressed because it is too large Load Diff
+39 -40
View File
@@ -1,6 +1,6 @@
{
"name": "nightscout",
"version": "14.2.3",
"version": "15.0.4",
"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",
@@ -27,16 +27,16 @@
},
"scripts": {
"start": "node lib/server/server.js",
"test": "env-cmd -f ./my.test.env mocha --require ./tests/hooks.js -exit ./tests/*.test.js",
"test-single": "env-cmd -f ./my.test.env mocha --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 --require ./tests/hooks.js --exit ./tests/*.test.js",
"test": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js -exit ./tests/*.test.js",
"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",
"coverage": "cat ./coverage/lcov.info | env-cmd -f ./tests/ci.test.env codacy-coverage",
"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",
"prod": "env-cmd -f ./my.prod.env node lib/server/server.js 0.0.0.0",
@@ -65,18 +65,17 @@
}
},
"engines": {
"node": "^10.22.0 || ^12.18.4",
"npm": "^6.14.6"
"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",
@@ -89,74 +88,74 @@
"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",
"flot": "^0.8.3",
"forwarded-for": "^1.1.0",
"helmet": "^4.0.0",
"jquery": "^3.5.1",
"jquery-ui-bundle": "^1.12.1-migrate",
"jquery.tooltips": "^1.0.0",
"js-storage": "^1.1.0",
"jsdom": "^11.11.0",
"jsonwebtoken": "^8.5.1",
"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.0",
"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",
"moment-timezone-data-webpack-plugin": "^1.5.0",
"mongo-url-parser": "^1.0.2",
"mongodb": "^3.6.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",
"request": "^2.88.2",
"semver": "^6.3.0",
"share2nightscout-bridge": "^0.2.4",
"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"
}
}
+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.$ = $;
+48 -6
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,17 +28,38 @@ 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) {
@@ -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)
+15 -1
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')
+42 -37
View File
@@ -29,7 +29,7 @@ describe('API3 CREATE', function() {
* Cleanup after successful creation
*/
self.delete = async function deletePermanent (identifier) {
let res = await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`)
let res = await self.instance.delete(`${self.url}/${identifier}?permanent=true`, self.jwt.delete)
.expect(200);
res.body.status.should.equal(200);
@@ -40,7 +40,7 @@ describe('API3 CREATE', function() {
* Get document detail for futher processing
*/
self.get = async function get (identifier) {
let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`)
let res = await self.instance.get(`${self.url}/${identifier}`, self.jwt.read)
.expect(200);
res.body.status.should.equal(200);
@@ -52,7 +52,7 @@ describe('API3 CREATE', function() {
* Get document detail for futher processing
*/
self.search = async function search (date) {
let res = await self.instance.get(`${self.url}?date$eq=${date}&token=${self.token.read}`)
let res = await self.instance.get(`${self.url}?date$eq=${date}`, self.jwt.read)
.expect(200);
res.body.status.should.equal(200);
@@ -68,11 +68,16 @@ describe('API3 CREATE', function() {
self.col = 'treatments'
self.url = `/api/v3/${self.col}`;
let authResult = await authSubject(self.instance.ctx.authorization.storage);
let authResult = await authSubject(self.instance.ctx.authorization.storage, [
'create',
'update',
'read',
'delete',
'all'
], self.instance.app);
self.subject = authResult.subject;
self.token = authResult.token;
self.urlToken = `${self.url}?token=${self.token.create}`;
self.jwt = authResult.jwt;
self.cache = self.instance.cacheMonitor;
});
@@ -103,7 +108,7 @@ describe('API3 CREATE', function() {
it('should not found not existing collection', async () => {
let res = await self.instance.post(`/api/v3/NOT_EXIST?token=${self.url}`)
let res = await self.instance.post(`/api/v3/NOT_EXIST`, self.jwt.create)
.send(self.validDoc)
.expect(404);
@@ -113,7 +118,7 @@ describe('API3 CREATE', function() {
it('should require create permission', async () => {
let res = await self.instance.post(`${self.url}?token=${self.token.read}`)
let res = await self.instance.post(`${self.url}`, self.jwt.read)
.send(self.validDoc)
.expect(403);
@@ -123,7 +128,7 @@ describe('API3 CREATE', function() {
it('should reject empty body', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send({ })
.expect(400);
@@ -132,7 +137,7 @@ describe('API3 CREATE', function() {
it('should accept valid document', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(self.validDoc)
.expect(201);
@@ -161,7 +166,7 @@ describe('API3 CREATE', function() {
let doc = Object.assign({}, self.validDoc);
delete doc.date;
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(doc)
.expect(400);
@@ -171,7 +176,7 @@ describe('API3 CREATE', function() {
it('should reject invalid date null', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { date: null }))
.expect(400);
@@ -181,7 +186,7 @@ describe('API3 CREATE', function() {
it('should reject invalid date ABC', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { date: 'ABC' }))
.expect(400);
@@ -191,7 +196,7 @@ describe('API3 CREATE', function() {
it('should reject invalid date -1', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { date: -1 }))
.expect(400);
@@ -202,7 +207,7 @@ describe('API3 CREATE', function() {
it('should reject invalid date 1 (too old)', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { date: 1 }))
.expect(400);
@@ -212,7 +217,7 @@ describe('API3 CREATE', function() {
it('should reject invalid date - illegal format', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { date: '2019-20-60T50:90:90' }))
.expect(400);
@@ -222,7 +227,7 @@ describe('API3 CREATE', function() {
it('should reject invalid utcOffset -5000', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { utcOffset: -5000 }))
.expect(400);
@@ -232,7 +237,7 @@ describe('API3 CREATE', function() {
it('should reject invalid utcOffset ABC', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { utcOffset: 'ABC' }))
.expect(400);
@@ -244,7 +249,7 @@ describe('API3 CREATE', function() {
it('should accept valid utcOffset', async () => {
const doc = Object.assign({}, self.validDoc, { utcOffset: 120 });
await self.instance.post(self.urlToken)
await self.instance.post(self.url, self.jwt.create)
.send(doc)
.expect(201);
@@ -258,7 +263,7 @@ describe('API3 CREATE', function() {
it('should reject invalid utcOffset null', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { utcOffset: null }))
.expect(400);
@@ -271,7 +276,7 @@ describe('API3 CREATE', function() {
let doc = Object.assign({}, self.validDoc);
delete doc.app;
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(doc)
.expect(400);
@@ -281,7 +286,7 @@ describe('API3 CREATE', function() {
it('should reject invalid app null', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { app: null }))
.expect(400);
@@ -291,7 +296,7 @@ describe('API3 CREATE', function() {
it('should reject empty app', async () => {
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { app: '' }))
.expect(400);
@@ -301,7 +306,7 @@ describe('API3 CREATE', function() {
it('should normalize date and store utcOffset', async () => {
await self.instance.post(self.urlToken)
await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { date: '2019-06-10T08:07:08,576+02:00' }))
.expect(201);
@@ -321,7 +326,7 @@ describe('API3 CREATE', function() {
const doc = Object.assign({}, self.validDoc);
await self.instance.post(self.urlToken)
await self.instance.post(self.url, self.jwt.create)
.send(doc)
.expect(201);
@@ -330,7 +335,7 @@ describe('API3 CREATE', function() {
self.cache.nextShouldEql(self.col, doc)
const doc2 = Object.assign({}, doc);
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(doc2)
.expect(403);
@@ -347,7 +352,7 @@ describe('API3 CREATE', function() {
const doc = Object.assign({}, self.validDoc);
await self.instance.post(self.urlToken)
await self.instance.post(self.url, self.jwt.create)
.send(doc)
.expect(201);
@@ -359,7 +364,7 @@ describe('API3 CREATE', function() {
insulin: 0.5
});
let resPost2 = await self.instance.post(`${self.url}?token=${self.token.all}`)
let resPost2 = await self.instance.post(`${self.url}`, self.jwt.all)
.send(doc2)
.expect(200);
@@ -399,7 +404,7 @@ describe('API3 CREATE', function() {
});
delete doc2._id; // APIv1 updates input document, we must get rid of _id for the next round
const resPost2 = await self.instance.post(`${self.url}?token=${self.token.all}`)
const resPost2 = await self.instance.post(`${self.url}`, self.jwt.all)
.send(doc2)
.expect(200);
@@ -447,7 +452,7 @@ describe('API3 CREATE', function() {
identifier: utils.randomString('32', 'aA#')
});
await self.instance.post(`${self.url}?token=${self.token.all}`)
await self.instance.post(`${self.url}`, self.jwt.all)
.send(doc2)
.expect(201);
@@ -471,18 +476,18 @@ describe('API3 CREATE', function() {
, identifier = utils.randomString('32', 'aA#')
, doc = Object.assign({}, self.validDoc, { identifier, date: date1.toISOString() });
await self.instance.post(self.urlToken)
await self.instance.post(self.url, self.jwt.create)
.send(doc)
.expect(201);
self.cache.nextShouldEql(self.col, Object.assign({}, doc, { date: date1.getTime() }));
let res = await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`)
let res = await self.instance.delete(`${self.url}/${identifier}`, self.jwt.delete)
.expect(200);
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
const date2 = new Date();
res = await self.instance.post(self.urlToken)
res = await self.instance.post(self.url, self.jwt.create)
.send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() }))
.expect(403);
@@ -491,7 +496,7 @@ describe('API3 CREATE', function() {
self.cache.shouldBeEmpty()
const doc2 = Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() });
res = await self.instance.post(`${self.url}?token=${self.token.all}`)
res = await self.instance.post(`${self.url}`, self.jwt.all)
.send(doc2)
.expect(200);
@@ -513,7 +518,7 @@ describe('API3 CREATE', function() {
delete self.validDoc.identifier;
const validIdentifier = opTools.calculateIdentifier(self.validDoc);
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(self.validDoc)
.expect(201);
@@ -536,7 +541,7 @@ describe('API3 CREATE', function() {
delete self.validDoc.identifier;
const validIdentifier = opTools.calculateIdentifier(self.validDoc);
let res = await self.instance.post(self.urlToken)
let res = await self.instance.post(self.url, self.jwt.create)
.send(self.validDoc)
.expect(201);
@@ -550,7 +555,7 @@ describe('API3 CREATE', function() {
self.cache.nextShouldEql(self.col, self.validDoc);
delete self.validDoc.identifier;
res = await self.instance.post(`${self.url}?token=${self.token.update}`)
res = await self.instance.post(`${self.url}`, self.jwt.update)
.send(self.validDoc)
.expect(200);

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