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 ] branches: [ dev ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ master ] branches: [ dev ]
schedule: schedule:
- cron: '43 23 * * 3' - cron: '43 23 * * 3'
@@ -37,11 +37,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -66,4 +66,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - 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 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x] node-version: [14.x, 16.x, 20, lts/*]
mongodb-version: [4.2, 4.4] mongodb-version: [4.4, 5.0, 6.0]
steps: steps:
- name: Git Checkout - name: Git Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@@ -40,14 +40,19 @@ jobs:
- name: Send Coverage - name: Send Coverage
run: npm run-script coverage run: npm run-script coverage
publish_dev: publish:
name: Publish dev branch to Docker Hub name: Publish to Docker Hub
needs: test needs: test
runs-on: ubuntu-latest 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: env:
DOCKER_IMAGE: nightscout/cgm-remote-monitor DOCKER_IMAGE: nightscout/cgm-remote-monitor
PLATFORMS: linux/amd64,linux/arm64
steps: 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 - name: Login to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
@@ -55,39 +60,31 @@ jobs:
password: ${{ secrets.DOCKER_PASS }} password: ${{ secrets.DOCKER_PASS }}
- name: Clean git Checkout - name: Clean git Checkout
if: success() if: success()
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Build, tag and push the dev Docker image - name: Build, tag and push the dev Docker image
if: success() if: success() && github.ref == 'refs/heads/dev'
run: | uses: docker/build-push-action@v2
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
with: with:
username: ${{ secrets.DOCKER_USER }} context: .
password: ${{ secrets.DOCKER_PASS }} push: true
- name: Clean git Checkout no-cache: true
if: success() platforms: ${{ env.PLATFORMS }}
uses: actions/checkout@v2 tags: |
- name: get-npm-version ${{ env.DOCKER_IMAGE }}:dev_${{ github.sha }}
if: success() ${{ env.DOCKER_IMAGE }}:latest_dev
- name: Get Nightscout release version
if: success() && github.ref == 'refs/heads/master'
id: package-version id: package-version
uses: martinbeentjes/npm-get-version-action@master uses: martinbeentjes/npm-get-version-action@master
- name: Build, tag and push the master Docker image - name: Build, tag and push the master Docker image
if: success() if: success() && github.ref == 'refs/heads/master'
run: | uses: docker/build-push-action@v2
docker build --no-cache=true -t ${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }} . with:
docker image push ${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }} context: .
docker tag ${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }} ${{ env.DOCKER_IMAGE }}:latest push: true
docker image push ${{ env.DOCKER_IMAGE }}:latest 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" LABEL maintainer="Nightscout Contributors"
RUN mkdir -p /opt/app
ADD . /opt/app
WORKDIR /opt/app WORKDIR /opt/app
RUN chown -R node:node /opt/app ADD . /opt/app
USER node
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 postinstall && \
npm run env && \ 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 EXPOSE 1337
CMD ["node", "lib/server/server.js"] CMD ["node", "lib/server/server.js"]
+133 -33
View File
@@ -1,5 +1,5 @@
Nightscout Web Monitor (a.k.a. cgm-remote-monitor) Nightscout Web Monitor (a.k.a. cgm-remote-monitor)
====================================== ==================================================
![nightscout horizontal](https://cloud.githubusercontent.com/assets/751143/8425633/93c94dc0-1ebc-11e5-99e7-71a8f464caac.png) ![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] [![Codacy Badge][codacy-img]][codacy-url]
[![Discord chat][discord-img]][discord-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 This acts as a web-based CGM (Continuous Glucose Monitor) to allow
multiple caregivers to remotely view a patient's glucose data in multiple caregivers to remotely view a patient's glucose data in
real time. The server reads a MongoDB which is intended to be data 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) - [`treatmentnotify` (Treatment Notifications)](#treatmentnotify-treatment-notifications)
- [`basal` (Basal Profile)](#basal-basal-profile) - [`basal` (Basal Profile)](#basal-basal-profile)
- [`bolus` (Bolus Rendering)](#bolus-bolus-rendering) - [`bolus` (Bolus Rendering)](#bolus-bolus-rendering)
- [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge) - [`connect` (Nightscout Connect)](#connect-nightscout-connect)
- [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge) - [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge), _deprecated_
- [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge), _deprecated_
- [`pump` (Pump Monitoring)](#pump-pump-monitoring) - [`pump` (Pump Monitoring)](#pump-pump-monitoring)
- [`openaps` (OpenAPS)](#openaps-openaps) - [`openaps` (OpenAPS)](#openaps-openaps)
- [`loop` (Loop)](#loop-loop) - [`loop` (Loop)](#loop-loop)
- [`override` (Override Mode)](#override-override-mode) - [`override` (Override Mode)](#override-override-mode)
- [`xdripjs` (xDrip-js)](#xdripjs-xdrip-js) - [`xdripjs` (xDrip-js)](#xdripjs-xdrip-js)
- [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) - [`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) - [`speech` (Speech)](#speech-speech)
- [`cors` (CORS)](#cors-cors) - [`cors` (CORS)](#cors-cors)
- [Extended Settings](#extended-settings) - [Extended Settings](#extended-settings)
@@ -132,40 +131,38 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
## Supported configurations: ## 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. - [Nightscout Setup](https://nightscout.github.io/nightscout/new_user/) (recommended)
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)
While you can install Nightscout on a virtual server or a Raspberry Pi, we do not recommend this unless you have at least some 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 experience hosting Node applications and development using the toolchain in use with Nightscout.
hosting for you and even many of the dvelopers run their production sites in Heroku due to convenience.
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. 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: ## 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 - Android Chrome: 104 or later (`and_chr`)
- iOS 6 - Google Chrome: 101 or later (`chrome`)
- Chrome 35 - Microsoft Edge: 103 or later (`edge`)
- Edge 17 - Mozilla Firefox: 102 or later (`firefox`)
- Firefox 61 - Apple Safari on iOS: 15.5 or later (`ios_saf`)
- Opera 12.1 - Opera Mini on Android: 63 or later (`op_mini`)
- Safari 6 (macOS 10.7) - Opera: 88 or later (`opera`)
- Internet Explorer: not supported - 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: 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: - 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 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. - 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` * 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. 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 ## 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}}}` * `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` * `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 ### 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. 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` (`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). * `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) ##### `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_USER_NAME` - Your username for the Share service.
* `BRIDGE_PASSWORD` - Your password 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. * `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. * `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) ##### `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)) 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_USER_NAME` - Your user name for CareLink Connect.
* `MMCONNECT_PASSWORD` - Your password 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) ##### `alexa` (Amazon Alexa)
Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md) 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) 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` (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. 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')", "description": "Default setting for new browser views, for the time mode. ('12' or '24')",
"value": "12", "value": "12",
"required": false "required": false
},
"USE_NPM_INSTALL": {
"description": "You need to have this set for deployment to work in Heroku",
"value": "true",
"required": true
} }
}, },
"addons": [ "addons": [
+1 -1
View File
@@ -218,7 +218,7 @@
}, },
"WEBSITE_NODE_DEFAULT_VERSION": { "WEBSITE_NODE_DEFAULT_VERSION": {
"type": "string", "type": "string",
"defaultValue": "8.11.1" "defaultValue": "16.16.0"
} }
}, },
"resources": [{ "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.pie');
require('../node_modules/flot/jquery.flot.fillbetween'); require('../node_modules/flot/jquery.flot.fillbetween');
window.moment = require('moment-timezone'); const moment = require('moment-timezone');
window.moment = moment;
window.Nightscout = window.Nightscout || {}; window.Nightscout = window.Nightscout || {};
var ctx = {
moment: moment
};
window.Nightscout = { window.Nightscout = {
client: require('../lib/client'), client: require('../lib/client'),
units: require('../lib/units')(), 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/'); window.Nightscout.report_plugins_preinit = require('../lib/report_plugins/');
+10
View File
@@ -1,11 +1,19 @@
version: '3' version: '3'
x-logging:
&default-logging
options:
max-size: '10m'
max-file: '5'
driver: json-file
services: services:
mongo: mongo:
image: mongo:4.4 image: mongo:4.4
restart: always restart: always
volumes: volumes:
- ${NS_MONGO_DATA_DIR:-./mongo-data}:/data/db:cached - ${NS_MONGO_DATA_DIR:-./mongo-data}:/data/db:cached
logging: *default-logging
nightscout: nightscout:
image: nightscout/cgm-remote-monitor:latest image: nightscout/cgm-remote-monitor:latest
@@ -20,6 +28,7 @@ services:
- 'traefik.http.routers.nightscout.rule=Host(`localhost`)' - 'traefik.http.routers.nightscout.rule=Host(`localhost`)'
- 'traefik.http.routers.nightscout.entrypoints=websecure' - 'traefik.http.routers.nightscout.entrypoints=websecure'
- 'traefik.http.routers.nightscout.tls.certresolver=le' - 'traefik.http.routers.nightscout.tls.certresolver=le'
logging: *default-logging
environment: environment:
### Variables for the container ### Variables for the container
NODE_ENV: production NODE_ENV: production
@@ -76,3 +85,4 @@ services:
volumes: volumes:
- './letsencrypt:/letsencrypt' - './letsencrypt:/letsencrypt'
- '/var/run/docker.sock:/var/run/docker.sock:ro' - '/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 --> <!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+3 -2
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
var moment = require('moment'); var moment;
var cleanentriesdb = { var cleanentriesdb = {
name: 'cleanentriesdb' name: 'cleanentriesdb'
@@ -8,7 +8,8 @@ var cleanentriesdb = {
, pluginType: 'admin' , pluginType: 'admin'
}; };
function init() { function init(ctx) {
moment = ctx.moment;
return cleanentriesdb; return cleanentriesdb;
} }
+3 -2
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
var moment = require('moment'); var moment;
var cleanstatusdb = { var cleanstatusdb = {
name: 'cleanstatusdb' name: 'cleanstatusdb'
@@ -8,7 +8,8 @@ var cleanstatusdb = {
, pluginType: 'admin' , pluginType: 'admin'
}; };
function init () { function init (ctx) {
moment = ctx.moment;
return cleanstatusdb; return cleanstatusdb;
} }
+3 -2
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
var moment = require('moment'); var moment;
var cleantreatmentsdb = { var cleantreatmentsdb = {
name: 'cleantreatmentsdb' name: 'cleantreatmentsdb'
@@ -8,7 +8,8 @@ var cleantreatmentsdb = {
, pluginType: 'admin' , pluginType: 'admin'
}; };
function init() { function init(ctx) {
moment = ctx.moment;
return cleantreatmentsdb; return cleantreatmentsdb;
} }
+7 -7
View File
@@ -3,14 +3,14 @@
var _find = require('lodash/find'); var _find = require('lodash/find');
var _each = require('lodash/each'); var _each = require('lodash/each');
function init() { function init(ctx) {
var allPlugins = [ var allPlugins = [
require('./subjects')() require('./subjects')(ctx)
, require('./roles')() , require('./roles')(ctx)
, require('./cleanstatusdb')() , require('./cleanstatusdb')(ctx)
, require('./cleantreatmentsdb')() , require('./cleantreatmentsdb')(ctx)
, require('./cleanentriesdb')() , require('./cleanentriesdb')(ctx)
, require('./futureitems')() , require('./futureitems')(ctx)
]; ];
function plugins(name) { function plugins(name) {
+4 -15
View File
@@ -12,23 +12,14 @@ function configure(app, wares, ctx) {
, api = express.Router(); , api = express.Router();
api.use(wares.compression()); api.use(wares.compression());
api.use(wares.bodyParser({
limit: 1048576 * 50
}));
// text body types get handled as raw buffer stream // text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw({ api.use(wares.rawParser);
limit: 1048576
}));
// json body types get handled as parsed json // json body types get handled as parsed json
api.use(wares.bodyParser.json({ api.use(wares.bodyParser.json({
limit: 1048576 limit: '50Mb'
, extended: true
})); }));
// also support url-encoded content-type // also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ api.use(wares.urlencodedParser);
limit: 1048576
, extended: true
}));
// invoke common middleware // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
@@ -94,9 +85,7 @@ function configure(app, wares, ctx) {
}); });
} }
api.post('/activity/', wares.bodyParser({ api.post('/activity/', ctx.authorization.isPermitted('api:activity:create'), post_response);
limit: 1048576 * 50
}), ctx.authorization.isPermitted('api:activity:create'), post_response);
api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) { api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) {
ctx.activity.remove(req.params._id, function() { ctx.activity.remove(req.params._id, function() {
+6 -13
View File
@@ -11,12 +11,12 @@ function configure (app, wares, ctx, env) {
// invoke common middleware // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream // 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 // json body types get handled as parsed json
api.use(wares.bodyParser.json({ api.use(wares.jsonParser);
limit: 1048576 // also support url-encoded content-type
, extended: true api.use(wares.urlencodedParser);
})); // text body types get handled as raw buffer stream
ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa); ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa);
@@ -94,7 +94,7 @@ function configure (app, wares, ctx, env) {
var handler = ctx.alexa.getIntentHandler(intentName, metric); var handler = ctx.alexa.getIntentHandler(intentName, metric);
if (handler){ if (handler){
var sbx = initializeSandbox(); var sbx = ctx.sbx;
handler(next, slots, sbx); handler(next, slots, sbx);
return; return;
} else { } 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; return api;
} }
+4 -6
View File
@@ -13,14 +13,12 @@ function configure (app, wares, ctx, env) {
// invoke common middleware // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream // 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 // json body types get handled as parsed json
api.use(wares.bodyParser.json({ api.use(wares.jsonParser);
limit: 1048576
, extended: true
}));
// also support url-encoded content-type // 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')); 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 // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream // 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 // json body types get handled as parsed json
api.use(wares.bodyParser.json({ api.use(wares.bodyParser.json({
limit: 1048576 limit: '50Mb'
, extended: true
})); }));
// 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 // shortcut to use extension to specify output content-type
api.use(wares.extensions([ api.use(wares.extensions([
'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv' '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')); api.use(ctx.authorization.isPermitted('api:entries:read'));
/** /**
@@ -357,7 +355,7 @@ function configure (app, wares, ctx, env) {
res.entries_err = err; res.entries_err = err;
return next(); return next();
}); });
}, format_entries); }, wares.obscure_device, format_entries);
/** /**
* @module get#/entries/:spec * @module get#/entries/:spec
@@ -391,7 +389,7 @@ function configure (app, wares, ctx, env) {
prepReqModel(req, req.params.model); prepReqModel(req, req.params.model);
query_models(req, res, next); query_models(req, res, next);
} }
}, format_entries); }, wares.obscure_device, format_entries);
/** /**
* @module get#/entries * @module get#/entries
@@ -402,7 +400,7 @@ function configure (app, wares, ctx, env) {
* `find[date]`. * `find[date]`.
* *
*/ */
api.get('/entries', ifModifiedSinceCTX, query_models, format_entries); api.get('/entries', ifModifiedSinceCTX, query_models, wares.obscure_device, format_entries);
/** /**
* @function echo_query * @function echo_query
@@ -474,14 +472,16 @@ function configure (app, wares, ctx, env) {
}); });
} else { } else {
inMemoryCollection = ctx.cache.getData('entries'); inMemoryCollection = ctx.cache.getData('entries');
inMemoryCollection = _.sortBy(inMemoryCollection, function(item) {
return item.mills;
}).reverse();
} }
if (inMemoryPossible && query.count <= inMemoryCollection.length) { if (inMemoryPossible && query.count <= inMemoryCollection.length) {
res.entries = _.cloneDeep(_.take(inMemoryCollection,query.count)); 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; res.entries_err = null;
return next(); return next();
} }
@@ -740,7 +740,7 @@ function configure (app, wares, ctx, env) {
* @routed * @routed
* @response 200 /definitions/Entries * @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); 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/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 * @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 // setting this flag tells insert_entries to not actually store the results
req.persist_entries = false; req.persist_entries = false;
next(); next();
}, insert_entries, format_entries); }, insert_entries, wares.obscure_device, format_entries);
// Protect endpoints with authenticated api. // Protect endpoints with authenticated api.
if (app.enabled('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 // setting this flag tells insert_entries to store the results
req.persist_entries = true; req.persist_entries = true;
next(); next();
}, insert_entries, format_entries); }, insert_entries, wares.obscure_device, format_entries);
/** /**
* @module delete#/entries/:spec * @module delete#/entries/:spec
+5 -6
View File
@@ -9,14 +9,13 @@ function configure (app, wares, ctx) {
// invoke common middleware // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream // 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 // json body types get handled as parsed json
api.use(wares.bodyParser.json({ api.use(wares.jsonParser);
limit: 1048576
, extended: true
}));
// also support url-encoded content-type // 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')); 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 // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream // 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 // json body types get handled as parsed json
api.use(wares.bodyParser.json()); api.use(wares.jsonParser);
ctx.virtAsstBase.setupVirtAsstHandlers(ctx.googleHome); 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); var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters.metric);
if (handler){ if (handler){
var sbx = initializeSandbox(); var sbx = ctx.sbx;
handler(function (title, response) { handler(function (title, response) {
res.json(ctx.googleHome.buildSpeechletResponse(response, false)); res.json(ctx.googleHome.buildSpeechletResponse(response, false));
next( ); next( );
@@ -45,13 +46,6 @@ function configure (app, wares, ctx, env) {
ctx.virtAsstBase.setupMutualIntents(ctx.googleHome); ctx.virtAsstBase.setupMutualIntents(ctx.googleHome);
function initializeSandbox() {
var sbx = require('../../sandbox')();
sbx.serverInit(env, ctx);
ctx.plugins.setProperties(sbx);
return sbx;
}
return api; return api;
} }
+1 -1
View File
@@ -6,7 +6,7 @@ function create (env, ctx) {
, app = express( ) , app = express( )
; ;
var wares = require('../middleware/')(env); const wares = ctx.wares;
// set up express app with our options // set up express app with our options
app.set('name', env.name); app.set('name', env.name);
+4
View File
@@ -1,12 +1,16 @@
'use strict'; 'use strict';
var consts = require('../constants'); var consts = require('../constants');
var bodyParser = require('body-parser');
function configure (app, wares, ctx) { function configure (app, wares, ctx) {
var express = require('express') var express = require('express')
, api = express.Router( ) , api = express.Router( )
; ;
app.use(bodyParser.urlencoded({extended : true}));
app.use(bodyParser.json());
api.post('/notifications/pushovercallback', function (req, res) { api.post('/notifications/pushovercallback', function (req, res) {
if (ctx.pushnotify.pushoverAck(req.body)) { if (ctx.pushnotify.pushoverAck(req.body)) {
res.sendStatus(consts.HTTP_OK); res.sendStatus(consts.HTTP_OK);
+4 -6
View File
@@ -9,14 +9,12 @@ function configure (app, wares, ctx) {
// invoke common middleware // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
// text body types get handled as raw buffer stream // 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 // json body types get handled as parsed json
api.use(wares.bodyParser.json({ api.use(wares.jsonParser);
limit: 1048576
, extended: true
}));
// also support url-encoded content-type // 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')); api.use(ctx.authorization.isPermitted('api:profile:read'));
+3 -1
View File
@@ -2,6 +2,7 @@
function configure (app, wares, env, ctx) { function configure (app, wares, env, ctx) {
var express = require('express'), var express = require('express'),
forwarded = require('forwarded-for'),
api = express.Router( ) api = express.Router( )
; ;
@@ -21,7 +22,8 @@ function configure (app, wares, env, ctx) {
var authToken = req.query.token || req.query.secret || ''; var authToken = req.query.token || req.query.secret || '';
function getRemoteIP (req) { 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(); var date = new Date();
+18 -26
View File
@@ -13,24 +13,16 @@ function configure (app, wares, ctx, env) {
, api = express.Router(); , api = express.Router();
api.use(wares.compression()); api.use(wares.compression());
api.use(wares.bodyParser({
limit: 1048576 * 50
, extended: true
}));
// text body types get handled as raw buffer stream // text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw({ api.use(wares.rawParser);
limit: 1048576
}));
// json body types get handled as parsed json // json body types get handled as parsed json
api.use(wares.bodyParser.json({ api.use(wares.bodyParser.json({
limit: 1048576 limit: '50Mb'
, extended: true
})); }));
// also support url-encoded content-type // also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ api.use(wares.urlencodedParser);
limit: 1048576
, extended: true
}));
// invoke common middleware // invoke common middleware
api.use(wares.sendJSONStatus); api.use(wares.sendJSONStatus);
@@ -71,18 +63,20 @@ function configure (app, wares, ctx, env) {
} }
}); });
if (!_isNil(d1)) res.setHeader('Last-Modified', d1.toUTCString()); if (!_isNil(d1)) {
res.setHeader('Last-Modified', d1.toUTCString());
if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) { if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) {
res.status(304).send({ res.status(304).send({
status: 304 status: 304
, message: 'Not modified' , message: 'Not modified'
, type: 'internal' , type: 'internal'
}); });
return; return;
} else { }
return res.json(results);
} }
return res.json(results);
} }
// List treatments available // List treatments available
@@ -150,9 +144,7 @@ function configure (app, wares, ctx, env) {
}); });
} }
api.post('/treatments/', wares.bodyParser({ api.post('/treatments/', ctx.authorization.isPermitted('api:treatments:create'), post_response);
limit: 1048576 * 50
}), ctx.authorization.isPermitted('api:treatments:create'), post_response);
/** /**
* @function delete_records * @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 = 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) { api.post('/loop', ctx.authorization.isPermitted('notifications:loop:push'), function (req, res) {
ctx.loop.sendNotification(req.body, req.connection.remoteAddress, function (error) { ctx.loop.sendNotification(req.body, req.connection.remoteAddress, function (error) {
if (error) { if (error) {
@@ -18,11 +18,11 @@ function create (env, ctx) {
* *
* Expecting to define extended syntax and support for several query params * 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) { properties.get(['/', '/*'], function getProperties (req, res) {
var sbx = sandbox.serverInit(env, ctx); if (!ctx.sbx) res.json({});
ctx.plugins.setProperties(sbx);
function notEmpty (part) { function notEmpty (part) {
return ! _isEmpty(part); return ! _isEmpty(part);
@@ -36,10 +36,10 @@ function create (env, ctx) {
selected = _filter(segments[0].split(','), notEmpty); selected = _filter(segments[0].split(','), notEmpty);
} }
var result = sbx.properties; var result = ctx.sbx.properties;
if (selected.length > 0) { if (selected.length > 0) {
result = _pick(sbx.properties, selected); result = _pick(ctx.sbx.properties, selected);
} }
result = env.settings.filteredSettings(result); result = env.settings.filteredSettings(result);
@@ -57,4 +57,4 @@ function create (env, ctx) {
return properties; return properties;
} }
module.exports = create; module.exports = create;
+139
View File
@@ -0,0 +1,139 @@
const { data } = require("jquery");
const dataProcessor = {};
function _hhmmAfter (hhmm, mills) {
var date = new Date(mills);
var withSameDate = new Date(
1900 + date.getYear()
, date.getMonth()
, date.getDate()
, parseInt(hhmm.substr(0, 2), 10)
, parseInt(hhmm.substr(3, 5), 10)
).getTime();
return withSameDate > date ? withSameDate : withSameDate + 24 * 60 * 60 * 1000;
}
// Outputs temp basal objects describing the profile temps for the duration
function _profileBasalsInWindow (basals, start, end) {
if (basals.length === 0) {
return [];
}
var i;
var out = [];
function nextProfileBasal () {
i = (i + 1) % basals.length;
var lastStart = out[out.length - 1].start;
return {
start: _hhmmAfter(basals[i]['time'], lastStart)
, absolute: parseFloat(basals[i]['value'])
, profile: 1
};
}
i = 0;
var startHHMM = new Date(start).toTimeString().substr(0, 5);
while (i < basals.length - 1 && basals[i + 1]['time'] <= startHHMM) {
i++;
}
out.push({
start: start
, absolute: parseFloat(basals[i]['value'])
, });
var next = nextProfileBasal();
while (next.start < end) {
out.push(next);
next = nextProfileBasal();
}
return out;
}
dataProcessor.filterSameAbsTemps = function filterSameAbsTemps (tempdata) {
var out = [];
var j = 0;
for (let i = 0; i < tempdata.length; i++) {
const temp = tempdata[i];
if (i == tempdata.length - 1) {
// If last was merged, skip
if (j != i) {
out.push(temp);
}
break;
}
const nextTemp = tempdata[i + 1];
if (temp.duration && (temp.start + temp.duration) >= nextTemp.start) {
if (temp.absolute == nextTemp.absolute) {
// Merge and skip next
temp.duration = nextTemp.start - temp.start + nextTemp.duration;
i += 1;
j = i;
} else {
// Adjust duration
temp.duration = nextTemp.start - temp.start;
}
}
out.push(temp);
}
return out;
}
dataProcessor.processTempBasals = function processTempBasals (profile, tempBasals, dataCap) {
var profileBasals = profile.basal;
var temps = tempBasals.map(function(temp) {
return {
start: new Date(temp['created_at']).getTime()
, duration: temp['duration'] === undefined ? 0 : parseInt(temp['duration'], 10) * 60 * 1000
, absolute: temp['absolute'] === undefined ? 0 : parseFloat(temp['absolute'])
};
}).concat([
{ start: Date.now() - 24 * 60 * 60 * 1000, duration: 0 }
, { start: Date.now(), duration: 0}
]).sort(function(a, b) {
return a.start - b.start;
});
var out = [];
temps.forEach(function(temp) {
var last = out[out.length - 1];
if (last && last.duration !== undefined && last.start + last.duration < temp.start) {
Array.prototype.push.apply(out, _profileBasalsInWindow(profileBasals, last.start + last.duration, temp.start));
}
if (temp.duration) out.push(temp);
});
var o2 = out;
var prevLength = 1;
var newLength = 0;
while (prevLength != newLength) {
prevLength = o2.length;
o2 = dataProcessor.filterSameAbsTemps(o2);
newLength = o2.length;
}
var o3 = [];
// Return temps from last hours
for (var i = 0; i < o2.length; i++) {
if ((o2[i].start + o2[i].duration) > dataCap) o3.push(o2[i]);
}
// Convert durations to seconds
for (var i = 0; i < o3.length; i++) {
o3[i].duration = o3[i].duration / 1000;
}
return o3;
}
module.exports = dataProcessor;
+135
View File
@@ -0,0 +1,135 @@
'use strict';
function configure (env, ctx) {
const _ = require('lodash')
, basalProcessor = require('./basaldataprocessor')
, express = require('express')
, api = express.Router();
const defaultHours = 6;
api.use(ctx.wares.compression());
function removeProps(obj,keys){
if(Array.isArray(obj)){
obj.forEach(function(item){
removeProps(item,keys)
});
}
else if(typeof obj === 'object' && obj != null){
Object.getOwnPropertyNames(obj).forEach(function(key){
if(keys.indexOf(key) !== -1)delete obj[key];
else removeProps(obj[key],keys);
});
}
}
function processSGVs(sgvs, hours) {
const bgData = [];
const dataCap = Date.now() - (hours * 60 * 60 * 1000);
for (let i = 0; i < sgvs.length; i++) {
const bg = sgvs[i];
if (bg.mills >= dataCap) {
let item = {
sgv: bg.mgdl
, mills: bg.mills
};
// only push noise data if there is noise
if (bg.noise != 1) { item.noise = bg.noise; }
bgData.push(item);
}
}
return bgData;
}
// Collect treatments that contain insulin or carbs, temp basals
function processTreatments(treatments, profile, hours) {
const rVal = {
tempBasals: [],
treatments: [],
targets: []
};
let _temps = [];
const dataCap = Date.now() - (hours * 60 * 60 * 1000);
for (let i = 0; i < treatments.length; i++) {
const t = treatments[i];
if (t.eventType == 'Temp Basal') {
_temps.push(t);
continue;
}
if (t.eventType == 'Temporary Target') {
rVal.targets.push({
targetTop: Math.round(t.targetTop),
targetBottom: Math.round(t.targetBottom),
duration: t.duration*60,
mills: t.mills
});
continue;
}
if (t.insulin || t.carbs) {
if (t.mills >= dataCap) {
const _t = {
mills: t.mills
};
if (!isNaN(t.carbs)) _t.carbs = t.carbs;
if (!isNaN(t.insulin)) _t.insulin = t.insulin;
rVal.treatments.push(_t);
}
continue;
}
}
rVal.tempBasals = basalProcessor.processTempBasals(profile,_temps, dataCap);
return rVal;
}
function constructState() {
const p = _.get(ctx, 'sbx.properties');
const state = {
iob: Math.round(_.get(p,'iob.iob')*100)/100,
cob: Math.round(_.get(p,'cob.cob')),
bwp: Math.round(_.get(p,'bwp.bolusEstimate')*100)/100,
cage: _.get(p,'cage.age'),
sage: _.get(p,'sage.age'),
iage: _.get(p,'iage.age'),
bage: _.get(p,'bage.age'),
battery: _.get(p,'upbat.level')
}
return state;
}
api.get('/', ctx.authorization.isPermitted('api:*:read'), function (req, res) {
const hours = req.query.hours || defaultHours;
const sgvs = processSGVs(ctx.ddata.sgvs, hours);
const profile = _.clone(ctx.sbx.data.profile.getCurrentProfile());
removeProps(profile,['timeAsSeconds']);
const treatments = processTreatments(ctx.ddata.treatments, profile, hours);
const state = constructState();
res.setHeader('content-type', 'application/json');
res.write(JSON.stringify({
sgvs,
treatments,
profile,
state
}));
res.end( );
});
return api;
}
module.exports = configure;
+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. 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: There is only one way to authorize API calls:
- use `token` query parameter to pass the *access token*, eg. `token=testreadab-76eaff2418bfb7e0`
- use so-called [JSON Web Tokens](https://jwt.io "JSON Web Tokens") - 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` - 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) - then, to each secure API operation attach a JWT token in the HTTP header, eg. `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NUb2tlbiI6InRlc3RyZWFkYWItNzZlYWZmMjQxOGJmYjdlMCIsImlhdCI6MTU2NTAzOTczMSwiZXhwIjoxNTY1MDQzMzMxfQ.Y-OFtFJ-gZNJcnZfm9r4S7085Z7YKVPiaQxuMMnraVk` (until the JWT expires)
+198 -129
View File
@@ -16,22 +16,23 @@ It is public (there is no need to add authorization parameters/headers).
Sample GET `/version` client code (to get actual versions): Sample GET `/version` client code (to get actual versions):
```javascript ```javascript
const request = require('request'); const axios = require('axios');
axios.get(`https://nsapiv3.herokuapp.com/api/v3/version`)
request('https://nsapiv3.herokuapp.com/api/v3/version', .then(res => {
(error, response, body) => console.log(body)); console.log(res.data);
});
``` ```
Sample result: Sample result:
```json ```json
{ {
"status": 200, "status": 200,
"result": { "result": {
"version": "14.1.0", "version": "14.2.0",
"apiVersion": "3.0.2-alpha", "apiVersion": "3.0.4-alpha",
"srvDate": 1609402081548, "srvDate": 1613056980085,
"storage": { "storage": {
"storage": "mongodb", "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): Sample GET `/status` client code (to get my actual permissions):
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`, .then(res => {
(error, response, body) => console.log(body)); 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: Sample result:
```json ```json
{ {
"status": 200, "status": 200,
"result": { "result": {
"version": "14.1.0", "version": "14.2.0",
"apiVersion": "3.0.2-alpha", "apiVersion": "3.0.4-alpha",
"srvDate": 1609427571833, "srvDate": 1613057148579,
"storage": { "storage": {
"storage": "mongodb", "storage": "mongodb",
"version": "4.2.11" "version": "4.4.3"
}, },
"apiPermissions": { "apiPermissions": {
"devicestatus": "crud", "devicestatus": "crud",
@@ -85,31 +96,41 @@ Sample result:
Sample GET `/entries` client code (to retrieve last 3 BG values): Sample GET `/entries` client code (to retrieve last 3 BG values):
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&limit=3&fields=dateString,sgv,direction`, .then(res => {
(error, response, body) => console.log(body)); 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: Sample result:
```json ```json
{ {
"status": 200, "status": 200,
"result": [ "result": [
{ {
"dateString": "2019-07-30T02:24:50.434+0200", "dateString": "2021-02-11T15:25:28.928Z",
"sgv": 115, "sgv": 116,
"direction": "FortyFiveDown" "direction": "FortyFiveDown"
}, },
{ {
"dateString": "2019-07-30T02:19:50.374+0200", "dateString": "2021-02-11T15:20:28.239Z",
"sgv": 121, "sgv": 124,
"direction": "FortyFiveDown" "direction": "FortyFiveDown"
}, },
{ {
"dateString": "2019-07-30T02:14:50.450+0200", "dateString": "2021-02-11T15:15:28.225Z",
"sgv": 129, "sgv": 130,
"direction": "FortyFiveDown" "direction": "Flat"
} }
] ]
} }
@@ -123,29 +144,37 @@ Sample result:
Sample POST `/treatments` client code: Sample POST `/treatments` client code:
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const doc = { const doc = {
date: 1564591511232, // (new Date()).getTime(), date: 1613057404186, // (new Date()).getTime(),
app: 'AndroidAPS', app: 'AndroidAPS',
device: 'Samsung XCover 4-861536030196001', device: 'Samsung XCover 4-861536030196001',
eventType: 'Correction Bolus', eventType: 'Correction Bolus',
insulin: 0.3 insulin: 0.3
}; };
request({ axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
method: 'post', .then(res => {
body: doc, const jwt = res.data.token;
json: true, return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments`,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}` {
}, method: 'post',
(error, response, body) => console.log(body)); data: doc,
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
``` ```
Sample result: Sample result:
```json ```json
{ {
"status": 201, "status": 201,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895", "identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975",
"lastModified": 1564591511711 "lastModified": 1613057523148
} }
``` ```
@@ -157,28 +186,38 @@ Sample result:
Sample GET `/treatments/{identifier}` client code: Sample GET `/treatments/{identifier}` client code:
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`, .then(res => {
(error, response, body) => console.log(body)); 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: Sample result:
```json ```json
{ {
"status": 200, "status": 200,
"result": { "result": {
"date": 1564591511232, "date": 1613057404186,
"app": "AndroidAPS", "app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001", "device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus", "eventType": "Correction Bolus",
"insulin": 0.3, "insulin": 0.3,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"utcOffset": 0, "utcOffset": 0,
"created_at": "2019-07-31T16:45:11.232Z", "created_at": "2021-02-11T15:30:04.186Z",
"srvModified": 1564591627732, "identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975",
"srvCreated": 1564591511711, "srvModified": 1613057523148,
"srvCreated": 1613057523148,
"subject": "test-admin" "subject": "test-admin"
} }
} }
@@ -192,23 +231,33 @@ Sample result:
Sample GET `/lastModified` client code (to get latest modification dates): Sample GET `/lastModified` client code (to get latest modification dates):
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`, .then(res => {
(error, response, body) => console.log(body)); 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: Sample result:
```json ```json
{ {
"status": 200, "status": 200,
"result": { "result": {
"srvDate": 1564591783202, "srvDate": 1613057924021,
"collections": { "collections": {
"devicestatus": 1564591490074, "devicestatus": 1613057731281,
"entries": 1564591486801, "entries": 1613057728148,
"profile": 1548524042744, "profile": 1580337948416,
"treatments": 1564591627732 "treatments": 1613057523148
} }
} }
} }
@@ -222,29 +271,37 @@ Sample result:
Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4): Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4):
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
const doc = { const doc = {
date: 1564591511232, date: 1613057404186,
app: 'AndroidAPS', app: 'AndroidAPS',
device: 'Samsung XCover 4-861536030196001', device: 'Samsung XCover 4-861536030196001',
eventType: 'Correction Bolus', eventType: 'Correction Bolus',
insulin: 0.4 insulin: 0.4
}; };
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request({ .then(res => {
method: 'put', const jwt = res.data.token;
body: doc, return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`,
json: true, {
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` method: 'put',
}, data: doc,
(error, response, body) => console.log(body)); headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
``` ```
Sample result: Sample result:
```json ```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): Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5):
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
const doc = { const doc = {
insulin: 0.5 insulin: 0.5
}; };
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request({ .then(res => {
method: 'patch', const jwt = res.data.token;
body: doc, return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`,
json: true, {
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` method: 'patch',
}, data: doc,
(error, response, body) => console.log(body)); headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
``` ```
Sample result: Sample result:
```json ```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. [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 ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975';
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request({ .then(res => {
method: 'delete', const jwt = res.data.token;
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`,
}, {
(error, response, body) => console.log(body)); method: 'delete',
headers: {
'Authorization': `Bearer ${jwt}`
}
});
})
.then(res => {
console.log(res.data);
});
``` ```
Sample result: Sample result:
```json ```json
@@ -311,12 +383,22 @@ Sample result:
Sample HISTORY `/treatments/history/{lastModified}` client code: Sample HISTORY `/treatments/history/{lastModified}` client code:
```javascript ```javascript
const request = require('request'); const axios = require('axios');
const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5';
const lastModified = 1564521267421; const lastModified = 1613057520148;
axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`)
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}?${auth}`, .then(res => {
(error, response, body) => console.log(response.body)); 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: Sample result:
```json ```json
@@ -324,32 +406,19 @@ Sample result:
"status": 200, "status": 200,
"result": [ "result": [
{ {
"date": 1564521267421, "date": 1613057404186,
"app": "AndroidAPS", "app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001", "device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus", "eventType": "Correction Bolus",
"insulin": 0.5, "insulin": 0.5,
"utcOffset": 0, "utcOffset": 0,
"created_at": "2019-07-30T21:14:27.421Z", "created_at": "2021-02-11T15:30:04.186Z",
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895", "identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975",
"srvModified": 1564592440416, "srvModified": 1613058548149,
"srvCreated": 1564592334853, "srvCreated": 1613057523148,
"subject": "test-admin", "subject": "test-admin",
"modifiedBy": "test-admin", "modifiedBy": "test-admin",
"isValid": false "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 [ return [
{ field: 'srvModified', operator: operator, value: lastModified.getTime() }, { field: 'srvModified', operator: operator, value: lastModified.getTime() }
{ field: 'created_at', operator: operator, value: lastModified.toISOString() },
{ field: 'date', operator: operator, value: lastModified.getTime() }
]; ];
} }
@@ -116,9 +114,7 @@ function parseFilter (opCtx) {
*/ */
function prepareSort () { function prepareSort () {
return { return {
srvModified: 1, srvModified: 1
created_at: 1,
date: 1
}; };
} }
+4 -2
View File
@@ -22,8 +22,10 @@ async function patch (opCtx) {
} }
await security.demandPermission(opCtx, `api:${col.colName}:update`); 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 const identifier = req.params.identifier
, identifyingFilter = col.storage.identifyingFilter(identifier); , identifyingFilter = col.storage.identifyingFilter(identifier);
+1 -1
View File
@@ -45,4 +45,4 @@ function validate (opCtx, doc, storageDoc, options) {
return opTools.validateCommon(doc, res, { isPatching }); return opTools.validateCommon(doc, res, { isPatching });
} }
module.exports = validate; module.exports = validate;
+4 -2
View File
@@ -3,7 +3,8 @@
const express = require('express') const express = require('express')
, bodyParser = require('body-parser') , bodyParser = require('body-parser')
, renderer = require('./shared/renderer') , renderer = require('./shared/renderer')
, StorageSocket = require('./storageSocket') , storageSocket = require('./storageSocket')
, alarmSocket = require('./alarmSocket')
, apiConst = require('./const.json') , apiConst = require('./const.json')
, security = require('./security') , security = require('./security')
, genericSetup = require('./generic/setup') , 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); 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; return app;
} }
+11 -2
View File
@@ -4,11 +4,13 @@ const apiConst = require('./const.json')
, _ = require('lodash') , _ = require('lodash')
, shiroTrie = require('shiro-trie') , shiroTrie = require('shiro-trie')
, opTools = require('./shared/operationTools') , opTools = require('./shared/operationTools')
, forwarded = require('forwarded-for')
; ;
function getRemoteIP (req) { 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 ] }); 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) { if (!token) {
return reject( return reject(
opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN)); 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') const apiConst = require('../const.json')
, stringTools = require('./stringTools') , stringTools = require('./stringTools')
, uuidv5 = require('uuid/v5') , uuid = require('uuid')
, uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-) , uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-)
; ;
@@ -103,7 +103,7 @@ function calculateIdentifier (doc) {
key += '_' + doc.eventType; key += '_' + doc.eventType;
} }
return uuidv5(key, uuidNamespace); return uuid.v5(key, uuidNamespace);
} }
+8 -2
View File
@@ -1,6 +1,12 @@
'use strict'; 'use strict';
const apiConst = require('./const'); 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 * Socket.IO broadcaster of any storage change
@@ -28,7 +34,7 @@ function StorageSocket (app, env, ctx) {
self.namespace = io.of(NAMESPACE); self.namespace = io.of(NAMESPACE);
self.namespace.on('connection', function onConnected (socket) { 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); console.log(LOG + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP);
socket.on('disconnect', function onDisconnect () { socket.on('disconnect', function onDisconnect () {
@@ -142,4 +148,4 @@ function StorageSocket (app, env, ctx) {
} }
} }
module.exports = StorageSocket; module.exports = StorageSocket;
+13 -161
View File
@@ -11,7 +11,7 @@
"name": "AGPL 3", "name": "AGPL 3",
"url": "https://www.gnu.org/licenses/agpl.txt" "url": "https://www.gnu.org/licenses/agpl.txt"
}, },
"version": "3.0.3" "version": "3.0.4"
}, },
"servers": [ "servers": [
{ {
@@ -49,17 +49,6 @@
"$ref": "#/components/schemas/paramCollection" "$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", "name": "filter_parameters",
"in": "query", "in": "query",
@@ -175,7 +164,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -216,9 +205,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -241,17 +227,6 @@
"schema": { "schema": {
"$ref": "#/components/schemas/paramCollection" "$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": { "requestBody": {
@@ -313,7 +288,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -354,9 +329,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -393,17 +365,6 @@
"$ref": "#/components/schemas/paramIdentifier" "$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", "name": "If-Modified-Since",
"in": "header", "in": "header",
@@ -473,7 +434,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -524,9 +485,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -561,17 +519,6 @@
"$ref": "#/components/schemas/paramIdentifier" "$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", "name": "If-Unmodified-Since",
"in": "header", "in": "header",
@@ -632,7 +579,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -693,9 +640,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -730,17 +674,6 @@
"$ref": "#/components/schemas/paramIdentifier" "$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", "name": "permanent",
"in": "query", "in": "query",
@@ -765,7 +698,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -806,9 +739,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -843,17 +773,6 @@
"$ref": "#/components/schemas/paramIdentifier" "$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", "name": "If-Unmodified-Since",
"in": "header", "in": "header",
@@ -899,7 +818,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -960,9 +879,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -989,17 +905,6 @@
"$ref": "#/components/schemas/paramCollection" "$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", "name": "Last-Modified",
"in": "header", "in": "header",
@@ -1087,7 +992,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -1128,9 +1033,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -1169,17 +1071,6 @@
"format": "int64" "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", "name": "limit",
"in": "query", "in": "query",
@@ -1256,7 +1147,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -1297,9 +1188,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -1346,7 +1234,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -1367,9 +1255,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -1384,19 +1269,6 @@
"summary": "LAST MODIFIED: Retrieves timestamp of the last modification of every collection", "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.", "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", "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": { "responses": {
"200": { "200": {
"description": "Successful operation returning the timestamps", "description": "Successful operation returning the timestamps",
@@ -1409,7 +1281,7 @@
} }
}, },
"401": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -1430,9 +1302,6 @@
} }
}, },
"security": [ "security": [
{
"accessToken": []
},
{ {
"jwtoken": [] "jwtoken": []
} }
@@ -1545,7 +1414,7 @@
}, },
"subject": { "subject": {
"type": "string", "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" "example": "uploader"
}, },
"srvModified": { "srvModified": {
@@ -2352,7 +2221,7 @@
} }
}, },
"401Unauthorized": { "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": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -2506,17 +2375,6 @@
} }
}, },
"parameters": { "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": { "limitParam": {
"name": "limit", "name": "limit",
"in": "query", "in": "query",
@@ -2645,12 +2503,6 @@
} }
}, },
"securitySchemes": { "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": { "jwtoken": {
"type": "http", "type": "http",
"description": "Use this if you know the temporary json webtoken.", "description": "Use this if you know the temporary json webtoken.",
+3 -50
View File
@@ -2,7 +2,7 @@ openapi: 3.0.0
servers: servers:
- url: '/api/v3' - url: '/api/v3'
info: info:
version: 3.0.3 version: 3.0.4
title: Nightscout API title: Nightscout API
contact: contact:
name: NS development discussion channel name: NS development discussion channel
@@ -75,8 +75,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/paramCollection' $ref: '#/components/schemas/paramCollection'
- $ref: '#/components/parameters/tokenParam'
###################################################################################### ######################################################################################
get: get:
tags: tags:
@@ -113,7 +111,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam' - $ref: '#/components/parameters/fieldsParam'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -158,7 +155,6 @@ paths:
$ref: '#/components/schemas/DocumentToPost' $ref: '#/components/schemas/DocumentToPost'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -196,8 +192,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/paramIdentifier' $ref: '#/components/schemas/paramIdentifier'
- $ref: '#/components/parameters/tokenParam'
###################################################################################### ######################################################################################
get: get:
tags: tags:
@@ -221,7 +215,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam' - $ref: '#/components/parameters/fieldsParam'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -276,7 +269,6 @@ paths:
$ref: '#/components/schemas/DocumentToPost' $ref: '#/components/schemas/DocumentToPost'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -341,7 +333,6 @@ paths:
$ref: '#/components/schemas/DocumentToPost' $ref: '#/components/schemas/DocumentToPost'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -382,7 +373,6 @@ paths:
- $ref: '#/components/parameters/permanentParam' - $ref: '#/components/parameters/permanentParam'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -408,8 +398,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/paramCollection' $ref: '#/components/schemas/paramCollection'
- $ref: '#/components/parameters/tokenParam'
get: get:
tags: tags:
- generic - generic
@@ -439,7 +427,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam' - $ref: '#/components/parameters/fieldsParam'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -475,8 +462,6 @@ paths:
type: integer type: integer
format: int64 format: int64
- $ref: '#/components/parameters/tokenParam'
get: get:
tags: tags:
- generic - generic
@@ -497,7 +482,6 @@ paths:
- $ref: '#/components/parameters/fieldsParam' - $ref: '#/components/parameters/fieldsParam'
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -543,7 +527,6 @@ paths:
This operation requires authorization in contrast with VERSION operation. This operation requires authorization in contrast with VERSION operation.
security: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -560,9 +543,6 @@ paths:
###################################################################################### ######################################################################################
/lastModified: /lastModified:
parameters:
- $ref: '#/components/parameters/tokenParam'
get: get:
tags: tags:
- other - 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. 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: security:
- accessToken: []
- jwtoken: [] - jwtoken: []
responses: responses:
@@ -594,22 +573,6 @@ components:
parameters: 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: limitParam:
in: query in: query
name: limit name: limit
@@ -887,7 +850,7 @@ components:
example: 400 example: 400
401Unauthorized: 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: content:
application/json: application/json:
schema: schema:
@@ -1226,7 +1189,7 @@ components:
subject: subject:
type: string type: string
description: 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) Note&#58; this field is immutable by the client (it cannot be updated or patched)
@@ -1750,16 +1713,6 @@ components:
###################################################################################### ######################################################################################
securitySchemes: 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: jwtoken:
type: http type: http
scheme: bearer scheme: bearer
+4 -2
View File
@@ -6,9 +6,11 @@ const shiroTrie = require('shiro-trie');
const consts = require('./../constants'); const consts = require('./../constants');
const sleep = require('util').promisify(setTimeout); const sleep = require('util').promisify(setTimeout);
const forwarded = require('forwarded-for');
function getRemoteIP (req) { 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) { function init (env, ctx) {
@@ -296,7 +298,7 @@ function init (env, ctx) {
const token = env.enclave.signJWT({ accessToken: subject.accessToken }); const token = env.enclave.signJWT({ accessToken: subject.accessToken });
const decoded = env.enclave.verifyJWT(token); const decoded = env.enclave.verifyJWT(token);
var roles = _.uniq(subject.roles.concat(defaultRoles)); var roles = subject.roles ? _.uniq(subject.roles.concat(defaultRoles)) : defaultRoles;
authorized = { authorized = {
token token
+5
View File
@@ -122,6 +122,11 @@ function init (env, ctx) {
, { name: 'activity', permissions: [ 'api:activity:create' ] } , { 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) { storage.getSHA1 = function getSHA1 (message) {
var shasum = crypto.createHash('sha1'); var shasum = crypto.createHash('sha1');
shasum.update(message); shasum.update(message);
+5 -7
View File
@@ -1,7 +1,6 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment-timezone');
var times = require('../times'); var times = require('../times');
var Storages = require('js-storage'); var Storages = require('js-storage');
@@ -46,9 +45,9 @@ function init (client, $) {
} }
function setDateAndTime (time) { function setDateAndTime (time) {
time = time || moment(); time = time || new Date();
eventTime.val(time.format('HH:mm')); eventTime.val(time.getHours() + ":" + time.getMinutes());
eventDate.val(time.format('YYYY-MM-DD')); eventDate.val(time.toISOString().split('T')[0]);
} }
function mergeDateAndTime () { function mergeDateAndTime () {
@@ -125,16 +124,15 @@ function init (client, $) {
boluscalc.calculateInsulin(); boluscalc.calculateInsulin();
maybePrevent(event); maybePrevent(event);
// Nightscout.utils.updateBrushToTime(moment.toDate());
}; };
boluscalc.eventTimeTypeChange = function eventTimeTypeChange (event) { boluscalc.eventTimeTypeChange = function eventTimeTypeChange (event) {
if ($('#bc_othertime').is(':checked')) { if ($('#bc_othertime').is(':checked')) {
$('#bc_eventTimeValue').focus(); $('#bc_eventTimeValue').focus();
$('#bc_retro').css('display', ''); $('#bc_retro').css('display', '');
if (mergeDateAndTime() < moment()) { if (mergeDateAndTime() < Date.now()) {
$('#bc_retro').css('background-color', 'red').text(translate('RETRO MODE')); $('#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')); $('#bc_retro').css('background-color', 'blue').text(translate('IN THE FUTURE'));
} else { } else {
$('#bc_retro').css('display', 'none'); $('#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 id = e.plugin.name + "-" + p.id;
const label = p.label; const label = p.label;
if (p.type == 'boolean') { 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); dl.append(html);
if (storage.get(id) == true) { const settingsBase = settings.extendedSettings[e.plugin.name];
if (settingsBase[p.id] == true) {
toggleCheckboxes.push(id); toggleCheckboxes.push(id);
} }
} }
+10 -4
View File
@@ -1,6 +1,5 @@
'use strict'; 'use strict';
var moment = require('moment-timezone');
var _ = require('lodash'); var _ = require('lodash');
var parse_duration = require('parse-duration'); // https://www.npmjs.com/package/parse-duration var parse_duration = require('parse-duration'); // https://www.npmjs.com/package/parse-duration
var times = require('../times'); var times = require('../times');
@@ -18,9 +17,9 @@ function init (client, $) {
var eventDate = $('#eventDateValue'); var eventDate = $('#eventDateValue');
function setDateAndTime (time) { function setDateAndTime (time) {
time = time || moment(); time = time || client.ctx.moment();
eventTime.val(time.format('HH:mm')); eventTime.val(time.hours() + ":" + time.minutes());
eventDate.val(time.format('YYYY-MM-DD')); eventDate.val(time.toISOString().split('T')[0]);
} }
function mergeDateAndTime () { function mergeDateAndTime () {
@@ -525,6 +524,11 @@ function init (client, $) {
careportal.dateTimeChange = function dateTimeChange (event) { careportal.dateTimeChange = function dateTimeChange (event) {
$('#othertime').prop('checked', true); $('#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 ele = $(this);
var merged = mergeDateAndTime(); var merged = mergeDateAndTime();
@@ -537,6 +541,8 @@ function init (client, $) {
setDateAndTime(merged); setDateAndTime(merged);
updateTime(ele, merged); updateTime(ele, merged);
*/
maybePrevent(event); maybePrevent(event);
}; };
+1 -1
View File
@@ -85,7 +85,7 @@ hashauth.init = function init (client, $) {
client.browserUtils.reload(); client.browserUtils.reload();
} }
// clear eveything just in case // clear everything just in case
hashauth.apisecret = null; hashauth.apisecret = null;
hashauth.apisecrethash = null; hashauth.apisecrethash = null;
hashauth.authenticated = false; hashauth.authenticated = false;
+72 -8
View File
@@ -9,7 +9,6 @@ var Storages = require('js-storage');
var language = require('../language')(); var language = require('../language')();
var sandbox = require('../sandbox')(); var sandbox = require('../sandbox')();
var profile = require('../profilefunctions')();
var units = require('../units')(); var units = require('../units')();
var levels = require('../levels'); var levels = require('../levels');
var times = require('../times'); var times = require('../times');
@@ -18,6 +17,8 @@ var receiveDData = require('./receiveddata');
var brushing = false; var brushing = false;
var browserSettings; var browserSettings;
var moment = window.moment;
var timezones = moment.tz.names();
var client = {}; var client = {};
@@ -152,6 +153,7 @@ client.load = function load (serverSettings, callback) {
var chart var chart
, socket , socket
, alarmSocket
, isInitialData = false , isInitialData = false
, opacity = { current: 1, DAY: 1, NIGHT: 0.5 } , opacity = { current: 1, DAY: 1, NIGHT: 0.5 }
, clientAlarms = {} , clientAlarms = {}
@@ -203,6 +205,7 @@ client.load = function load (serverSettings, callback) {
, extendedSettings: client.settings.extendedSettings , extendedSettings: client.settings.extendedSettings
, language: language , language: language
, levels: levels , levels: levels
, moment: moment
}).registerClientDefaults(); }).registerClientDefaults();
browserSettings.loadPluginSettings(client); browserSettings.loadPluginSettings(client);
@@ -210,6 +213,7 @@ client.load = function load (serverSettings, callback) {
client.utils = require('../utils')({ client.utils = require('../utils')({
settings: client.settings settings: client.settings
, language: language , language: language
, moment: moment
}); });
client.rawbg = client.plugins('rawbg'); client.rawbg = client.plugins('rawbg');
@@ -223,6 +227,8 @@ client.load = function load (serverSettings, callback) {
, bus: require('../bus')(client.settings, client.ctx) , bus: require('../bus')(client.settings, client.ctx)
, settings: client.settings , settings: client.settings
, pluginBase: client.plugins.base(majorPills, minorPills, statusPills, bgStatus, client.tooltip, Storages.localStorage) , pluginBase: client.plugins.base(majorPills, minorPills, statusPills, bgStatus, client.tooltip, Storages.localStorage)
, moment: moment
, timezones: timezones
}; };
client.ctx.language = language; client.ctx.language = language;
@@ -298,6 +304,8 @@ client.load = function load (serverSettings, callback) {
client.careportal = require('./careportal')(client, $); client.careportal = require('./careportal')(client, $);
client.boluscalc = require('./boluscalc')(client, $); client.boluscalc = require('./boluscalc')(client, $);
var profile = require('../profilefunctions')(null, client.ctx);
client.profilefunctions = profile; client.profilefunctions = profile;
client.editMode = false; client.editMode = false;
@@ -804,7 +812,7 @@ client.load = function load (serverSettings, callback) {
// only emit ack if client invoke by button press // only emit ack if client invoke by button press
if (isClient && currentNotify) { if (isClient && currentNotify) {
socket.emit('ack', currentNotify.level, currentNotify.group, silenceTime); alarmSocket.emit('ack', currentNotify.level, currentNotify.group, silenceTime);
} }
currentNotify = null; currentNotify = null;
@@ -1033,7 +1041,8 @@ client.load = function load (serverSettings, callback) {
// Client-side code to connect to server and handle incoming data // Client-side code to connect to server and handle incoming data
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* global io */ /* 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); socket.on('dataUpdate', dataUpdate);
@@ -1120,6 +1129,39 @@ client.load = function load (serverSettings, callback) {
client.authorizeSocket(); 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 () { function hasRequiredPermission () {
if (client.requiredPermission) { if (client.requiredPermission) {
if (client.hashauth && client.hashauth.isAuthenticated()) { 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; 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); console.log('notification from server:', notify);
if (notify.timestamp && previousNotifyTimestamp !== notify.timestamp) { if (notify.timestamp && previousNotifyTimestamp !== 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'); console.info('announcement received from server');
currentAnnouncement = notify; currentAnnouncement = notify;
currentAnnouncement.received = Date.now(); currentAnnouncement.received = Date.now();
updateTitle(); updateTitle();
}); });
socket.on('alarm', function(notify) { alarmSocket.on('alarm', function(notify) {
console.info('alarm received from server'); console.info('alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmHigh) || (isAlarmForLow() && client.settings.alarmLow); var enabled = (isAlarmForHigh() && client.settings.alarmHigh) || (isAlarmForLow() && client.settings.alarmLow);
if (enabled) { if (enabled) {
@@ -1173,7 +1215,7 @@ client.load = function load (serverSettings, callback) {
chart.update(false); chart.update(false);
}); });
socket.on('urgent_alarm', function(notify) { alarmSocket.on('urgent_alarm', function(notify) {
console.info('urgent alarm received from server'); console.info('urgent alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmUrgentHigh) || (isAlarmForLow() && client.settings.alarmUrgentLow); var enabled = (isAlarmForHigh() && client.settings.alarmUrgentHigh) || (isAlarmForLow() && client.settings.alarmUrgentLow);
if (enabled) { if (enabled) {
@@ -1185,12 +1227,34 @@ client.load = function load (serverSettings, callback) {
chart.update(false); chart.update(false);
}); });
socket.on('clear_alarm', function(notify) { alarmSocket.on('clear_alarm', function(notify) {
if (alarmInProgress) { if (alarmInProgress) {
console.log('clearing alarm'); console.log('clearing alarm');
stopAlarm(false, null, notify); 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) { $('#testAlarms').click(function(event) {
+1 -1
View File
@@ -7,7 +7,7 @@
"HTTP_BAD_REQUEST": 400, "HTTP_BAD_REQUEST": 400,
"ENTRIES_DEFAULT_COUNT" : 10, "ENTRIES_DEFAULT_COUNT" : 10,
"PROFILES_DEFAULT_COUNT" : 10, "PROFILES_DEFAULT_COUNT" : 10,
"MMOL_TO_MGDL": 18, "MMOL_TO_MGDL": 18.018018018,
"ONE_DAY" : 86400000, "ONE_DAY" : 86400000,
"TWO_DAYS" : 172800000, "TWO_DAYS" : 172800000,
"FIFTEEN_MINUTES": 900000, "FIFTEEN_MINUTES": 900000,
+3 -3
View File
@@ -139,7 +139,6 @@ function init(env, ctx) {
}); });
console.info('Load Complete:\n\t', counts.join(', ')); console.info('Load Complete:\n\t', counts.join(', '));
done(err, result); done(err, result);
} }
@@ -190,6 +189,7 @@ function loadEntries(ddata, ctx, callback) {
} }
}; };
var obscureDeviceProvenance = ctx.settings.obscureDeviceProvenance;
ctx.entries.list(q, function(err, results) { ctx.entries.list(q, function(err, results) {
if (err) { if (err) {
@@ -213,7 +213,7 @@ function loadEntries(ddata, ctx, callback) {
_id: element._id, _id: element._id,
mgdl: Number(element.mbg), mgdl: Number(element.mbg),
mills: element.date, mills: element.date,
device: element.device, device: obscureDeviceProvenance || element.device,
type: 'mbg' type: 'mbg'
}); });
} else if (element.sgv) { } else if (element.sgv) {
@@ -221,7 +221,7 @@ function loadEntries(ddata, ctx, callback) {
_id: element._id, _id: element._id,
mgdl: Number(element.sgv), mgdl: Number(element.sgv),
mills: element.date, mills: element.date,
device: element.device, device: obscureDeviceProvenance || element.device,
direction: element.direction, direction: element.direction,
filtered: element.filtered, filtered: element.filtered,
unfiltered: element.unfiltered, unfiltered: element.unfiltered,
+30 -17
View File
@@ -266,28 +266,41 @@ function init () {
// filter temp target // filter temp target
var tempTargetTreatments = ddata.treatments.filter(function filterTargets (t) { var tempTargetTreatments = ddata.treatments.filter(function filterTargets (t) {
//check for a units being sent return t.eventType && t.eventType.indexOf('Temporary Target') > -1;
if (t.units) { });
if (t.units == 'mmol') {
//convert to mgdl 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 (!converted && (t.targetTop < 20 || t.targetBottom < 20)) {
t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; t.targetTop = t.targetTop * consts.MMOL_TO_MGDL;
t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL;
t.units = 'mg/dl'; t.units = 'mg/dl';
} }
} }
//if we have a temp target thats below 20, assume its mmol and convert to mgdl for safety. return treatments;
if (t.targetTop < 20) { }
t.targetTop = t.targetTop * consts.MMOL_TO_MGDL;
t.units = 'mg/dl'; if (preserveOrignalTreatments) tempTargetTreatments = _.cloneDeep(tempTargetTreatments);
} tempTargetTreatments = convertTempTargetTreatmentUnites(tempTargetTreatments);
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);
ddata.tempTargetTreatments = ddata.processDurations(tempTargetTreatments, false); ddata.tempTargetTreatments = ddata.processDurations(tempTargetTreatments, false);
}; };
+2
View File
@@ -64,6 +64,8 @@ function configure (app, ctx) {
next( ); 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); api.get('/at/:at?', ensure_at, get_ddata, format_result);
return api; 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('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($('<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('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].carbs))
.append($('<span>').addClass('width100px').css('text-align','center').append(foodlist[i].gi)) .append($('<span>').addClass('width100px').css('text-align','center').append(foodlist[i].gi))
.append($('<span>').addClass('width150px').append(foodlist[i].category)) .append($('<span>').addClass('width150px').text(foodlist[i].category))
.append($('<span>').addClass('width150px').append(foodlist[i].subcategory)) .append($('<span>').addClass('width150px').text(foodlist[i].subcategory))
.append($('<span>').addClass('width100px').append(foodlist[i].fat)) .append($('<span>').addClass('width100px').append(foodlist[i].fat))
.append($('<span>').addClass('width100px').append(foodlist[i].protein)) .append($('<span>').addClass('width100px').append(foodlist[i].protein))
.append($('<span>').addClass('width100px').append(foodlist[i].energy)) .append($('<span>').addClass('width100px').append(foodlist[i].energy))
+3 -1
View File
@@ -12,7 +12,8 @@ function init (fs) {
language.lang = 'en'; language.lang = 'en';
language.languages = [ 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: 'cs', file: 'cs_CZ', language: 'Čeština', speechCode: 'cs-CZ' }
, { code: 'de', file: 'de_DE', language: 'Deutsch', speechCode: 'de-DE' } , { code: 'de', file: 'de_DE', language: 'Deutsch', speechCode: 'de-DE' }
, { code: 'dk', file: 'da_DK', language: 'Dansk', speechCode: 'dk-DK' } , { 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: 'sl', file: 'sl_SL', language: 'Slovenščina', speechCode: 'sl-SL' }
, { code: 'sv', file: 'sv_SE', language: 'Svenska', speechCode: 'sv-SE' } , { code: 'sv', file: 'sv_SE', language: 'Svenska', speechCode: 'sv-SE' }
, { code: 'tr', file: 'tr_TR', language: 'Türkçe', speechCode: 'tr-TR' } , { 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_cn', file: 'zh_CN', language: '中文(简体)', speechCode: 'cmn-Hans-CN' }
// , { code: 'zh_tw', file: 'zh_TW', language: '中文(繁體)', speechCode: 'cmn-Hant-TW' } // , { code: 'zh_tw', file: 'zh_TW', language: '中文(繁體)', speechCode: 'cmn-Hant-TW' }
]; ];
+16 -3
View File
@@ -3,19 +3,32 @@
var wares = { var wares = {
sendJSONStatus : require('./send-json-status'), sendJSONStatus : require('./send-json-status'),
bodyParser : require('body-parser'), bodyParser : require('body-parser'),
compression : require('compression') compression : require('compression'),
obscureDeviceProvenance: require('./obscure-provenance')
}; };
function extensions (list) { function extensions (list) {
return require('./express-extension-to-accept')(list); return require('./express-extension-to-accept')(list);
} }
function configure () { function configure (env) {
return { return {
sendJSONStatus: wares.sendJSONStatus( ), sendJSONStatus: wares.sendJSONStatus( ),
bodyParser: wares.bodyParser, 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, 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); notifications.ack(1, group, time);
} }
/*
* TODO: modify with a local clear, this will clear all connected clients,
* globally
*/
if (sendClear) { if (sendClear) {
var notify = { var notify = {
clear: true clear: true
@@ -192,6 +196,8 @@ function init (env, ctx) {
, message: group + ' - ' + ctx.levels.toDisplay(level) + ' was ack\'d' , message: group + ' - ' + ctx.levels.toDisplay(level) + ' was ack\'d'
, group: group , group: group
}; };
// When web client sends ack, this translates the websocket message into
// an event on our internal bus.
ctx.bus.emit('notification', notify); ctx.bus.emit('notification', notify);
logEmitEvent(notify); logEmitEvent(notify);
} }
+1 -1
View File
@@ -2,7 +2,6 @@
var _ = require('lodash'); var _ = require('lodash');
var times = require('../times'); var times = require('../times');
var moment = require('moment');
var BG_REF = 140; //Central tendency var BG_REF = 140; //Central tendency
var BG_MIN = 36; //Not 39, but why? var BG_MIN = 36; //Not 39, but why?
@@ -17,6 +16,7 @@ var AR2_COLOR = 'cyan';
function init (ctx) { function init (ctx) {
var translate = ctx.language.translate; var translate = ctx.language.translate;
var moment = ctx.moment;
var ar2 = { var ar2 = {
name: 'ar2' name: 'ar2'
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict'; 'use strict';
var times = require('../times'); var times = require('../times');
var moment = require('moment');
var consts = require('../constants'); var consts = require('../constants');
var _ = require('lodash'); var _ = require('lodash');
function init (ctx) { function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
+1 -1
View File
@@ -1,9 +1,9 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
function init(ctx) { function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var levels = ctx.levels; var levels = ctx.levels;
+1 -1
View File
@@ -1,7 +1,6 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
var times = require('../times'); var times = require('../times');
var offset = times.mins(2.5).msecs; var offset = times.mins(2.5).msecs;
@@ -9,6 +8,7 @@ var bucketFields = ['index', 'fromMills', 'toMills'];
function init (ctx) { function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var utils = require('../utils')(ctx); var utils = require('../utils')(ctx);
+43 -4
View File
@@ -24,6 +24,7 @@ function bridged (entries) {
mostRecentRecord = glucose[i].date; mostRecentRecord = glucose[i].date;
} }
} }
//console.log("DEXCOM: Most recent entry received; "+new Date(mostRecentRecord).toString());
} }
entries.create(glucose, function stored (err) { entries.create(glucose, function stored (err) {
if (err) { if (err) {
@@ -46,12 +47,12 @@ function options (env) {
, minutes: env.extendedSettings.bridge.minutes || 1440 , 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) { if (interval < 1000 || interval > 300000) {
// Invalid interval range. Revert to default // Invalid interval range. Revert to default
console.error("Invalid interval set: [" + interval + "ms]. Defaulting to 2.5 minutes.") console.error("Invalid interval set: [" + interval + "ms]. Defaulting to 2.6 minutes.")
interval = 60000 * 2.5 // 2.5 minutes interval = 60000 * 2.6 // 2.6 minutes
} }
return { return {
@@ -75,15 +76,53 @@ function create (env, bus) {
bridge.startEngine = function startEngine (entries) { bridge.startEngine = function startEngine (entries) {
opts.callback = bridged(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 () { let timer = setInterval(function () {
if (!should_run()) return;
opts.fetch.minutes = parseInt((new Date() - mostRecentRecord) / 60000); opts.fetch.minutes = parseInt((new Date() - mostRecentRecord) / 60000);
opts.fetch.maxCount = parseInt((opts.fetch.minutes / 5) + 1); opts.fetch.maxCount = parseInt((opts.fetch.minutes / 5) + 1);
opts.firstFetchCount = opts.fetch.maxCount; opts.firstFetchCount = opts.fetch.maxCount;
console.log("Fetching Share Data: ", 'minutes', opts.fetch.minutes, 'maxCount', opts.fetch.maxCount); console.log("Fetching Share Data: ", 'minutes', opts.fetch.minutes, 'maxCount', opts.fetch.maxCount);
engine(opts); engine(opts);
}, interval); }, 1000 /*interval*/);
if (bus) { if (bus) {
bus.on('teardown', function serverTeardown () { bus.on('teardown', function serverTeardown () {
+1 -1
View File
@@ -1,9 +1,9 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
function init(ctx) { function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var levels = ctx.levels; var levels = ctx.levels;
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict'; 'use strict';
var _ = require('lodash') var _ = require('lodash')
, moment = require('moment')
, times = require('../times'); , times = require('../times');
function init (ctx) { function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var iob = require('./iob')(ctx); var iob = require('./iob')(ctx);
+1 -1
View File
@@ -1,9 +1,9 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
function init(ctx) { function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var levels = ctx.levels; var levels = ctx.levels;
+3 -3
View File
@@ -1,10 +1,10 @@
'use strict'; 'use strict';
var _ = require('lodash') const _ = require('lodash')
, moment = require('moment') const times = require('../times');
, times = require('../times');
function init(ctx) { function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var utils = require('../utils')(ctx); var utils = require('../utils')(ctx);
+22 -14
View File
@@ -1,12 +1,13 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
var times = require('../times'); var times = require('../times');
// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'freq', 'rssi']; Unused variable // var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'freq', 'rssi']; Unused variable
function init (ctx) { function init (ctx) {
var moment = ctx.moment;
var utils = require('../utils')(ctx); var utils = require('../utils')(ctx);
var translate = ctx.language.translate; var translate = ctx.language.translate;
var levels = ctx.levels; var levels = ctx.levels;
@@ -231,7 +232,6 @@ function init (ctx) {
, split: false , split: false
, targets: false , targets: false
, reasons: reasonconf , reasons: reasonconf
, otp: true
, submitHook: postLoopNotification , submitHook: postLoopNotification
}, },
{ {
@@ -252,16 +252,16 @@ function init (ctx) {
{ {
val: 'Remote Carbs Entry' val: 'Remote Carbs Entry'
, name: 'Remote Carbs Entry' , name: 'Remote Carbs Entry'
, remoteCarbs: true , remoteCarbs: true
, remoteAbsorption: true , remoteAbsorption: true
, otp: true , otp: true
, submitHook: postLoopNotification , submitHook: postLoopNotification
}, },
{ {
val: 'Remote Bolus Entry' val: 'Remote Bolus Entry'
, name: 'Remote Bolus Entry' , name: 'Remote Bolus Entry'
, remoteBolus: true , remoteBolus: true
, otp: true , otp: true
, submitHook: postLoopNotification , submitHook: postLoopNotification
} }
]; ];
@@ -269,7 +269,7 @@ function init (ctx) {
// TODO: Add event listener to customize labels // TODO: Add event listener to customize labels
loop.updateVisualisation = function updateVisualisation (sbx) { loop.updateVisualisation = function updateVisualisation (sbx) {
var prop = sbx.properties.loop; var prop = sbx.properties.loop;
@@ -356,13 +356,21 @@ function init (ctx) {
function addLastEnacted () { function addLastEnacted () {
if (prop.lastEnacted) { if (prop.lastEnacted) {
var canceled = prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0; var valueParts = []
var valueParts = [ if (prop.lastEnacted.bolusVolume) {
'<b>Temp Basal' + (canceled ? ' Canceled' : ' Started') + '</b>' valueParts.push('<b>Automatic Bolus</b>')
, canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + 'U/hour for ' + prop.lastEnacted.duration + 'm' valueParts.push(' ' + prop.lastEnacted.bolusVolume + 'U')
, valueString(', ', prop.lastEnacted.reason) 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 = concatIOB(valueParts);
valueParts = concatCOB(valueParts); valueParts = concatCOB(valueParts);
+9 -3
View File
@@ -1,13 +1,13 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
var times = require('../times'); var times = require('../times');
var consts = require('../constants'); var consts = require('../constants');
// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'meal-assist', 'freq', 'rssi']; Unused variable // var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'meal-assist', 'freq', 'rssi']; Unused variable
function init (ctx) { function init (ctx) {
var moment = ctx.moment;
var utils = require('../utils')(ctx); var utils = require('../utils')(ctx);
var openaps = { var openaps = {
name: 'openaps' name: 'openaps'
@@ -392,7 +392,7 @@ function init (ctx) {
function addSuggestion () { function addSuggestion () {
if (prop.lastSuggested) { if (prop.lastSuggested) {
var bg = prop.lastSuggested.bg; var bg = prop.lastSuggested.bg;
var units = sbx.data.profile.getUnits(); var units = sbx.settings.units;
if (units === 'mmol') { if (units === 'mmol') {
bg = Math.round(bg / consts.MMOL_TO_MGDL * 10) / 10; bg = Math.round(bg / consts.MMOL_TO_MGDL * 10) / 10;
@@ -478,9 +478,15 @@ function init (ctx) {
if ('enacted' === prop.status.code) { if ('enacted' === prop.status.code) {
var canceled = prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0; 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 = [ var valueParts = [
valueString('BG: ', prop.lastEnacted.bg) valueString('BG: ', bg)
, ', <b>Temp Basal' + (canceled ? ' Canceled' : ' Started') + '</b>' , ', <b>Temp Basal' + (canceled ? ' Canceled' : ' Started') + '</b>'
, canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + ' for ' + prop.lastEnacted.duration + 'm' , canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + ' for ' + prop.lastEnacted.duration + 'm'
, valueString(', ', prop.lastEnacted.reason) , valueString(', ', prop.lastEnacted.reason)
+12 -9
View File
@@ -1,12 +1,12 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
var times = require('../times'); var times = require('../times');
var ALL_STATUS_FIELDS = ['reservoir', 'battery', 'clock', 'status', 'device']; var ALL_STATUS_FIELDS = ['reservoir', 'battery', 'clock', 'status', 'device'];
function init (ctx) { function init (ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var timeago = require('./timeago')(ctx); var timeago = require('./timeago')(ctx);
var openaps = require('./openaps')(ctx); var openaps = require('./openaps')(ctx);
@@ -91,7 +91,7 @@ function init (ctx) {
var prefs = pump.getPrefs(sbx); var prefs = pump.getPrefs(sbx);
if (!prefs.enableAlerts) { return; } if (!prefs.enableAlerts) { return; }
pump.warnOnSuspend = prefs.warnOnSuspend; pump.warnOnSuspend = prefs.warnOnSuspend;
var data = prepareData(sbx.properties.pump, prefs, sbx); var data = prepareData(sbx.properties.pump, prefs, sbx);
@@ -130,7 +130,7 @@ function init (ctx) {
} }
} }
}); });
if (result.extended) { if (result.extended) {
info.push({label: '------------', value: ''}); info.push({label: '------------', value: ''});
_.forOwn(result.extended, function(value, key) { _.forOwn(result.extended, function(value, key) {
@@ -236,11 +236,7 @@ function init (ctx) {
function updateReservoir (prefs, result) { function updateReservoir (prefs, result) {
if (result.reservoir) { if (result.reservoir) {
result.reservoir.label = 'Reservoir'; result.reservoir.label = 'Reservoir';
if (result.reservoir_display_override) { result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
result.reservoir.display = result.reservoir_display_override;
} else {
result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
}
if (result.reservoir.value < prefs.urgentRes) { if (result.reservoir.value < prefs.urgentRes) {
result.reservoir.level = levels.URGENT; result.reservoir.level = levels.URGENT;
result.reservoir.message = 'URGENT: Pump Reservoir Low'; result.reservoir.message = 'URGENT: Pump Reservoir Low';
@@ -250,11 +246,17 @@ function init (ctx) {
} else { } else {
result.reservoir.level = levels.NONE; result.reservoir.level = levels.NONE;
} }
} else if (result.manufacturer === 'Insulet' && result.model === 'Eros') { } else if (result.manufacturer === 'Insulet') {
result.reservoir = { result.reservoir = {
label: 'Reservoir', display: '50+ U' 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) { function updateBattery (type, prefs, result, batteryWarn) {
@@ -319,6 +321,7 @@ function init (ctx) {
, clock: pump.clock ? { value: moment(pump.clock) } : null , clock: pump.clock ? { value: moment(pump.clock) } : null
, reservoir: pump.reservoir || pump.reservoir === 0 ? { value: pump.reservoir } : null , reservoir: pump.reservoir || pump.reservoir === 0 ? { value: pump.reservoir } : null
, reservoir_display_override: pump.reservoir_display_override || null , reservoir_display_override: pump.reservoir_display_override || null
, reservoir_level_override: pump.reservoir_level_override || null
, manufacturer: pump.manufacturer , manufacturer: pump.manufacturer
, model: pump.model , model: pump.model
, extended: pump.extended || null , extended: pump.extended || null
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
var times = require('../times'); var times = require('../times');
function init(ctx) { function init(ctx) {
var moment = ctx.moment;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var levels = ctx.levels; var levels = ctx.levels;
+5
View File
@@ -123,6 +123,11 @@ function init(ctx) {
message = '...'; 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 hash = crypto.createHash('sha1');
const info = JSON.stringify({ eventType, timestamp}); const info = JSON.stringify({ eventType, timestamp});
hash.update(info); hash.update(info);
+2 -1
View File
@@ -72,6 +72,7 @@ function init(ctx) {
var battery = uploaderStatus.battery; var battery = uploaderStatus.battery;
var voltage = uploaderStatus.batteryVoltage; var voltage = uploaderStatus.batteryVoltage;
var charging = status.isCharging ? status.isCharging : false;
var voltageDisplay; var voltageDisplay;
if (voltage) { if (voltage) {
@@ -93,7 +94,7 @@ function init(ctx) {
uploaderStatus.voltageDisplay = voltageDisplay; uploaderStatus.voltageDisplay = voltageDisplay;
} }
uploaderStatus.display = battery ? battery + '%' : voltageDisplay; uploaderStatus.display = (battery ? battery + '%' : voltageDisplay) + (charging ? "⚡" : "");
if (battery >= 95) { if (battery >= 95) {
uploaderStatus.level = 100; uploaderStatus.level = 100;
+2 -1
View File
@@ -1,9 +1,10 @@
'use strict'; 'use strict';
var moment = require('moment');
var _each = require('lodash/each'); var _each = require('lodash/each');
function init(env, ctx) { function init(env, ctx) {
var moment = ctx.moment;
function virtAsstBase() { function virtAsstBase() {
return virtAsstBase; return virtAsstBase;
} }
+1 -1
View File
@@ -1,10 +1,10 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment');
var times = require('../times'); var times = require('../times');
function init(ctx) { function init(ctx) {
var moment = ctx.moment;
var levels = ctx.levels; var levels = ctx.levels;
var utils = require('../utils')(ctx); var utils = require('../utils')(ctx);
var firstPrefs = true; var firstPrefs = true;
+33 -10
View File
@@ -4,7 +4,6 @@ var init = function init () {
//for the tests window isn't the global object //for the tests window isn't the global object
var $ = window.$; var $ = window.$;
var _ = window._; var _ = window._;
var moment = window.moment;
var Nightscout = window.Nightscout; var Nightscout = window.Nightscout;
var client = Nightscout.client; var client = Nightscout.client;
@@ -19,6 +18,7 @@ var init = function init () {
client.init(function loaded () { client.init(function loaded () {
console.log("LOADING CLIENT INIT");
if (c_profile !== null) { if (c_profile !== null) {
return; // already loaded so don't load again return; // already loaded so don't load again
} }
@@ -157,7 +157,7 @@ var init = function init () {
// Load timezones // Load timezones
timezoneInput.empty(); timezoneInput.empty();
moment.tz.names().forEach(function addTz(tz) { client.ctx.timezones.forEach(function addTz(tz) {
timezoneInput.append('<option value="' + tz + '">' + tz + '</option>'); timezoneInput.append('<option value="' + tz + '">' + tz + '</option>');
}); });
@@ -198,8 +198,8 @@ var init = function init () {
} }
databaseRecords.val(currentrecord); databaseRecords.val(currentrecord);
timeInput.val(moment(mongorecords[currentrecord].startDate).format('HH:mm')); timeInput.val(client.ctx.moment(mongorecords[currentrecord].startDate).format('HH:mm'));
dateInput.val(moment(mongorecords[currentrecord].startDate).format('YYYY-MM-DD')); dateInput.val(client.ctx.moment(mongorecords[currentrecord].startDate).format('YYYY-MM-DD'));
initProfile(); initProfile();
} }
@@ -313,11 +313,10 @@ var init = function init () {
profileSubmit(); profileSubmit();
} }
GUIToObject(); GUIToObject();
mongorecords.push(_.cloneDeep(mongorecords[currentrecord])); mongorecords.push(_.omit(mongorecords[currentrecord], ['_id', 'srvModified', 'srvCreated', 'identifier', 'mills']));
currentrecord = mongorecords.length - 1; currentrecord = mongorecords.length - 1;
mongorecords[currentrecord].startDate = new Date().toISOString(); mongorecords[currentrecord].startDate = new Date().toISOString();
currentprofile = mongorecords[currentrecord].defaultProfile; currentprofile = mongorecords[currentrecord].defaultProfile;
delete mongorecords[currentrecord]._id;
initRecord(); initRecord();
dirty = true; dirty = true;
@@ -390,6 +389,7 @@ var init = function init () {
newname += '1'; newname += '1';
} }
record.store[newname] = _.cloneDeep(record.store[currentprofile]); record.store[newname] = _.cloneDeep(record.store[currentprofile]);
currentprofile = newname; currentprofile = newname;
dirty = true; dirty = true;
@@ -565,7 +565,20 @@ var init = function init () {
$('#pe_delay_high').val(c_profile.delay_high); $('#pe_delay_high').val(c_profile.delay_high);
$('#pe_delay_medium').val(c_profile.delay_medium); $('#pe_delay_medium').val(c_profile.delay_medium);
$('#pe_delay_low').val(c_profile.delay_low); $('#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; var index;
[ { prefix:'pe_basal', array:'basal' }, [ { 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_high = parseInt($('#pe_delay_high').val());
c_profile.delay_medium = parseInt($('#pe_delay_medium').val()); c_profile.delay_medium = parseInt($('#pe_delay_medium').val());
c_profile.delay_low = parseInt($('#pe_delay_low').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; var index;
[ { prefix:'pe_basal', array:'basal' }, [ { prefix:'pe_basal', array:'basal' },
@@ -635,11 +656,11 @@ var init = function init () {
} }
function toTimeString(minfrommidnight) { 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) { 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'); return client.settings.timeFormat === 24 ? time.format('HH:mm') : time.format('h:mm A');
} }
@@ -652,6 +673,8 @@ var init = function init () {
profileChange(event); profileChange(event);
var record = mongorecords[currentrecord]; var record = mongorecords[currentrecord];
record.startDate = new Date(client.utils.mergeInputTime(timeInput.val(), dateInput.val())).toISOString( ); 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); var adjustedRecord = _.cloneDeep(record);
+7 -3
View File
@@ -1,14 +1,15 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment-timezone');
var c = require('memory-cache'); var c = require('memory-cache');
var times = require('./times'); var times = require('./times');
var cacheTTL = 5000; var cacheTTL = 5000;
var prevBasalTreatment = null; var prevBasalTreatment = null;
function init (profileData) { function init (profileData, ctx) {
var moment = ctx.moment;
var cache = new c.Cache(); var cache = new c.Cache();
var profile = {}; var profile = {};
@@ -174,7 +175,10 @@ function init (profileData) {
}; };
profile.getTimezone = function getTimezone (spec_profile) { 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 () { 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 //for the tests window isn't the global object
var $ = window.$; var $ = window.$;
var _ = window._; var _ = window._;
var moment = window.moment;
var Nightscout = window.Nightscout; var Nightscout = window.Nightscout;
var client = Nightscout.client; var client = Nightscout.client;
var report_plugins_preinit = Nightscout.report_plugins_preinit; var report_plugins_preinit = Nightscout.report_plugins_preinit;
@@ -12,6 +11,8 @@ var init = function init () {
client.init(function loaded () { client.init(function loaded () {
var moment = client.ctx.moment;
report_plugins = report_plugins_preinit(client.ctx); report_plugins = report_plugins_preinit(client.ctx);
Nightscout.report_plugins = report_plugins; Nightscout.report_plugins = report_plugins;
@@ -257,9 +258,11 @@ var init = function init () {
function datefilter () { function datefilter () {
if ($('#rp_enabledate').is(':checked')) { if ($('#rp_enabledate').is(':checked')) {
matchesneeded++; matchesneeded++;
var from = moment.tz($('#rp_from').val().replace(/\//g, '-') + 'T00:00:00', zone); var from = moment.tz(moment($('#rp_from').val()).startOf('day'), zone).startOf('day');
var to = moment.tz($('#rp_to').val().replace(/\//g, '-') + 'T23:59:59', zone); 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(); 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); //console.log($('#rp_from').val(),$('#rp_to').val(),zone,timerange);
while (from <= to) { while (from <= to) {
if (daystoshow[from.format('YYYY-MM-DD')]) { 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; var fatSum = 0;
daytoday.prepareHtml(sorteddaystoshow); daytoday.prepareHtml(sorteddaystoshow);
console.log('DAY2DAY', 'sorteddaystoshow', sorteddaystoshow);
sorteddaystoshow.forEach(function eachDay (day) { sorteddaystoshow.forEach(function eachDay (day) {
drawChart(day, datastorage[day], options); 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 // create svg and g to contain the chart contents
charts = d3.select('#daytodaychart-' + day).html( charts = d3.select('#daytodaychart-' + day).html(
'<b>' + '<b>' +
report_plugins.utils.localeDate(day) + report_plugins.utils.localeDate(moment(day)) +
'</b><br>' '</b><br>'
).append('svg'); ).append('svg');
@@ -432,8 +434,8 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
contextCircles.exit() contextCircles.exit()
.remove(); .remove();
var to = moment(day).add(1, 'days'); var from = moment.tz(moment(day), profile.getTimezone( )).startOf('day');
var from = moment(day); var to = moment(from.clone( )).add(1, 'days');
var iobpolyline = '' var iobpolyline = ''
, cobpolyline = ''; , cobpolyline = '';
+3 -4
View File
@@ -55,7 +55,7 @@ function init () {
sbx.language = ctx.language; sbx.language = ctx.language;
sbx.translate = ctx.language.translate; sbx.translate = ctx.language.translate;
var profile = require('./profilefunctions')(); var profile = require('./profilefunctions')(null, ctx);
//Plugins will expect the right profile based on time //Plugins will expect the right profile based on time
profile.loadData(_.cloneDeep(ctx.ddata.profiles)); profile.loadData(_.cloneDeep(ctx.ddata.profiles));
profile.updateTreatments(ctx.ddata.profileTreatments, ctx.ddata.tempbasalTreatments, ctx.ddata.combobolusTreatments); profile.updateTreatments(ctx.ddata.profileTreatments, ctx.ddata.tempbasalTreatments, ctx.ddata.combobolusTreatments);
@@ -235,10 +235,9 @@ function init () {
}; };
sbx.displayBg = function displayBg (entry) { sbx.displayBg = function displayBg (entry) {
var isDex = entry && (!entry.device || entry.device === 'dexcom'); if (Number(entry.mgdl) === 39) {
if (isDex && Number(entry.mgdl) === 39) {
return 'LOW'; return 'LOW';
} else if (isDex && Number(entry.mgdl) === 401) { } else if (Number(entry.mgdl) === 401) {
return 'HIGH'; return 'HIGH';
} else { } else {
return sbx.scaleEntry(entry); return sbx.scaleEntry(entry);
+4 -15
View File
@@ -26,8 +26,6 @@ function create (env, ctx) {
var appInfo = env.name + ' ' + env.version; var appInfo = env.name + ' ' + env.version;
app.set('title', appInfo); app.set('title', appInfo);
app.enable('trust proxy'); // Allows req.secure test on heroku https connections. 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 insecureUseHttp = env.insecureUseHttp;
var secureHstsHeader = env.secureHstsHeader; var secureHstsHeader = env.secureHstsHeader;
if (!insecureUseHttp) { if (!insecureUseHttp) {
@@ -186,9 +184,8 @@ function create (env, ctx) {
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
const apiRoot = require('../api/root')(env, ctx); const apiRoot = require('../api/root')(env, ctx);
var api = require('../api/')(env, ctx); var api = require('../api/')(env, ctx);
var api2 = require('../api2/')(env,ctx, api);
var api3 = require('../api3/')(env, ctx); var api3 = require('../api3/')(env, ctx);
var ddata = require('../data/endpoints')(env, ctx);
var notificationsV2 = require('../api/notifications-v2')(app, ctx);
app.use(compression({ app.use(compression({
filter: function shouldCompress (req, res) { filter: function shouldCompress (req, res) {
@@ -247,16 +244,8 @@ function create (env, ctx) {
app.use("/clock", clockviews); app.use("/clock", clockviews);
app.use('/api', apiRoot); app.use('/api', apiRoot);
app.use('/api/v1', api); app.use('/api/v1', api);
app.use('/api/v2', api2);
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/v3', api3); app.use('/api/v3', api3);
// pebble data // pebble data
@@ -322,7 +311,7 @@ function create (env, ctx) {
} }
// Production bundling // Production bundling
const tmpFiles = express.static(resolvePath('/tmp/public'), { const tmpFiles = express.static(resolvePath('/node_modules/.cache/_ns_cache/public'), {
maxAge: maxAge maxAge: maxAge
}); });
@@ -345,7 +334,7 @@ function create (env, ctx) {
, coffee_match: /coffeescript/ , coffee_match: /coffeescript/
, json_match: /json/ , json_match: /json/
, cssmin: myCssmin , cssmin: myCssmin
, cache: resolvePath('/tmp/public') , cache: resolvePath('/node_modules/.cache/_ns_cache/public')
, onerror: undefined , 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>'; return '<dt><b>' + obj.desc + '</b></dt><dd>' + message.replace(/\\n/g, '<br/>') + '</dd>';
}).join(' '); }).join(' ');
res.render('error.html', { res.status(500).render('error.html', {
errors, errors,
locals locals
}); });
+50 -25
View File
@@ -9,6 +9,8 @@ function boot (env, language) {
console.log('Executing startBoot'); console.log('Executing startBoot');
ctx.bootErrors = [ ];
ctx.moment = require('moment-timezone');
ctx.runtimeState = 'booting'; ctx.runtimeState = 'booting';
ctx.settings = env.settings; ctx.settings = env.settings;
ctx.bus = require('../bus')(env.settings, ctx); ctx.bus = require('../bus')(env.settings, ctx);
@@ -23,7 +25,8 @@ function boot (env, language) {
////////////////////////////////////////////////// //////////////////////////////////////////////////
// Check Node version. // 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. // Older Node versions or Node versions with known security issues will not work.
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
function checkNodeVersion (ctx, next) { function checkNodeVersion (ctx, next) {
@@ -34,10 +37,10 @@ function boot (env, language) {
var nodeVersion = process.version; var nodeVersion = process.version;
const isLTS = process.release.lts ? true : false; 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'))) { 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 10 LTS and Node 12 LTS are recommended and supported. //Latest Node 14 LTS and Node 16 LTS are recommended and supported.
//Require at least Node 8 LTS and Node 10 LTS without known security issues //Require at least Node 14 without known security issues
console.debug('Node LTS version ' + nodeVersion + ' is supported'); console.debug('Node LTS version ' + nodeVersion + ' is supported');
next(); next();
return; return;
@@ -71,29 +74,35 @@ function boot (env, language) {
var configURL = env.IMPORT_CONFIG || null; var configURL = env.IMPORT_CONFIG || null;
var url = require('url'); var url = require('url');
var href = null; var href = null;
try {
href = url.parse(configURL).href; if (configURL) {
} catch (e) { try {
console.error('Parsing config URL from IMPORT_CONFIG failed'); href = url.parse(configURL).href;
} catch (e) {
console.error('Parsing config URL from IMPORT_CONFIG failed');
}
} }
if(configURL && href) { 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); console.log('Getting settings from', href);
request.get({url: href, json: true}, function (err, resp, body) { return axios.get(href).then(function (resp) {
if (err) { var body = resp.data;
console.log('Attempt to fetch config', href, 'failed.'); var settings = body.settings || body;
console.error(err); console.log('extending settings with', settings);
throw err; _.merge(env.settings, settings);
} else { if (body.extendedSettings) {
var settings = body.settings || body; console.log('extending extendedSettings with', body.extendedSettings);
console.log('extending settings with', settings); _.merge(env.extendedSettings, body.extendedSettings);
_.merge(env.settings, settings);
if (body.extendedSettings) {
console.log('extending extendedSettings with', body.extendedSettings);
_.merge(env.extendedSettings, body.extendedSettings);
}
} }
next( ); 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 { } else {
next( ); next( );
@@ -179,6 +188,7 @@ function boot (env, language) {
} }
ctx.authorization = require('../authorization')(env, ctx); ctx.authorization = require('../authorization')(env, ctx);
ctx.authorization.storage.ensureIndexes();
ctx.authorization.storage.reload(function loaded (err) { ctx.authorization.storage.reload(function loaded (err) {
if (err) { if (err) {
ctx.bootErrors = ctx.bootErrors || [ ]; ctx.bootErrors = ctx.bootErrors || [ ];
@@ -206,8 +216,11 @@ function boot (env, language) {
settings: env.settings settings: env.settings
, language: ctx.language , language: ctx.language
, levels: ctx.levels , levels: ctx.levels
, moment: ctx.moment
}).registerServerDefaults(); }).registerServerDefaults();
ctx.wares = require('../middleware/')(env);
ctx.pushover = require('../plugins/pushover')(env, ctx); ctx.pushover = require('../plugins/pushover')(env, ctx);
ctx.maker = require('../plugins/maker')(env); ctx.maker = require('../plugins/maker')(env);
ctx.pushnotify = require('./pushnotify')(env, ctx); ctx.pushnotify = require('./pushnotify')(env, ctx);
@@ -220,7 +233,7 @@ function boot (env, language) {
ctx.profile = require('./profile')(env.profile_collection, ctx); ctx.profile = require('./profile')(env.profile_collection, ctx);
ctx.food = require('./food')(env, ctx); ctx.food = require('./food')(env, ctx);
ctx.pebble = require('./pebble')(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.ddata = require('../data/ddata')();
ctx.cache = require('./cache')(env,ctx); ctx.cache = require('./cache')(env,ctx);
ctx.dataloader = require('../data/dataloader')(env, ctx); ctx.dataloader = require('../data/dataloader')(env, ctx);
@@ -292,7 +305,8 @@ function boot (env, language) {
ctx.notifications.initRequests(); ctx.notifications.initRequests();
ctx.plugins.checkNotifications(sbx); ctx.plugins.checkNotifications(sbx);
ctx.notifications.process(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 ( ) { ctx.bus.on('data-processed', function processed ( ) {
@@ -304,6 +318,13 @@ function boot (env, language) {
next( ); next( );
} }
function setupConnect (ctx, next) {
console.log('Executing setupConnect');
ctx.nightscoutConnect = require('nightscout-connect')(env, ctx)
// ctx.nightscoutConnect.
return next( );
}
function setupBridge (ctx, next) { function setupBridge (ctx, next) {
console.log('Executing setupBridge'); console.log('Executing setupBridge');
@@ -315,6 +336,7 @@ function boot (env, language) {
ctx.bridge = require('../plugins/bridge')(env, ctx.bus); ctx.bridge = require('../plugins/bridge')(env, ctx.bus);
if (ctx.bridge) { if (ctx.bridge) {
ctx.bridge.startEngine(ctx.entries); ctx.bridge.startEngine(ctx.entries);
console.log("DEPRECATION WARNING", "PLEASE CONSIDER nightscout-connect instead.");
} }
next( ); next( );
} }
@@ -330,6 +352,7 @@ function boot (env, language) {
ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus, ctx.bus); ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus, ctx.bus);
if (ctx.mmconnect) { if (ctx.mmconnect) {
ctx.mmconnect.run(); ctx.mmconnect.run();
console.log("DEPRECATION WARNING", "PLEASE CONSIDER nightscout-connect instead.");
} }
next( ); next( );
} }
@@ -341,6 +364,7 @@ function boot (env, language) {
if (hasBootErrors(ctx)) { if (hasBootErrors(ctx)) {
return next(); return next();
} }
ctx.bus.emit('finishBoot');
ctx.runtimeState = 'booted'; ctx.runtimeState = 'booted';
ctx.bus.uptime( ); ctx.bus.uptime( );
@@ -359,6 +383,7 @@ function boot (env, language) {
.acquire(setupInternals) .acquire(setupInternals)
.acquire(ensureIndexes) .acquire(ensureIndexes)
.acquire(setupListeners) .acquire(setupListeners)
.acquire(setupConnect)
.acquire(setupBridge) .acquire(setupBridge)
.acquire(setupMMConnect) .acquire(setupMMConnect)
.acquire(finishBoot); .acquire(finishBoot);
+11 -3
View File
@@ -29,23 +29,31 @@ function cache (env, ctx) {
, entries: constants.TWO_DAYS , 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) { function mergeCacheArrays (oldData, newData, retentionPeriod) {
const ageLimit = Date.now() - retentionPeriod; const ageLimit = Date.now() - retentionPeriod;
var filteredOld = filterForAge(oldData, ageLimit); var filteredOld = filterForAge(oldData, ageLimit);
var filteredNew = filterForAge(newData, ageLimit); var filteredNew = filterForAge(newData, ageLimit);
const merged = ctx.ddata.idMergePreferNew(filteredOld, filteredNew); const merged = ctx.ddata.idMergePreferNew(filteredOld, filteredNew);
return _.sortBy(merged, function(item) { return _.sortBy(merged, function(item) {
return -item.mills; const age = getObjectAge(item);
return -age;
}); });
function filterForAge(data, ageLimit) { function filterForAge(data, ageLimit) {
return _.filter(data, function hasId(object) { return _.filter(data, function hasId(object) {
const hasId = !_.isEmpty(object._id); const hasId = !_.isEmpty(object._id);
const isFresh = object.mills >= ageLimit; const age = getObjectAge(object);
const isFresh = age >= ageLimit;
return isFresh && hasId; return isFresh && hasId;
}); });
} }
+4 -4
View File
@@ -19,7 +19,7 @@ const init = function init () {
let apiKeySet = false; let apiKeySet = false;
function readKey (filename) { 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)) { if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath).toString().trim(); return fs.readFileSync(filePath).toString().trim();
} }
@@ -32,7 +32,7 @@ const init = function init () {
function genHash(data, algorihtm) { function genHash(data, algorihtm) {
const hash = crypto.createHash(algorihtm); const hash = crypto.createHash(algorihtm);
data = hash.update(data, 'utf-8'); data = hash.update(data, 'utf-8');
return data.digest('hex'); return data.digest('hex').toLowerCase();
} }
enclave.setApiKey = function setApiKey (keyValue) { enclave.setApiKey = function setApiKey (keyValue) {
@@ -48,7 +48,7 @@ const init = function init () {
} }
enclave.isApiKey = function isApiKey (keyValue) { 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) { enclave.setJWTKey = function setJWTKey (keyValue) {
@@ -72,7 +72,7 @@ const init = function init () {
var shasum = crypto.createHash('sha1'); var shasum = crypto.createHash('sha1');
shasum.update(secrets[apiKeySHA1]); shasum.update(secrets[apiKeySHA1]);
shasum.update(id); shasum.update(id);
return shasum.digest('hex'); return shasum.digest('hex').toLowerCase();
} }
return enclave; return enclave;
+5 -8
View File
@@ -99,8 +99,11 @@ function storage (env, ctx) {
// Normalize dates to be in UTC, store offset in utcOffset // Normalize dates to be in UTC, store offset in utcOffset
var _sysTime = moment(doc.dateString).isValid() ? moment.parseZone(doc.dateString) : moment(doc.date); var _sysTime;
_sysTime = _sysTime.isValid() ? _sysTime : moment();
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.utcOffset = _sysTime.utcOffset();
doc.sysTime = _sysTime.toISOString(); doc.sysTime = _sysTime.toISOString();
@@ -163,17 +166,11 @@ function storage (env, ctx) {
api.aggregate = require('./aggregate')({}, api); api.aggregate = require('./aggregate')({}, api);
api.indexedFields = [ api.indexedFields = [
'date' 'date'
, 'type' , 'type'
, 'sgv' , 'sgv'
, 'mbg' , 'mbg'
, 'sysTime' , 'sysTime'
, 'dateString' , 'dateString'
, { 'type': 1, 'date': -1, 'dateString': 1 } , { 'type': 1, 'date': -1, 'dateString': 1 }
]; ];
return api; return api;
+45 -12
View File
@@ -1,6 +1,6 @@
//'use strict'; //'use strict';
const apn = require('apn'); const apn = require('@parse/node-apn');
function init (env, ctx) { function init (env, ctx) {
@@ -71,9 +71,9 @@ function init (env, ctx) {
payload["override-duration-minutes"] = parseInt(data.duration); payload["override-duration-minutes"] = parseInt(data.duration);
} }
alert = data.reasonDisplay + " Temporary Override"; alert = data.reasonDisplay + " Temporary Override";
} else if (data.eventType === 'Remote Carbs Entry') { } else if (data.eventType === 'Remote Carbs Entry') {
payload["carbs-entry"] = parseFloat(data.remoteCarbs); payload["carbs-entry"] = parseFloat(data.remoteCarbs);
if(payload["carbs-entry"] > 0.0 ) { if(payload["carbs-entry"] > 0.0 ) {
payload["absorption-time"] = 3.0; payload["absorption-time"] = 3.0;
if (data.remoteAbsorption !== undefined && parseFloat(data.remoteAbsorption) > 0.0) { if (data.remoteAbsorption !== undefined && parseFloat(data.remoteAbsorption) > 0.0) {
payload["absorption-time"] = parseFloat(data.remoteAbsorption); payload["absorption-time"] = parseFloat(data.remoteAbsorption);
@@ -81,21 +81,24 @@ function init (env, ctx) {
if (data.otp !== undefined && data.otp.length > 0) { if (data.otp !== undefined && data.otp.length > 0) {
payload["otp"] = ""+data.otp 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 = "Remote Carbs Entry: "+payload["carbs-entry"]+" grams\n";
alert += "Absorption Time: "+payload["absorption-time"]+" hours"; alert += "Absorption Time: "+payload["absorption-time"]+" hours";
} else { } else {
completion("Loop remote carbs failed. Incorrect carbs entry: ", data.remoteCarbs); completion("Loop remote carbs failed. Incorrect carbs entry: ", data.remoteCarbs);
return; return;
} }
} else if (data.eventType === 'Remote Bolus Entry') { } else if (data.eventType === 'Remote Bolus Entry') {
payload["bolus-entry"] = parseFloat(data.remoteBolus); payload["bolus-entry"] = parseFloat(data.remoteBolus);
if(payload["bolus-entry"] > 0.0 ) { if(payload["bolus-entry"] > 0.0 ) {
alert = "Remote Bolus Entry: "+payload["bolus-entry"]+" U\n"; alert = "Remote Bolus Entry: "+payload["bolus-entry"]+" U\n";
if (data.otp !== undefined && data.otp.length > 0) { if (data.otp !== undefined && data.otp.length > 0) {
payload["otp"] = ""+data.otp payload["otp"] = ""+data.otp
} }
} else { } else {
completion("Loop remote bolus failed. Incorrect bolus entry: ", data.remoteBolus); completion("Loop remote bolus failed. Incorrect bolus entry: ", data.remoteBolus);
return; return;
} }
@@ -112,21 +115,51 @@ function init (env, ctx) {
alert += " - " + data.enteredBy 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(); let notification = new apn.Notification();
notification.alert = alert; notification.alert = alert;
notification.topic = loopSettings.bundleIdentifier; notification.topic = loopSettings.bundleIdentifier;
notification.contentAvailable = 1; notification.contentAvailable = 1;
notification.expiry = Math.round((Date.now() / 1000)) + 60 * 5; // Allow this to enact within 5 minutes.
notification.payload = payload; 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) { if (response.sent && response.sent.length > 0) {
completion(); completion();
} else { } else {
console.log("APNs delivery failed:", response.failed) console.log("APNs delivery failed:", response.failed);
completion("APNs delivery failed: " + response.failed[0].response.reason);
// 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(); return loop();
-8
View File
@@ -70,14 +70,6 @@ require('./bootevent')(env, language).boot(function booted (ctx) {
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
var websocket = require('./websocket')(env, ctx, server); 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 //after startup if there are no alarms send all clear
let sendStartupAllClearTimer = setTimeout(function sendStartupAllClear () { let sendStartupAllClearTimer = setTimeout(function sendStartupAllClear () {
var alarm = ctx.notifications.findHighestAlarm(); var alarm = ctx.notifications.findHighestAlarm();
+1 -1
View File
@@ -881,7 +881,7 @@
"securitySchemes": { "securitySchemes": {
"api_secret": { "api_secret": {
"type": "apiKey", "type": "apiKey",
"name": "api_secret", "name": "api-secret",
"in": "header", "in": "header",
"description": "The hash of the API_SECRET env var" "description": "The hash of the API_SECRET env var"
}, },
+1 -1
View File
@@ -656,7 +656,7 @@ components:
securitySchemes: securitySchemes:
api_secret: api_secret:
type: apiKey type: apiKey
name: api_secret name: api-secret
in: header in: header
description: The hash of the API_SECRET env var description: The hash of the API_SECRET env var
token_in_url: token_in_url:
+34 -49
View File
@@ -3,6 +3,12 @@
var times = require('../times'); var times = require('../times');
var calcData = require('../data/calcdelta'); var calcData = require('../data/calcdelta');
var ObjectID = require('mongodb').ObjectID; 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) { function init (env, ctx, server) {
@@ -10,8 +16,6 @@ function init (env, ctx, server) {
return websocket; return websocket;
} }
var levels = ctx.levels;
//var log_yellow = '\x1B[33m'; //var log_yellow = '\x1B[33m';
var log_green = '\x1B[32m'; var log_green = '\x1B[32m';
var log_magenta = '\x1B[35m'; var log_magenta = '\x1B[35m';
@@ -68,13 +72,21 @@ function init (env, ctx, server) {
function start () { function start () {
io = require('socket.io')({ io = require('socket.io')({
'transports': ['xhr-polling'] 'log level': 0
, 'log level': 0
}).listen(server, { }).listen(server, {
//these only effect the socket.io.js file that is sent to the client, but better than nothing //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 etag': true
, 'browser client gzip': false , 'browser client gzip': false
, 'perMessageDeflate': {
threshold: 512
}
, transports: ["polling", "websocket"]
, httpCompression: {
threshold: 512
}
}); });
ctx.bus.on('teardown', function serverTeardown () { ctx.bus.on('teardown', function serverTeardown () {
@@ -83,6 +95,11 @@ function init (env, ctx, server) {
}); });
io.close(); io.close();
}); });
ctx.bus.on('data-processed', function() {
update();
});
} }
function verifyAuthorization (message, ip, callback) { function verifyAuthorization (message, ip, callback) {
@@ -116,7 +133,7 @@ function init (env, ctx, server) {
delta.status = status(ctx.ddata.profiles); delta.status = status(ctx.ddata.profiles);
lastProfileSwitch = ctx.ddata.lastProfileFromSwitch; 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 timeDiff;
var history; 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); console.log(LOG_WS + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP);
io.emit('clients', ++watchers); io.emit('clients', ++watchers);
socket.on('ack', function onAck (level, group, silenceTime) {
ctx.notifications.ack(level, group, silenceTime, true);
});
socket.on('disconnect', function onDisconnect () { socket.on('disconnect', function onDisconnect () {
io.emit('clients', --watchers); io.emit('clients', --watchers);
console.log(LOG_WS + 'Disconnected client ID: ', socket.client.id); console.log(LOG_WS + 'Disconnected client ID: ', socket.client.id);
@@ -177,7 +190,7 @@ function init (env, ctx, server) {
callback({ result: 'success' }); callback({ result: 'success' });
} }
//TODO: use opts to only send delta for retro data //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); console.info('sent retroUpdate', opts);
}); });
@@ -217,7 +230,7 @@ function init (env, ctx, server) {
ctx.store.collection(collection).findOne({ '_id': id } ctx.store.collection(collection).findOne({ '_id': id }
, function(err, results) { , function(err, results) {
console.log('Got results', results); console.log('Got results', results);
if (!err) { if (!err && results !== null) {
ctx.bus.emit('data-update', { ctx.bus.emit('data-update', {
type: data.collection type: data.collection
, op: 'update' , op: 'update'
@@ -264,7 +277,7 @@ function init (env, ctx, server) {
ctx.store.collection(collection).findOne({ '_id': objId } ctx.store.collection(collection).findOne({ '_id': objId }
, function(err, results) { , function(err, results) {
console.log('Got results', results); console.log('Got results', results);
if (!err) { if (!err && results !== null) {
ctx.bus.emit('data-update', { ctx.bus.emit('data-update', {
type: data.collection type: data.collection
, op: 'update' , op: 'update'
@@ -292,7 +305,7 @@ function init (env, ctx, server) {
socket.on('dbAdd', function dbAdd (data, callback) { socket.on('dbAdd', function dbAdd (data, callback) {
console.log(LOG_WS + 'dbAdd client ID: ', socket.client.id, ' data: ', data); console.log(LOG_WS + 'dbAdd client ID: ', socket.client.id, ' data: ', data);
var collection = supportedCollections[data.collection]; var collection = supportedCollections[data.collection];
var maxtimediff = times.mins(1).msecs; var maxtimediff = times.secs(2).msecs;
var check = checkConditions('dbAdd', data); var check = checkConditions('dbAdd', data);
if (check) { if (check) {
@@ -523,7 +536,7 @@ function init (env, ctx, server) {
// [, status : true ] // [, status : true ]
// } // }
socket.on('authorize', function authorize (message, callback) { 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) { verifyAuthorization(message, remoteIP, function verified (err, authorization) {
if (err) { 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'); // console.log(LOG_WS + 'running websocket.update');
if (lastData.sgvs) { if (lastData.sgvs) {
var delta = calcData(lastData, ctx.ddata); var delta = calcData(lastData, ctx.ddata);
@@ -586,25 +586,6 @@ function init (env, ctx, server) {
lastData = ctx.ddata.clone(); 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(); start();
listeners(); listeners();
@@ -612,6 +593,10 @@ function init (env, ctx, server) {
ctx.storageSocket.init(io); ctx.storageSocket.init(io);
} }
if (ctx.alarmSocket) {
ctx.alarmSocket.init(io);
}
return websocket(); return websocket();
} }
+11
View File
@@ -70,6 +70,9 @@ function init () {
, frameName8: '' , frameName8: ''
, authFailDelay: 5000 , authFailDelay: 5000
, adminNotifiesEnabled: true , adminNotifiesEnabled: true
, obscured: ''
, obscureDeviceProvenance: ''
, authenticationPromptOnLoad: false
}; };
var secureSettings = [ var secureSettings = [
@@ -78,6 +81,8 @@ function init () {
, 'developerTeamId' , 'developerTeamId'
, 'userName' , 'userName'
, 'password' , 'password'
, 'obscured'
, 'obscureDeviceProvenance'
]; ];
var valueMappers = { var valueMappers = {
@@ -107,6 +112,7 @@ function init () {
, bgTargetBottom: mapNumber , bgTargetBottom: mapNumber
, authFailDelay: mapNumber , authFailDelay: mapNumber
, adminNotifiesEnabled: mapTruthy , adminNotifiesEnabled: mapTruthy
, authenticationPromptOnLoad: mapTruthy
}; };
function filterObj(obj, secureKeys) { function filterObj(obj, secureKeys) {
@@ -129,6 +135,9 @@ function init () {
function filteredSettings(settingsObject) { function filteredSettings(settingsObject) {
let so = _.cloneDeep(settingsObject); let so = _.cloneDeep(settingsObject);
if (so.obscured) {
so.enable = _.difference(so.enable, so.obscured);
}
return filterObj(so, secureSettings); return filterObj(so, secureSettings);
} }
@@ -244,6 +253,7 @@ function init () {
var enable = getAndPrepare('enable'); var enable = getAndPrepare('enable');
var disable = getAndPrepare('disable'); var disable = getAndPrepare('disable');
var obscured = getAndPrepare('obscured');
settings.alarmTypes = prepareAlarmTypes(); settings.alarmTypes = prepareAlarmTypes();
@@ -266,6 +276,7 @@ function init () {
//all enabled feature, without any that have been disabled //all enabled feature, without any that have been disabled
settings.enable = _.difference(enable, disable); settings.enable = _.difference(enable, disable);
settings.obscured = obscured;
var thresholds = settings.thresholds; var thresholds = settings.thresholds;
+1 -2
View File
@@ -1,12 +1,11 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var moment = require('moment-timezone');
var units = require('./units')(); var units = require('./units')();
function init(ctx) { function init(ctx) {
var moment = ctx.moment;
var settings = ctx.settings; var settings = ctx.settings;
var translate = ctx.language.translate; var translate = ctx.language.translate;
var timeago = require('./plugins/timeago')(ctx); var timeago = require('./plugins/timeago')(ctx);
+4239 -7199
View File
File diff suppressed because it is too large Load Diff
+39 -40
View File
@@ -1,6 +1,6 @@
{ {
"name": "nightscout", "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.", "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", "license": "AGPL-3.0",
"author": "Nightscout Team", "author": "Nightscout Team",
@@ -27,16 +27,16 @@
}, },
"scripts": { "scripts": {
"start": "node lib/server/server.js", "start": "node lib/server/server.js",
"test": "env-cmd -f ./my.test.env 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 --require ./tests/hooks.js --exit ./tests/$TEST.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 --require ./tests/hooks.js --exit ./tests/*.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", "env": "env",
"postinstall": "webpack --mode production --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 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 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", "bundle-analyzer": "webpack --mode development --config webpack/webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json",
"generate-keys": "node bin/generateRandomString.js >tmp/randomString", "post-generate-keys": "node bin/generateRandomString.js >node_modules/.cache/_ns_cache/randomString",
"coverage": "cat ./coverage/lcov.info | env-cmd -f ./tests/ci.test.env codacy-coverage", "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": "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", "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", "prod": "env-cmd -f ./my.prod.env node lib/server/server.js 0.0.0.0",
@@ -65,18 +65,17 @@
} }
}, },
"engines": { "engines": {
"node": "^10.22.0 || ^12.18.4", "node": "^16.x || ^14.x",
"npm": "^6.14.6" "npm": "^6.x"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.11.1", "@babel/core": "^7.18.10",
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.18.10",
"@parse/node-apn": "^5.1.3",
"acorn": "^8.0.5", "acorn": "^8.0.5",
"acorn-jsx": "^5.3.1", "acorn-jsx": "^5.3.1",
"apn": "^2.2.0",
"async": "^0.9.2", "async": "^0.9.2",
"babel-loader": "^8.1.0", "babel-loader": "^8.2.5",
"base64url": "^3.0.1",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bootevent": "0.0.1", "bootevent": "0.0.1",
"braces": "^3.0.2", "braces": "^3.0.2",
@@ -89,74 +88,74 @@
"d3": "^5.16.0", "d3": "^5.16.0",
"dompurify": "^2.2.6", "dompurify": "^2.2.6",
"easyxml": "^2.0.1", "easyxml": "^2.0.1",
"ejs": "^2.7.4", "ejs": "^3.1.8",
"errorhandler": "^1.5.1", "errorhandler": "^1.5.1",
"event-stream": "3.3.4", "event-stream": "3.3.4",
"expose-loader": "^2.0.0", "expose-loader": "^2.0.0",
"express": "^4.17.1", "express": "4.17.1",
"express-minify": "^1.0.0", "express-minify": "^1.0.0",
"fast-password-entropy": "^1.1.1", "fast-password-entropy": "^1.1.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"flot": "^0.8.3", "flot": "^0.8.3",
"forwarded-for": "^1.1.0",
"helmet": "^4.0.0", "helmet": "^4.0.0",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"jquery-ui-bundle": "^1.12.1-migrate", "jquery-ui-bundle": "^1.12.1-migrate",
"jquery.tooltips": "^1.0.0", "jquery.tooltips": "^1.0.0",
"js-storage": "^1.1.0", "js-storage": "^1.1.0",
"jsdom": "^11.11.0", "jsdom": "=11.11.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"mime": "^2.4.6", "mime": "^2.4.6",
"minimed-connect-to-nightscout": "^1.5.0", "minimed-connect-to-nightscout": "^1.5.5",
"moment": "^2.27.0", "moment": "^2.27.0",
"moment-locales-webpack-plugin": "^1.2.0", "moment-locales-webpack-plugin": "^1.2.0",
"moment-timezone": "^0.5.31", "moment-timezone": "^0.5.31",
"moment-timezone-data-webpack-plugin": "^1.3.0", "moment-timezone-data-webpack-plugin": "^1.5.0",
"mongo-url-parser": "^1.0.1", "mongo-url-parser": "^1.0.2",
"mongodb": "^3.6.0", "mongodb": "^3.6.0",
"mongomock": "^0.1.2", "mongomock": "^0.1.2",
"nightscout-connect": "^0.0.12",
"node-cache": "^4.2.1", "node-cache": "^4.2.1",
"parse-duration": "^0.1.3", "parse-duration": "^0.1.3",
"pem": "^1.14.4",
"process": "^0.11.10", "process": "^0.11.10",
"pushover-notifications": "^1.2.2", "pushover-notifications": "^1.2.2",
"random-token": "0.0.8", "random-token": "0.0.8",
"request": "^2.88.2", "request": "^2.88.2",
"semver": "^6.3.0", "semver": "^6.3.0",
"share2nightscout-bridge": "^0.2.4", "share2nightscout-bridge": "^0.2.9",
"shiro-trie": "^0.4.9", "shiro-trie": "^0.4.9",
"simple-statistics": "^0.7.0", "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", "stream-browserify": "^3.0.0",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"swagger-ui-dist": "^3.32.1", "swagger-ui-dist": "^4.13.2",
"swagger-ui-express": "^4.1.4", "swagger-ui-express": "^4.5.0",
"traverse": "^0.6.6", "traverse": "^0.6.6",
"uuid": "^3.4.0", "uuid": "^9.0.0",
"webpack": "^5.20.2", "webpack": "^5.74.0",
"webpack-cli": "^4.5.0" "webpack-cli": "^4.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/tough-cookie": "^4.0.0", "@types/tough-cookie": "^4.0.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"benv": "^3.3.0", "benv": "^3.3.0",
"codacy-coverage": "^3.4.0",
"csv-parse": "^4.12.0", "csv-parse": "^4.12.0",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"eslint": "^7.19.0", "eslint": "^7.19.0",
"eslint-plugin-security": "^1.4.0", "eslint-plugin-security": "^1.4.0",
"eslint-webpack-plugin": "^2.4.3", "eslint-webpack-plugin": "^2.7.0",
"mocha": "^8.1.1", "mocha": "^8.4.0",
"nodemon": "^1.19.4", "nodemon": "^2.0.19",
"nyc": "^14.1.1", "nyc": "^14.1.1",
"should": "^13.2.3", "should": "^13.2.3",
"supertest": "^3.4.2", "supertest": "^3.4.2",
"webpack-bundle-analyzer": "^4.4.0", "webpack-bundle-analyzer": "^4.5.0",
"webpack-dev-middleware": "^4.1.0", "webpack-dev-middleware": "^4.3.0",
"webpack-hot-middleware": "^2.25.0", "webpack-hot-middleware": "^2.25.2",
"xml2js": "^0.4.23" "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) { before(function (done) {
benv.setup(function() { 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.$ = $; self.$ = $;
+48 -6
View File
@@ -4,8 +4,12 @@ var request = require('supertest');
var load = require('./fixtures/load'); var load = require('./fixtures/load');
var bootevent = require('../lib/server/bootevent'); var bootevent = require('../lib/server/bootevent');
var language = require('../lib/language')(); var language = require('../lib/language')();
const _ = require('lodash');
require('should'); require('should');
const FIVE_MINUTES=1000*60*5;
describe('Entries REST api', function ( ) { describe('Entries REST api', function ( ) {
var entries = require('../lib/api/entries/'); var entries = require('../lib/api/entries/');
var self = this; var self = this;
@@ -24,17 +28,38 @@ describe('Entries REST api', function ( ) {
bootevent(self.env, language).boot(function booted (ctx) { bootevent(self.env, language).boot(function booted (ctx) {
self.app.use('/', entries(self.app, self.wares, ctx, self.env)); self.app.use('/', entries(self.app, self.wares, ctx, self.env));
self.archive = require('../lib/server/entries')(self.env, ctx); self.archive = require('../lib/server/entries')(self.env, ctx);
self.ctx = ctx;
var creating = load('json'); done();
creating.push({type: 'sgv', sgv: 100, date: Date.now()});
self.archive.create(creating, done);
}); });
}); });
beforeEach(function (done) { beforeEach(function (done) {
var creating = load('json'); 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) { 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) { it('/echo/ api shows query', function (done) {
request(self.app) request(self.app)
+16 -2
View File
@@ -29,7 +29,7 @@ describe('Security of REST API V1', function() {
self.app.use('/api/v2/authorization', ctx.authorization.endpoints); self.app.use('/api/v2/authorization', ctx.authorization.endpoints);
let authResult = await authSubject(ctx.authorization.storage); let authResult = await authSubject(ctx.authorization.storage);
self.subject = authResult.subject; self.subject = authResult.subject;
self.token = authResult.token; self.token = authResult.accessToken;
done(); 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) { it('Data load should succeed with API SECRET', function(done) {
request(self.app) request(self.app)
.get('/api/v1/entries.json') .get('/api/v1/entries.json')
@@ -138,7 +152,7 @@ describe('Security of REST API V1', function() {
.expect(200) .expect(200)
.end(function(err, res) { .end(function(err, res) {
res.body.message.message.should.equal('OK'); res.body.message.message.should.equal('OK');
res.body.message.isAdmin.should.equal(true); res.body.message.isAdmin.should.equal(true);
done(); done();
}); });
}); });

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