Merge pull request #6836 from nightscout/dev

## Welcome to Release 14.2 Sweet Liquorice!

This release focuses on improving the overall security of Nightscout. 14.2 adds a new method for Nightscout to notify you of various security issues in your setup. After upgrading to the this release, if Nightscout wants to tell you something about the system security, you'll see a red megaphone appear in the Nightscout web client. To see the messages, you'll have to sign in using your API-SECRET or a token that's got administration privileges. Full details of the messages can be found in the Nightscout documentation: https://nightscout.github.io/nightscout/security/

Advance warning regarding future releases: we are likely to make compatibility breaking changes in upcoming releases that will change how the authentication flows with the Nightscout API works, along with changes to validation of data sent to Nightscout. If you're an app developer and are using the Nightscout APIs in your application, please join our Discord channel to learn about the changes are they're implemented. You can join the channel here: https://discord.gg/zg7CvCQ

Nightscout translations are now made in Crowdin. This is very easy even for non-technical folks, so please join and contribute! https://crowdin.com/project/nightscout

Note if you're running your instance with a very old MongoDB version, your installation might break. We've tested the release using MongoDB 4.2 and 4.4.

## New Features and Improvements

* Administration messages support
* Bolus bubble rendering in Nightscout UI is now more configurable, see the new Settings in the client settings panel
* You can now configure Nightscout to disable battery alarms during night
* Security improvement: treatments and CGM entries sent over the REST API V1 are now filtered for XSS injection code
* A lot of work has been put into localization, huge thanks to all the contributors
* Reports now remember the settings you've chosen across sessions
* Alexa integration now supports Spanish
* Fixed a bug with AAPS updating CGM values after Dexcom rounds the value
* Added support for Portuguese and Slovenian
* Support for Traditional Chinese has been removed until we find a contributor to help with translating more of the software. The next release will remove support for Japanese unless a larger portion of the text has been translated by time of release.

## For developers 

* APIV3 results are now wrapped differently from before
* Webpack was upgraded to V5
* Client JS bundling was simplified to just one bundle, cutting down bundling time to ~50% of current
* Removed cache invalidation token from bundling process and generating it on server boot
* Security improvement: generate strong persistent random string on deploy to use for JWT signing instead of api_secret
* Security improvement: moved api-secret and JWT signing to a separate centralized security component and deletes api_secret from environment, so it's not accessible elsewhere
* Security improvement: Clients can now send the api_secret using SHA512
* Moved some server components away from project root to make it easier to see what code runs in server vs client
* Fixes some issues reported by linter
This commit is contained in:
Sulka Haro
2021-02-16 13:57:44 +02:00
committed by GitHub
200 changed files with 24012 additions and 6940 deletions
+28 -16
View File
@@ -1,23 +1,35 @@
module.exports = {
"plugins": [ ],
"extends": [
"eslint:recommended"
'plugins': [
'security'
],
"parser": "babel-eslint",
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true,
"mocha": true,
"jquery": true
'extends': [
'eslint:recommended',
'plugin:security/recommended'
],
'parser': 'babel-eslint',
'env': {
'browser': true,
'commonjs': true,
'es6': true,
'node': true,
'mocha': true,
'jquery': true
},
"rules": {
"no-unused-vars": [
"error",
'rules': {
'security/detect-object-injection' : 0,
'no-unused-vars': [
'error',
{
"varsIgnorePattern": "should|expect"
'varsIgnorePattern': 'should|expect'
}
]
}
},
'overrides': [
{
'files': ['lib/client/*.js'],
'rules': {
'security/detect-object-injection': 0
}
}
],
};
+1
View File
@@ -25,6 +25,7 @@ jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/dev' && github.repository_owner == 'nightscout'
strategy:
fail-fast: false
+1 -1
View File
@@ -9,6 +9,7 @@ bundle/bundle.out.js
*.iml
my.env
my.*.env
*.pem
static/bower_components/
.*.sw?
@@ -28,5 +29,4 @@ npm-debug.log
/cgm-remote-monitor.njsproj
/cgm-remote-monitor.sln
/obj/Debug
/bin
/*.bat
+1 -1
View File
@@ -15,4 +15,4 @@ RUN npm install && \
EXPOSE 1337
CMD ["node", "server.js"]
CMD ["node", "lib/server/server.js"]
+1 -1
View File
@@ -1 +1 @@
web: node server.js
web: node lib/server/server.js
+13 -2
View File
@@ -104,6 +104,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
- [`bage` (Battery Age)](#bage-battery-age)
- [`treatmentnotify` (Treatment Notifications)](#treatmentnotify-treatment-notifications)
- [`basal` (Basal Profile)](#basal-basal-profile)
- [`bolus` (Bolus Rendering)](#bolus-bolus-rendering)
- [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge)
- [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge)
- [`pump` (Pump Monitoring)](#pump-pump-monitoring)
@@ -294,6 +295,8 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
### Predefined values for your browser settings (optional)
* `TIME_FORMAT` (`12`)- possible values `12` or `24`
* `DAY_START` (`7.0`) - time for start of day (0.0 - 24.0) for features using day time / night time options
* `DAY_END` (`21.0`) - time for end of day (0.0 - 24.0) for features using day time / night time options
* `NIGHT_MODE` (`off`) - possible values `on` or `off`
* `SHOW_RAWBG` (`never`) - possible values `always`, `never` or `noise`
* `CUSTOM_TITLE` (`Nightscout`) - Title for the main view
@@ -311,7 +314,6 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
* The `linear` option has equidistant tick marks; the range used is dynamic so that space at the top of chart isn't wasted.
* The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale.
* `EDIT_MODE` (`on`) - possible values `on` or `off`. Enables the icon allowing for editing of treatments in the main view.
* `BOLUS_RENDER_OVER` (1) - U value over which the bolus values are rendered on the chart if the 'x U and Over' option is selected. This value can be an integer or a float, e.g. 0.3, 1.5, 2, etc...
### Predefined values for your server settings (optional)
* `INSECURE_USE_HTTP` (`false`) - Redirect unsafe http traffic to https. Possible values `false`, or `true`. Your site redirects to `https` by default. If you don't want that from Nightscout, but want to implement that with a Nginx or Apache proxy, set `INSECURE_USE_HTTP` to `true`. Note: This will allow (unsafe) http traffic to your Nightscout instance and is not recommended.
@@ -468,12 +470,20 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
* `BAGE_URGENT` (`360`) - If time since last `Pump Battery Change` matches `BAGE_URGENT` hours, user will be issued a persistent warning of overdue change (default of 360 hours is 15 days).
##### `treatmentnotify` (Treatment Notifications)
Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment. Default snooze is 10 minutes, and can be set using the `TREATMENTNOTIFY_SNOOZE_MINS` [extended setting](#extended-settings).
Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment.
* `TREATMENTNOTIFY_SNOOZE_MINS` (`10`) - Number of minutes to snooze notifications after a treatment is entered
* `TREATMENTNOTIFY_INCLUDE_BOLUSES_OVER` (`0`) - U value over which the bolus will trigger a notification and snooze alarms
##### `basal` (Basal Profile)
Adds the Basal pill visualization to display the basal rate for the current time. Also enables the `bwp` plugin to calculate correction temp basal suggestions. Uses the `basal` field from the [treatment profile](#treatment-profile). Also uses the extended setting:
* `BASAL_RENDER` (`none`) - Possible values are `none`, `default`, or `icicle` (inverted)
##### `bolus` (Bolus Rendering)
Settings to configure Bolus rendering
* `BOLUS_RENDER_OVER` (`0`) - U value over which the bolus labels use the format defined in `BOLUS_RENDER_FORMAT`. This value can be an integer or a float, e.g. 0.3, 1.5, 2, etc.
* `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).
##### `bridge` (Share2Nightscout bridge)
Glucose reading directly from the Dexcom Share service, uses these extended settings:
* `BRIDGE_USER_NAME` - Your username for the Share service.
@@ -511,6 +521,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
* `PUMP_URGENT_BATT_P` (`20`) - The % of the pump battery remaining, an urgent alarm will be triggered when dropping below this threshold.
* `PUMP_WARN_BATT_V` (`1.35`) - The voltage (if percent isn't available) of the pump battery, a warning will be triggered when dropping below this threshold.
* `PUMP_URGENT_BATT_V` (`1.30`) - The voltage (if percent isn't available) of the pump battery, an urgent alarm will be triggered when dropping below this threshold.
* `PUMP_WARN_BATT_QUIET_NIGHT` (`false`) - Do not generate battery alarms at night.
##### `openaps` (OpenAPS)
Integrated OpenAPS loop monitoring, uses these extended settings:
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
require('crypto').randomBytes(1024, function(err, buffer) {
var token = buffer.toString('hex');
console.log(token);
});
+117
View File
@@ -0,0 +1,117 @@
'use strict';
const axios = require('axios');
const moment = require('moment');
const crypto = require('crypto');
const shasum = crypto.createHash('sha1');
const FIVE_MINUTES = 1000 * 60 * 5;
if (process.argv.length < 4) {
console.error('This is an utility to send continuous CGM entry data to a test Nightscout server')
console.error('USAGE: node testdatarunner.js <SERVER BASE URL> <API_SECRET>');
process.exit();
}
const URL = process.argv[2];
const SECRET = process.argv[3];
shasum.update(SECRET);
const SECRET_SHA1 = shasum.digest('hex');
const HEADERS = {'api-secret': SECRET_SHA1};
const ENTRIES_URL = URL + '/api/v1/entries';
var done = (function wait () { if (!done) setTimeout(wait, 1000); })();
console.log('NS data filler active');
const entry = {
device: 'Dev simulator',
date: 1609061083612,
dateString: '2020-12-27T09:24:43.612Z',
sgv: 100,
delta: 0,
direction: 'Flat',
type: 'sgv'
};
function addEntry () {
console.log('Sending add new entry');
sendEntry(Date.now());
setTimeout(addEntry, FIVE_MINUTES);
}
function oscillator(time, frequency = 1, amplitude = 1, phase = 0, offset = 0){
return Math.sin(time * frequency * Math.PI * 2 + phase * Math.PI * 2) * amplitude + offset;
}
async function sendFail() {
try {
console.log('Sending fail');
const response = await axios.post(ENTRIES_URL, entry, {headers: {'api-secret': 'incorrect' }});
} catch (e) { }
}
async function sendEntry (date) {
const m = moment(date);
entry.date = date;
entry.dateString = m.toISOString();
entry.sgv = 100 + Math.round(oscillator(date / 1000, 1/(60*60), 30));
console.log('Adding entry', entry);
const response = await axios.post(ENTRIES_URL, entry, {headers: HEADERS});
if (date > Date.now() - 5000) sendFail();
}
(async () => {
try {
console.log('GETTING', ENTRIES_URL);
const response = await axios.get(ENTRIES_URL, {headers: HEADERS} );
const latestEntry = response.data ? response.data[0] : null;
if (!latestEntry) {
// Fill in history
console.log('I would fill in history');
const totalToSave = 24;
const now = Date.now();
const start = now - ( totalToSave * FIVE_MINUTES);
let current = start;
while (current <= now) {
await sendEntry(current);
current += FIVE_MINUTES;
}
setTimeout(addEntry, 1000*60*5);
} else {
let latestDate = latestEntry.date;
const now = Date.now();
if ((now - latestDate) > FIVE_MINUTES) {
console.log('We got data but it is older than 5 minutes, makign a partial fill');
let current = latestDate + FIVE_MINUTES;
while (current < now) {
await sendEntry(current);
current += FIVE_MINUTES;
}
latestDate = current;
} else {
console.log('Looks like we got history, not filling');
}
setTimeout(addEntry, Date.now() - latestDate);
}
sendFail();
sendFail();
} catch (error) {
console.log(error.response.data);
}
})();
+2
View File
@@ -5,6 +5,8 @@ console.info('Nightscout report bundle start');
window.Nightscout.report_plugins_preinit = require('../lib/report_plugins/');
window.Nightscout.predictions = require('../lib/report/predictions');
window.Nightscout.reportclient = require('../lib/report/reportclient');
window.Nightscout.profileclient = require('../lib/profile/profileeditor');
window.Nightscout.foodclient = require('../lib/food/food');
console.info('Nightscout report bundle ready');
+6
View File
@@ -28,6 +28,12 @@ window.Nightscout = {
admin_plugins: require('../lib/admin_plugins/')()
};
window.Nightscout.report_plugins_preinit = require('../lib/report_plugins/');
window.Nightscout.predictions = require('../lib/report/predictions');
window.Nightscout.reportclient = require('../lib/report/reportclient');
window.Nightscout.profileclient = require('../lib/profile/profileeditor');
window.Nightscout.foodclient = require('../lib/food/food');
console.info('Nightscout bundle ready');
// Needed for Hot Module Replacement
+320
View File
@@ -0,0 +1,320 @@
{
"interactionModel": {
"languageModel": {
"invocationName": "mi monitor",
"intents": [
{
"name": "NSStatus",
"slots": [],
"samples": [
"Como lo estoy haciendo"
]
},
{
"name": "LastLoop",
"slots": [],
"samples": [
"Cuando fue mi ultimo bucle"
]
},
{
"name": "MetricNow",
"slots": [
{
"name": "metric",
"type": "LIST_OF_METRICS",
"samples": [
"que es {pwd} {metric}",
"cual es mi {metric}",
"como es {pwd} {metric}",
"como es {metric}",
"cuanta {metric} tiene {pwd}",
"cuanta {metric} tengo",
"cuanta {metric}",
"{pwd} {metric}",
"{metric}",
"mi {metric}"
]
},
{
"name": "pwd",
"type": "AMAZON.FirstName"
}
],
"samples": [
"cuanto {metric} le queda a {pwd}",
"cual es mi {metric}",
"cuanta {metric} queda",
"Cuanta {metric}",
"como es {metric}",
"como es mi {metric}",
"como es {pwd} {metric}",
"como esta mi {metric}",
"que es {metric}",
"cuanta {metric} tengo",
"cuanta {metric} tiene {pwd}",
"que es {pwd} {metric}"
]
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
}
],
"types": [
{
"name": "LIST_OF_METRICS",
"values": [
{
"name": {
"value": "delta",
"synonyms": [
"delta de glucosa en sangre",
"delta de azucar en sangre",
"delta azucar",
"delta glucosa"
]
}
},
{
"name": {
"value": "uploader battery",
"synonyms": [
"bateria restante del cargador",
"carga de la batera"
]
}
},
{
"name": {
"value": "pump reservoir",
"synonyms": [
"insulina restante",
"queda insulina",
"insulina que queda",
"insulina en mi bomba",
"insulina"
]
}
},
{
"name": {
"value": "pump battery",
"synonyms": [
"bateria de la bomba restante",
"bomba de energia de la bateria"
]
}
},
{
"name": {
"value": "bg",
"synonyms": [
"numero",
"glucosa",
"azucar en sangre",
"glucosa en sangre"
]
}
},
{
"name": {
"value": "iob",
"synonyms": [
"insulina que tengo",
"insulina a bordo"
]
}
},
{
"name": {
"value": "basal",
"synonyms": [
"basal que tengo",
"basal",
"basal actual"
]
}
},
{
"name": {
"value": "cob",
"synonyms": [
"carbohidratos",
"carbohidratos a bordo",
"carbo hidratos",
"carbohidratos que tengo"
]
}
},
{
"name": {
"value": "forecast",
"synonyms": [
"prevision ar2",
"prevision del bucle"
]
}
},
{
"name": {
"value": "raw bg",
"synonyms": [
"numero bruto",
"azucar en sangre en bruto",
"glucosa en sangre en bruto"
]
}
},
{
"name": {
"value": "cgm noise",
"synonyms": [
"ruido cgm",
"ruido del cgm"
]
}
},
{
"name": {
"value": "cgm tx age",
"synonyms": [
"edad del transmisor",
"transmisor edad",
"edad del transmisor cgm"
]
}
},
{
"name": {
"value": "cgm tx status",
"synonyms": [
"estado del transmisor",
"estado transmisor",
"estado del transmisor cgm"
]
}
},
{
"name": {
"value": "cgm battery",
"synonyms": [
"nivel de bateria cgm",
"niveles de bateria cgm",
"bateria del cgm",
"bateria del transmisor cgm",
"nivel de bateria del transmisor cgm",
"nivel bateria transmisor cgm",
"nivel bateria del transmisor cgm",
"bateria transmisor",
"nivel bateria transmisor",
"niveles de bateria del transmisor",
"baterias del transmisor"
]
}
},
{
"name": {
"value": "cgm session age",
"synonyms": [
"edad de la sesion"
]
}
},
{
"name": {
"value": "cgm status",
"synonyms": [
"estado cgm",
"estado del cgm"
]
}
},
{
"name": {
"value": "cgm mode",
"synonyms": [
"modo cgm",
"modo del cgm"
]
}
},
{
"name": {
"value": "db size",
"synonyms": [
"ocupacion de la base de datos",
"ocupacion de datos",
"ocupacion fichero"
]
}
}
]
}
]
},
"dialog": {
"intents": [
{
"name": "MetricNow",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "metric",
"type": "LIST_OF_METRICS",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.1421281086569.34001419564"
}
},
{
"name": "pwd",
"type": "AMAZON.FirstName",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
}
]
}
],
"delegationStrategy": "ALWAYS"
},
"prompts": [
{
"id": "Elicit.Slot.1421281086569.34001419564",
"variations": [
{
"type": "PlainText",
"value": "¿Que metrica estas buscando?"
},
{
"type": "PlainText",
"value": "¿Que valor buscas?"
},
{
"type": "PlainText",
"value": "¿Que metrica quieres saber?"
},
{
"type": "PlainText",
"value": "¿Que valor quieres saber?"
}
]
}
]
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ function createOrSaveRole (role, client, callback) {
reload(client, callback);
}).fail(function fail (err) {
console.error('Unable to ' + method + ' Role', err.responseText);
window.alert(client.translate('Unable to %1 Role', { params: [method] }));
window.alert(client.translate('Unable to save Role'));
if (callback) {
callback(err);
}
+1 -1
View File
@@ -45,7 +45,7 @@ function createOrSaveSubject (subject, client, callback) {
reload(client, callback);
}).fail(function fail (err) {
console.error('Unable to ' + method + ' Subject', err.responseText);
window.alert(client.translate('Unable to ' + method + ' Subject'));
window.alert(client.translate('Unable to save Subject'));
if (callback) {
callback();
}
+52
View File
@@ -0,0 +1,52 @@
'use strict';
const _ = require('lodash');
function init (ctx) {
const adminnotifies = {};
adminnotifies.addNotify = function addnotify (notify) {
if (!notify) return;
notify.title = notify.title || 'No title';
notify.message = notify.message || 'No message';
const existingMessage = _.find(adminnotifies.notifies, function findExisting (obj) {
return obj.message == notify.message;
});
if (existingMessage) {
existingMessage.count += 1;
existingMessage.lastRecorded = Date.now();
} else {
notify.count = 1;
notify.lastRecorded = Date.now();
adminnotifies.notifies.push(notify);
}
}
adminnotifies.getNotifies = function getNotifies () {
return adminnotifies.notifies;
}
ctx.bus.on('admin-notify', adminnotifies.addNotify);
adminnotifies.clean = function cleanNotifies () {
adminnotifies.notifies = _.filter(adminnotifies.notifies, function findExisting (obj) {
return obj.persistent || ((Date.now() - obj.lastRecorded) < 1000 * 60 * 60 * 12);
});
}
adminnotifies.cleanAll = function cleanAll() {
adminnotifies.notifies = [];
}
adminnotifies.cleanAll();
ctx.bus.on('tick', adminnotifies.clean);
return adminnotifies;
}
module.exports = init;
+1
View File
@@ -22,6 +22,7 @@ function configure(app, wares, ctx) {
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({
+35
View File
@@ -0,0 +1,35 @@
'use strict';
const _ = require('lodash');
const consts = require('../constants');
function configure (ctx) {
const express = require('express')
, api = express.Router();
api.get('/adminnotifies', function(req, res) {
ctx.authorization.resolveWithRequest(req, function resolved (err, result) {
const isAdmin = ctx.authorization.checkMultiple('*:*:admin', result.shiros); //full admin permissions
const response = {
notifies: []
, notifyCount: 0
};
if (ctx.adminnotifies) {
const notifies = _.filter(ctx.adminnotifies.getNotifies(), function isOld (obj) {
return (obj.persistent || (Date.now() - obj.lastRecorded) < 1000 * 60 * 60 * 8);
});
if (isAdmin) { response.notifies = notifies }
response.notifyCount = notifies.length;
}
res.sendJSONStatus(res, consts.HTTP_OK, response);
});
});
return api;
}
module.exports = configure;
+4 -1
View File
@@ -13,7 +13,10 @@ function configure (app, wares, ctx, env) {
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw());
// json body types get handled as parsed json
api.use(wares.bodyParser.json());
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa);
+7 -1
View File
@@ -15,7 +15,10 @@ function configure (app, wares, ctx, env) {
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw());
// json body types get handled as parsed json
api.use(wares.bodyParser.json());
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ extended: true }));
@@ -67,6 +70,9 @@ function configure (app, wares, ctx, env) {
function doPost (req, res) {
var obj = req.body;
ctx.purifier.purifyObject(obj);
ctx.devicestatus.create(obj, function(err, created) {
if (err) {
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
+9 -1
View File
@@ -43,7 +43,10 @@ function configure (app, wares, ctx, env) {
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw());
// json body types get handled as parsed json
api.use(wares.bodyParser.json());
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
// shortcut to use extension to specify output content-type
api.use(wares.extensions([
'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv'
@@ -274,6 +277,11 @@ function configure (app, wares, ctx, env) {
incoming = incoming.concat(req.body);
}
for (let i = 0; i < incoming.length; i++) {
const e = incoming[i];
ctx.purifier.purifyObject(e);
}
/**
* @function persist
* @returns {WritableStream} a writable persistent storage stream
+4 -1
View File
@@ -11,7 +11,10 @@ function configure (app, wares, ctx) {
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw( ));
// json body types get handled as parsed json
api.use(wares.bodyParser.json( ));
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ extended: true }));
+7 -4
View File
@@ -13,13 +13,13 @@ function create (env, ctx) {
app.set('version', env.version);
app.set('units', env.DISPLAY_UNITS);
// Only allow access to the API if API_SECRET is set on the server.
// Only allow access to the API if API KEY is set on the server.
app.disable('api');
if (env.api_secret) {
console.log('API_SECRET present, enabling API');
if (env.enclave.isApiKeySet()) {
console.log('API KEY present, enabling API');
app.enable('api');
} else {
console.log('API_SECRET not found, API disabled');
console.log('API KEY has not been set, API disabled');
}
if (env.settings.enable) {
@@ -58,6 +58,9 @@ function create (env, ctx) {
app.all('/activity*', require('./activity/')(app, wares, ctx));
app.use('/', wares.sendJSONStatus, require('./verifyauth')(ctx));
app.use('/', wares.sendJSONStatus, require('./adminnotifiesapi')(ctx));
app.all('/food*', require('./food/')(app, wares, ctx));
// Status first
+5 -1
View File
@@ -11,7 +11,10 @@ function configure (app, wares, ctx) {
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw( ));
// json body types get handled as parsed json
api.use(wares.bodyParser.json( ));
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({ extended: true }));
@@ -61,6 +64,7 @@ function configure (app, wares, ctx) {
// create new record
api.post('/profile/', ctx.authorization.isPermitted('api:profile:create'), function(req, res) {
var data = req.body;
ctx.purifier.purifyObject(data);
ctx.profile.create(data, function (err, created) {
if (err) {
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
+4
View File
@@ -15,6 +15,7 @@ function configure (app, wares, ctx, env) {
api.use(wares.compression());
api.use(wares.bodyParser({
limit: 1048576 * 50
, extended: true
}));
// text body types get handled as raw buffer stream
api.use(wares.bodyParser.raw({
@@ -23,6 +24,7 @@ function configure (app, wares, ctx, env) {
// json body types get handled as parsed json
api.use(wares.bodyParser.json({
limit: 1048576
, extended: true
}));
// also support url-encoded content-type
api.use(wares.bodyParser.urlencoded({
@@ -119,6 +121,8 @@ function configure (app, wares, ctx, env) {
t.created_at = new Date().toISOString();
}
ctx.purifier.purifyObject(t);
/*
if (!t.created_at) {
console.log('Trying to create treatment without created_at field', t);
+3 -6
View File
@@ -1,7 +1,6 @@
{
"API3_VERSION": "3.0.0-alpha",
"API3_VERSION": "3.0.3-alpha",
"API3_SECURITY_ENABLE": true,
"API3_TIME_SKEW_TOLERANCE": 5,
"API3_DEDUP_FALLBACK_ENABLED": true,
"API3_CREATED_AT_FALLBACK_ENABLED": true,
"API3_MAX_LIMIT": 1000,
@@ -34,13 +33,11 @@
"HTTP_400_SORT_SORT_DESC": "Parameters sort and sort_desc cannot be combined",
"HTTP_400_UNSUPPORTED_FILTER_OPERATOR": "Unsupported filter operator {0}",
"HTTP_400_IMMUTABLE_FIELD": "Field {0} cannot be modified by the client",
"HTTP_401_BAD_DATE": "Bad Date header",
"HTTP_401_BAD_TOKEN": "Bad access token or JWT",
"HTTP_401_DATE_OUT_OF_TOLERANCE": "Date header out of tolerance",
"HTTP_401_MISSING_DATE": "Missing Date header",
"HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT",
"HTTP_403_MISSING_PERMISSION": "Missing permission {0}",
"HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS",
"HTTP_404_BAD_OPERATION": "Bad operation or collection",
"HTTP_406_UNSUPPORTED_FORMAT": "Unsupported output format requested",
"HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document",
"HTTP_500_INTERNAL_ERROR": "Internal Server Error",
@@ -52,4 +49,4 @@
"MIN_TIMESTAMP": 946684800000,
"MIN_UTC_OFFSET": -1440,
"MAX_UTC_OFFSET": 1440
}
}
+28 -25
View File
@@ -18,31 +18,34 @@ The server replies with `406 Not Acceptable` HTTP status in case of not supporte
Default content type is JSON, output can look like this:
```
[
{
"type":"sgv",
"sgv":"171",
"dateString":"2014-07-19T02:44:15.000-07:00",
"date":1405763055000,
"device":"dexcom",
"direction":"Flat",
"identifier":"5c5a2404e0196f4d3d9a718a",
"srvModified":1405763055000,
"srvCreated":1405763055000
},
{
"type":"sgv",
"sgv":"176",
"dateString":"2014-07-19T03:09:15.000-07:00",
"date":1405764555000,
"device":"dexcom",
"direction":"Flat",
"identifier":"5c5a2404e0196f4d3d9a7187",
"srvModified":1405764555000,
"srvCreated":1405764555000
}
]
```json
{
"status": 200,
"result": [
{
"type": "sgv",
"sgv": "171",
"dateString": "2014-07-19T02:44:15.000-07:00",
"date": 1405763055000,
"device": "dexcom",
"direction": "Flat",
"identifier": "5c5a2404e0196f4d3d9a718a",
"srvModified": 1405763055000,
"srvCreated": 1405763055000
},
{
"type": "sgv",
"sgv": "176",
"dateString": "2014-07-19T03:09:15.000-07:00",
"date": 1405764555000,
"device": "dexcom",
"direction": "Flat",
"identifier": "5c5a2404e0196f4d3d9a7187",
"srvModified": 1405764555000,
"srvCreated": 1405764555000
}
]
}
```
### XML
-16
View File
@@ -27,22 +27,6 @@ There are two ways to authorize API calls:
- 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)
---
### Client timestamps
As previously mentioned, a potential attacker cannot decrypt the captured messages, but he can send them back to the client/server at any later time. APIv3 is partially preventing this by the temporal validity of each secured API call.
The client must include his current timestamp to each call so that the server can compare it against its clock. If the timestamp difference is not within the limit, the request is considered invalid. The tolerance limit is set in minutes in the `API3_TIME_SKEW_TOLERANCE` environment variable.
There are two ways to include the client timestamp to the call:
- use `now` query parameter with UNIX epoch millisecond timestamp, eg. `now=1565041446908`
- add HTTP `Date` header to the request, eg. `Date: Sun, 12 May 2019 07:49:58 GMT`
The client can check each server response in the same way, because each response contains a server timestamp in the HTTP *Date* header as well.
---
APIv3 security is enabled by default, but it can be completely disabled for development and debugging purposes by setting the web environment variable `API3_SECURITY_ENABLE=false`.
This setting is hazardous and it is strongly discouraged to be used for production purposes!
+143 -115
View File
@@ -22,15 +22,18 @@ request('https://nsapiv3.herokuapp.com/api/v3/version',
(error, response, body) => console.log(body));
```
Sample result:
```javascript
{
"version":"0.12.2",
"apiVersion":"3.0.0-alpha",
"srvDate":1564386001772,
"storage":{
"storage":"mongodb",
"version":"3.6.12"
}
```json
{
"status": 200,
"result": {
"version": "14.1.0",
"apiVersion": "3.0.2-alpha",
"srvDate": 1609402081548,
"storage": {
"storage": "mongodb",
"version": "4.2.11"
}
}
}
```
@@ -44,28 +47,31 @@ It is public (there is no need to add authorization parameters/headers).
Sample GET `/status` client code (to get my actual permissions):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`,
(error, response, body) => console.log(body));
```
Sample result:
```javascript
{
"version":"0.12.2",
"apiVersion":"3.0.0-alpha",
"srvDate":1564391740738,
"storage":{
"storage":"mongodb",
"version":"3.6.12"
},
"apiPermissions":{
"devicestatus":"crud",
"entries":"crud",
"food":"crud",
"profile":"crud",
"settings":"crud",
"treatments":"crud"
```json
{
"status": 200,
"result": {
"version": "14.1.0",
"apiVersion": "3.0.2-alpha",
"srvDate": 1609427571833,
"storage": {
"storage": "mongodb",
"version": "4.2.11"
},
"apiPermissions": {
"devicestatus": "crud",
"entries": "crud",
"food": "crud",
"profile": "crud",
"settings": "crud",
"treatments": "crud"
}
}
}
```
@@ -80,30 +86,33 @@ Sample result:
Sample GET `/entries` client code (to retrieve last 3 BG values):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&limit=3&fields=dateString,sgv,direction`,
(error, response, body) => console.log(body));
```
Sample result:
```
[
{
"dateString":"2019-07-30T02:24:50.434+0200",
"sgv":115,
"direction":"FortyFiveDown"
},
{
"dateString":"2019-07-30T02:19:50.374+0200",
"sgv":121,
"direction":"FortyFiveDown"
},
{
"dateString":"2019-07-30T02:14:50.450+0200",
"sgv":129,
"direction":"FortyFiveDown"
}
]
```json
{
"status": 200,
"result": [
{
"dateString": "2019-07-30T02:24:50.434+0200",
"sgv": 115,
"direction": "FortyFiveDown"
},
{
"dateString": "2019-07-30T02:19:50.374+0200",
"sgv": 121,
"direction": "FortyFiveDown"
},
{
"dateString": "2019-07-30T02:14:50.450+0200",
"sgv": 129,
"direction": "FortyFiveDown"
}
]
}
```
@@ -115,7 +124,7 @@ Sample result:
Sample POST `/treatments` client code:
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const doc = {
date: 1564591511232, // (new Date()).getTime(),
app: 'AndroidAPS',
@@ -129,11 +138,15 @@ request({
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}`
},
(error, response, body) => console.log(response.headers.location));
(error, response, body) => console.log(body));
```
Sample result:
```
/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895
```json
{
"status": 201,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"lastModified": 1564591511711
}
```
@@ -145,26 +158,29 @@ Sample result:
Sample GET `/treatments/{identifier}` client code:
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`,
(error, response, body) => console.log(body));
```
Sample result:
```
{
"date":1564591511232,
"app":"AndroidAPS",
"device":"Samsung XCover 4-861536030196001",
"eventType":"Correction Bolus",
"insulin":0.3,
"identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"utcOffset":0,
"created_at":"2019-07-31T16:45:11.232Z",
"srvModified":1564591627732,
"srvCreated":1564591511711,
"subject":"test-admin"
```json
{
"status": 200,
"result": {
"date": 1564591511232,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus",
"insulin": 0.3,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"utcOffset": 0,
"created_at": "2019-07-31T16:45:11.232Z",
"srvModified": 1564591627732,
"srvCreated": 1564591511711,
"subject": "test-admin"
}
}
```
@@ -177,20 +193,23 @@ Sample result:
Sample GET `/lastModified` client code (to get latest modification dates):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`,
(error, response, body) => console.log(body));
```
Sample result:
```javascript
```json
{
"srvDate":1564591783202,
"collections":{
"devicestatus":1564591490074,
"entries":1564591486801,
"profile":1548524042744,
"treatments":1564591627732
"status": 200,
"result": {
"srvDate": 1564591783202,
"collections": {
"devicestatus": 1564591490074,
"entries": 1564591486801,
"profile": 1548524042744,
"treatments": 1564591627732
}
}
}
```
@@ -204,7 +223,7 @@ Sample result:
Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
const doc = {
date: 1564591511232,
@@ -220,11 +239,13 @@ request({
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(response.statusCode));
(error, response, body) => console.log(body));
```
Sample result:
```
204
```json
{
"status": 200
}
```
@@ -236,7 +257,7 @@ Sample result:
Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
const doc = {
insulin: 0.5
@@ -248,11 +269,13 @@ request({
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(response.statusCode));
(error, response, body) => console.log(body));
```
Sample result:
```
204
```json
{
"status": 200
}
```
@@ -264,18 +287,20 @@ Sample result:
Sample DELETE `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5):
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
request({
method: 'delete',
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(response.statusCode));
(error, response, body) => console.log(body));
```
Sample result:
```
204
```json
{
"status": 200
}
```
@@ -287,43 +312,46 @@ Sample result:
Sample HISTORY `/treatments/history/{lastModified}` client code:
```javascript
const request = require('request');
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
const lastModified = 1564521267421;
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}?${auth}`,
(error, response, body) => console.log(response.body));
```
Sample result:
```
[
{
"date":1564521267421,
"app":"AndroidAPS",
"device":"Samsung XCover 4-861536030196001",
"eventType":"Correction Bolus",
"insulin":0.5,
"utcOffset":0,
"created_at":"2019-07-30T21:14:27.421Z",
"identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"srvModified":1564592440416,
"srvCreated":1564592334853,
"subject":"test-admin",
"modifiedBy":"test-admin",
"isValid":false
},
{
"date":1564592545299,
"app":"AndroidAPS",
"device":"Samsung XCover 4-861536030196001",
"eventType":"Snack Bolus",
"carbs":10,
"identifier":"267c43c2-f629-5191-a542-4f410c69e486",
"utcOffset":0,
"created_at":"2019-07-31T17:02:25.299Z",
"srvModified":1564592545781,
"srvCreated":1564592545781,
"subject":"test-admin"
}
]
```json
{
"status": 200,
"result": [
{
"date": 1564521267421,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus",
"insulin": 0.5,
"utcOffset": 0,
"created_at": "2019-07-30T21:14:27.421Z",
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"srvModified": 1564592440416,
"srvCreated": 1564592334853,
"subject": "test-admin",
"modifiedBy": "test-admin",
"isValid": false
},
{
"date": 1564592545299,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Snack Bolus",
"carbs": 10,
"identifier": "267c43c2-f629-5191-a542-4f410c69e486",
"utcOffset": 0,
"created_at": "2019-07-31T17:02:25.299Z",
"srvModified": 1564592545781,
"srvCreated": 1564592545781,
"subject": "test-admin"
}
]
}
```
Notice the `"isValid":false` field marking the deletion of the document.
+9 -2
View File
@@ -4,6 +4,7 @@ const apiConst = require('../../const.json')
, security = require('../../security')
, validate = require('./validate.js')
, path = require('path')
, opTools = require('../../shared/operationTools')
;
/**
@@ -35,7 +36,13 @@ async function insert (opCtx, doc) {
res.setHeader('Last-Modified', now.toUTCString());
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier));
res.status(apiConst.HTTP.CREATED).send({ });
const fields = {
identifier: identifier,
lastModified: now.getTime()
};
opTools.sendJSON({ res, status: apiConst.HTTP.CREATED, fields: fields });
ctx.bus.emit('storage-socket-create', { colName: col.colName, doc });
col.autoPrune();
@@ -43,4 +50,4 @@ async function insert (opCtx, doc) {
}
module.exports = insert;
module.exports = insert;
+6 -6
View File
@@ -36,7 +36,7 @@ async function validateDelete (opCtx) {
throw new Error('empty result');
if (result.length === 0) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
else {
const storageDoc = result[0];
@@ -62,13 +62,13 @@ async function deletePermanently (opCtx) {
throw new Error('empty result');
if (!result.deleted) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
col.autoPrune();
ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier });
ctx.bus.emit('data-received');
return res.status(apiConst.HTTP.NO_CONTENT).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.OK });
}
@@ -89,13 +89,13 @@ async function markAsDeleted (opCtx) {
throw new Error('empty result');
if (!result.updated) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier });
col.autoPrune();
ctx.bus.emit('data-received');
return res.status(apiConst.HTTP.NO_CONTENT).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.OK });
}
@@ -119,4 +119,4 @@ function deleteOperation (ctx, env, app, col) {
};
}
module.exports = deleteOperation;
module.exports = deleteOperation;
+2 -1
View File
@@ -39,7 +39,8 @@ async function history (opCtx, fieldsProjector) {
throw new Error('empty result');
if (result.length === 0) {
return res.status(apiConst.HTTP.NO_CONTENT).end();
res.status(apiConst.HTTP.OK);
return renderer.render(res, result);
}
_.each(result, col.resolveDates);
+5 -5
View File
@@ -36,7 +36,7 @@ async function patch (opCtx) {
const storageDoc = result[0];
if (storageDoc.isValid === false) {
return res.status(apiConst.HTTP.GONE).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.GONE);
}
const modifiedDate = col.resolveDates(storageDoc)
@@ -44,13 +44,13 @@ async function patch (opCtx) {
if (ifUnmodifiedSince
&& dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) {
return res.status(apiConst.HTTP.PRECONDITION_FAILED).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED);
}
await applyPatch(opCtx, identifier, doc, storageDoc);
}
else {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND);
}
}
@@ -82,7 +82,7 @@ async function applyPatch (opCtx, identifier, doc, storageDoc) {
throw new Error('matchedCount empty');
res.setHeader('Last-Modified', now.toUTCString());
res.status(apiConst.HTTP.NO_CONTENT).send({ });
opTools.sendJSONStatus(res, apiConst.HTTP.OK);
const fieldsProjector = new FieldsProjector('_all');
const patchedDocs = await col.storage.findOne(identifier, fieldsProjector);
@@ -115,4 +115,4 @@ function patchOperation (ctx, env, app, col) {
};
}
module.exports = patchOperation;
module.exports = patchOperation;
+3 -3
View File
@@ -26,12 +26,12 @@ async function read (opCtx) {
throw new Error('empty result');
if (result.length === 0) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
const doc = result[0];
if (doc.isValid === false) {
return res.status(apiConst.HTTP.GONE).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.GONE });
}
@@ -74,4 +74,4 @@ function readOperation (ctx, env, app, col) {
};
}
module.exports = readOperation;
module.exports = readOperation;
+3 -3
View File
@@ -48,7 +48,7 @@ async function updateConditional (opCtx, doc, storageDoc) {
const { col, req, res } = opCtx;
if (storageDoc.isValid === false) {
return res.status(apiConst.HTTP.GONE).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.GONE);
}
const modifiedDate = col.resolveDates(storageDoc)
@@ -56,7 +56,7 @@ async function updateConditional (opCtx, doc, storageDoc) {
if (ifUnmodifiedSince
&& dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) {
return res.status(apiConst.HTTP.PRECONDITION_FAILED).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED);
}
await replace(opCtx, doc, storageDoc);
@@ -83,4 +83,4 @@ function updateOperation (ctx, env, app, col) {
};
}
module.exports = updateOperation;
module.exports = updateOperation;
+11 -2
View File
@@ -4,6 +4,7 @@ const apiConst = require('../../const.json')
, security = require('../../security')
, validate = require('./validate.js')
, path = require('path')
, opTools = require('../../shared/operationTools')
;
/**
@@ -37,12 +38,20 @@ async function replace (opCtx, doc, storageDoc, options) {
throw new Error('empty matchedCount');
res.setHeader('Last-Modified', now.toUTCString());
const fields = {
lastModified: now.getTime()
}
if (storageDoc.identifier !== doc.identifier || isDeduplication) {
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, doc.identifier));
fields.identifier = doc.identifier;
fields.isDeduplication = true;
if (storageDoc.identifier !== doc.identifier) {
fields.deduplicatedIdentifier = storageDoc.identifier;
}
}
res.status(apiConst.HTTP.NO_CONTENT).send({ });
opTools.sendJSON({ res, status: apiConst.HTTP.OK, fields });
ctx.bus.emit('storage-socket-update', { colName: col.colName, doc });
col.autoPrune();
@@ -50,4 +59,4 @@ async function replace (opCtx, doc, storageDoc, options) {
}
module.exports = replace;
module.exports = replace;
+5 -1
View File
@@ -7,6 +7,7 @@ const express = require('express')
, apiConst = require('./const.json')
, security = require('./security')
, genericSetup = require('./generic/setup')
, opTools = require('./shared/operationTools')
;
function configure (env, ctx) {
@@ -65,7 +66,6 @@ function configure (env, ctx) {
app.set('enabledCollections', ['devicestatus', 'entries', 'food', 'profile', 'settings', 'treatments']);
self.setENVTruthy('API3_SECURITY_ENABLE', apiConst.API3_SECURITY_ENABLE);
self.setENVTruthy('API3_TIME_SKEW_TOLERANCE', apiConst.API3_TIME_SKEW_TOLERANCE);
self.setENVTruthy('API3_DEDUP_FALLBACK_ENABLED', apiConst.API3_DEDUP_FALLBACK_ENABLED);
self.setENVTruthy('API3_CREATED_AT_FALLBACK_ENABLED', apiConst.API3_CREATED_AT_FALLBACK_ENABLED);
self.setENVTruthy('API3_MAX_LIMIT', apiConst.API3_MAX_LIMIT);
@@ -104,6 +104,10 @@ function configure (env, ctx) {
res.redirect(307, '../../../api3-docs');
});
app.use((req, res) => {
opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND, apiConst.MSG.HTTP_404_BAD_OPERATION);
})
ctx.storageSocket = new StorageSocket(app, env, ctx);
return app;
+2 -45
View File
@@ -1,10 +1,8 @@
'use strict';
const moment = require('moment')
, apiConst = require('./const.json')
const apiConst = require('./const.json')
, _ = require('lodash')
, shiroTrie = require('shiro-trie')
, dateTools = require('./shared/dateTools')
, opTools = require('./shared/operationTools')
;
@@ -13,37 +11,6 @@ function getRemoteIP (req) {
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
}
/**
* Check if Date header in HTTP request (or 'now' query parameter) is present and valid (with error response sending)
*/
function checkDateHeader (opCtx) {
const { app, req, res } = opCtx;
let dateString = req.header('Date');
if (!dateString) {
dateString = req.query.now;
}
if (!dateString) {
return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_DATE);
}
let dateMoment = dateTools.parseToMoment(dateString);
if (!dateMoment) {
return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_DATE);
}
let nowMoment = moment(new Date());
let diffMinutes = moment.duration(nowMoment.diff(dateMoment)).asMinutes();
if (Math.abs(diffMinutes) > app.get('API3_TIME_SKEW_TOLERANCE')) {
return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE);
}
return true;
}
function authenticate (opCtx) {
return new Promise(function promise (resolve, reject) {
@@ -56,16 +23,6 @@ function authenticate (opCtx) {
return resolve({ shiros: [ adminShiro ] });
}
if (req.protocol !== 'https') {
return reject(
opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_NOT_USING_HTTPS));
}
const checkDateResult = checkDateHeader(opCtx);
if (checkDateResult !== true) {
return checkDateResult;
}
let token = ctx.authorization.extractToken(req);
if (!token) {
return reject(
@@ -123,4 +80,4 @@ module.exports = {
authenticate,
checkPermission,
demandPermission
};
};
+29 -5
View File
@@ -6,18 +6,41 @@ const apiConst = require('../const.json')
, uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-)
;
function sendJSON ({ res, result, status, fields }) {
const json = {
status: status || apiConst.HTTP.OK,
result: result
};
if (result) {
json.result = result
}
if (fields) {
Object.assign(json, fields);
}
res.status(json.status).json(json);
}
function sendJSONStatus (res, status, title, description, warning) {
const json = {
status: status,
message: title,
description: description
status: status
};
if (title) { json.message = title }
if (description) { json.description = description }
// Add optional warning message.
if (warning) { json.warning = warning; }
res.status(status).json(json);
res.status(status)
.json(json);
return title;
}
@@ -104,8 +127,9 @@ function resolveIdentifier (doc) {
module.exports = {
sendJSON,
sendJSONStatus,
validateCommon,
calculateIdentifier,
resolveIdentifier
};
};
+15 -2
View File
@@ -53,7 +53,7 @@ function extension2accept (req, res, next) {
*/
function render (res, data) {
res.format({
'json': () => res.send(data),
'json': () => renderJson(res, data),
'csv': () => renderCsv(res, data),
'xml': () => renderXml(res, data),
'default': () =>
@@ -62,6 +62,19 @@ function render (res, data) {
}
/**
* Format data to output as JSON
* @param {Object} res
* @param {any} data
*/
function renderJson (res, data) {
res.send({
status: apiConst.HTTP.OK,
result: data
});
}
/**
* Format data to output as .csv
* @param {Object} res
@@ -96,4 +109,4 @@ function renderXml (res, data) {
module.exports = {
extension2accept,
render
};
};
+2 -2
View File
@@ -38,7 +38,7 @@ function configure (app, ctx, env) {
return { colName: col.colName, lastModified: result };
}
async function collectionsAsync (auth) {
const cols = app.get('collections')
@@ -77,7 +77,7 @@ function configure (app, ctx, env) {
info.collections = await collectionsAsync(auth);
res.json(info);
opTools.sendJSON({ res, result: info });
}
+1 -1
View File
@@ -47,7 +47,7 @@ function configure (app, ctx, env) {
}
}
res.json(info);
opTools.sendJSON({ res, result: info });
}
+1 -1
View File
@@ -13,7 +13,7 @@ function configure (app) {
try {
const versionInfo = await storageTools.getVersionInfo(app);
res.json(versionInfo);
opTools.sendJSON({ res, result: versionInfo });
} catch(error) {
console.error(error);
+854 -443
View File
File diff suppressed because it is too large Load Diff
+236 -116
View File
@@ -2,7 +2,7 @@ openapi: 3.0.0
servers:
- url: '/api/v3'
info:
version: "3.0.1"
version: 3.0.3
title: Nightscout API
contact:
name: NS development discussion channel
@@ -22,11 +22,6 @@ info:
but this should never be set to false in production.
- Number of minutes of acceptable time skew between client's and server's clock (optional, default = 5)
<pre>API3_TIME_SKEW_TOLERANCE=5</pre>
This security parameter is used for preventing anti-replay attacks, specifically when checking the time from `Date` header.
- Maximum limit count of documents retrieved from single query
<pre>API3_MAX_LIMIT=1000</pre>
@@ -80,8 +75,6 @@ paths:
schema:
$ref: '#/components/schemas/paramCollection'
- $ref: '#/components/parameters/dateHeader'
- $ref: '#/components/parameters/nowParam'
- $ref: '#/components/parameters/tokenParam'
######################################################################################
@@ -102,7 +95,7 @@ paths:
3) paging - using `limit` and `skip` parameters
When there is no document matching the filtering criteria, HTTP status 204 is returned with empty response content. Otherwise HTTP 200 code is returned with JSON array of matching documents as a response content.
If successful, HTTP 200 code is returned with JSON array of matching documents as a response content (it may be empty).
This operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`).
@@ -120,13 +113,12 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/search200'
204:
$ref: '#/components/responses/search204'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -145,10 +137,11 @@ paths:
- generic
summary: 'CREATE: Inserts a new document into the collection'
description:
Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified and with an empty response content. `identifier` can be parsed from the `Location` response header.
Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified.
`identifier` is included in response body or it can be parsed from the `Location` response header.
When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 204 HTTP status code along with `Last-Modified` and correct `Location` headers.
When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 200 HTTP status code along with `Last-Modified` and correct `Location` headers. The response body then includes `isDeduplication`=`true` and `deduplicatedIdentifier` fields.
This operation provides autopruning of the collection (if autopruning is enabled).
@@ -165,13 +158,14 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/200Deduplication'
201:
$ref: '#/components/responses/201CreatedLocation'
204:
$ref: '#/components/responses/204NoContentLocation'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -202,8 +196,6 @@ paths:
schema:
$ref: '#/components/schemas/paramIdentifier'
- $ref: '#/components/parameters/dateHeader'
- $ref: '#/components/parameters/nowParam'
- $ref: '#/components/parameters/tokenParam'
######################################################################################
@@ -215,7 +207,7 @@ paths:
Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
When `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side.
@@ -229,7 +221,8 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
@@ -254,10 +247,10 @@ paths:
- generic
summary: 'UPDATE: Updates a document in the collection'
description:
Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 204 HTTP status code will be returned with empty response body.
Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 200 HTTP status code will be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
When no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter).
@@ -283,13 +276,14 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/200Ok'
201:
$ref: '#/components/responses/201Created'
204:
$ref: '#/components/responses/204NoContentLocation'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -312,10 +306,10 @@ paths:
- generic
summary: 'PATCH: Partially updates document in the collection'
description:
Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 204 HTTP status code will be returned with empty response body.
Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 200 HTTP status code will be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
When no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code.
@@ -347,11 +341,12 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
204:
$ref: '#/components/responses/204NoContentLocation'
200:
$ref: '#/components/responses/200Ok'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -360,10 +355,10 @@ paths:
$ref: '#/components/responses/403Forbidden'
404:
$ref: '#/components/responses/404NotFound'
412:
$ref: '#/components/responses/412PreconditionFailed'
410:
$ref: '#/components/responses/410Gone'
412:
$ref: '#/components/responses/412PreconditionFailed'
422:
$ref: '#/components/responses/422UnprocessableEntity'
@@ -387,11 +382,12 @@ paths:
- $ref: '#/components/parameters/permanentParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
204:
description: Successful operation - empty response
200:
$ref: '#/components/responses/200Ok'
401:
$ref: '#/components/responses/401Unauthorized'
403:
@@ -412,8 +408,6 @@ paths:
schema:
$ref: '#/components/schemas/paramCollection'
- $ref: '#/components/parameters/dateHeader'
- $ref: '#/components/parameters/nowParam'
- $ref: '#/components/parameters/tokenParam'
get:
@@ -430,9 +424,6 @@ paths:
Deleted documents will appear with `isValid` = `false` field.
When there is no change detected since the timestamp the operation ends with 204 HTTP status code and empty response content.
HISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections).
@@ -448,13 +439,12 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/history200'
204:
$ref: '#/components/responses/history204'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -485,8 +475,6 @@ paths:
type: integer
format: int64
- $ref: '#/components/parameters/dateHeader'
- $ref: '#/components/parameters/nowParam'
- $ref: '#/components/parameters/tokenParam'
get:
@@ -509,13 +497,12 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/history200'
204:
$ref: '#/components/responses/history204'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -556,7 +543,8 @@ paths:
This operation requires authorization in contrast with VERSION operation.
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
@@ -573,8 +561,6 @@ paths:
######################################################################################
/lastModified:
parameters:
- $ref: '#/components/parameters/dateHeader'
- $ref: '#/components/parameters/nowParam'
- $ref: '#/components/parameters/tokenParam'
get:
@@ -591,7 +577,8 @@ paths:
This operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to.
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
@@ -606,41 +593,6 @@ components:
parameters:
dateHeader:
in: header
name: Date
schema:
type: string
required: false
description:
Timestamp (defined by client's clock) when the HTTP request was constructed on client.
This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code.
This can be set alternatively in `now` query parameter.
Example&#58;
<pre>Date&#58; Wed, 17 Oct 2018 05&#58;13&#58;00 GMT</pre>
nowParam:
in: query
name: now
schema:
type: integer
format: int64
required: false
description:
Timestamp (defined by client's clock) when the HTTP request was constructed on client.
This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code.
This can be set alternatively in `Date` header.
Example&#58;
<pre>now=1525383610088</pre>
tokenParam:
in: query
@@ -850,11 +802,54 @@ components:
######################################################################################
responses:
200Ok:
description: The request was successfully processed
content:
application/json:
schema:
properties:
status:
type: integer
example: 200
200Deduplication:
description: Successfully updated a duplicate document in the collection
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
'Location':
$ref: '#/components/schemas/headerLocation'
content:
application/json:
schema:
properties:
status:
type: integer
example: 200
identifier:
$ref: '#/components/schemas/identifierField'
isDeduplication:
$ref: '#/components/schemas/isDeduplicationField'
deduplicatedIdentifier:
$ref: '#/components/schemas/deduplicatedIdentifierField'
201Created:
description: Successfully created a new document in collection
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
content:
application/json:
schema:
properties:
status:
type: integer
example: 201
identifier:
$ref: '#/components/schemas/identifierField'
lastModified:
$ref: '#/components/schemas/lastModifiedField'
201CreatedLocation:
description: Successfully created a new document in collection
@@ -863,20 +858,17 @@ components:
$ref: '#/components/schemas/headerLastModified'
'Location':
$ref: '#/components/schemas/headerLocation'
204NoContent:
description: Successfully finished operation
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
204NoContentLocation:
description: Successfully finished operation
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
'Location':
$ref: '#/components/schemas/headerLocation'
content:
application/json:
schema:
properties:
status:
type: integer
example: 201
identifier:
$ref: '#/components/schemas/identifierField'
lastModified:
$ref: '#/components/schemas/lastModifiedField'
304NotModified:
description: The document has not been modified on the server since timestamp specified in If-Modified-Since header
@@ -886,34 +878,95 @@ components:
400BadRequest:
description: The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.
content:
application/json:
schema:
properties:
status:
type: integer
example: 400
401Unauthorized:
description: The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.
description: The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.
content:
application/json:
schema:
properties:
status:
type: integer
example: 401
403Forbidden:
description: Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.
content:
application/json:
schema:
properties:
status:
type: integer
example: 403
404NotFound:
description: The collection or document specified was not found.
content:
application/json:
schema:
properties:
status:
type: integer
example: 404
406NotAcceptable:
description: The requested content type (in `Accept` header) is not supported.
content:
application/json:
schema:
properties:
status:
type: integer
example: 406
412PreconditionFailed:
description: The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).
content:
application/json:
schema:
properties:
status:
type: integer
example: 412
410Gone:
description: The requested document has already been deleted.
content:
application/json:
schema:
properties:
status:
type: integer
example: 410
422UnprocessableEntity:
description: The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).
content:
application/json:
schema:
properties:
status:
type: integer
example: 422
search200:
description: Successful operation returning array of documents matching the filtering criteria
content:
application/json:
schema:
$ref: '#/components/schemas/DocumentArray'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/DocumentArray'
text/csv:
schema:
$ref: '#/components/schemas/DocumentArray'
@@ -921,15 +974,17 @@ components:
schema:
$ref: '#/components/schemas/DocumentArray'
search204:
description: Successful operation - no documents matching the filtering criteria
read200:
description: The document has been succesfully found and its JSON form returned in the response content.
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/Document'
text/csv:
schema:
$ref: '#/components/schemas/Document'
@@ -946,7 +1001,12 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/DocumentArray'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/DocumentArray'
text/csv:
schema:
$ref: '#/components/schemas/DocumentArray'
@@ -959,15 +1019,18 @@ components:
'ETag':
$ref: '#/components/schemas/headerEtagLastModifiedMaximum'
history204:
description: No changes detected
lastModified200:
description: Successful operation returning the timestamps
content:
application/json:
schema:
$ref: '#/components/schemas/LastModifiedResult'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/LastModifiedResult'
######################################################################################
schemas:
@@ -1025,6 +1088,44 @@ components:
example: '53409478-105f-11e9-ab14-d663bd873d93'
identifierField:
description:
Identifier of created or modified document
type: string
example: '53409478-105f-11e9-ab14-d663bd873d93'
lastModifiedField:
type: integer
format: int64
description:
Timestamp of the last document modification on the server, formatted as
Unix epoch in milliseconds (1525383610088)
example: 1525383610088
statusField:
type: integer
description:
HTTP response status code. The status appears also in response body's field for those clients
that are unable to process standard HTTP status code.
example: 200
isDeduplicationField:
type: boolean
description:
Flag whether the operation found a duplicate document (to update)
example: true
deduplicatedIdentifierField:
type: string
description:
The original document that has been marked as a duplicate document and which has been updated
example: 'abc09478-105f-11e9-ab14-d663bd873d93'
DocumentBase:
description: Shared base for all documents
properties:
@@ -1527,6 +1628,17 @@ components:
Version:
description: Information about versions
type: object
properties:
status:
$ref: '#/components/schemas/statusField'
result:
$ref: '#/components/schemas/VersionResult'
VersionResult:
type: object
properties:
@@ -1562,8 +1674,17 @@ components:
Status:
description: Information about versions and API permissions
properties:
status:
$ref: '#/components/schemas/statusField'
result:
$ref: '#/components/schemas/StatusResult'
StatusResult:
allOf:
- $ref: '#/components/schemas/Version'
- $ref: '#/components/schemas/VersionResult'
- type: object
properties:
@@ -1586,7 +1707,6 @@ components:
type: string
example: 'crud'
LastModifiedResult:
description: Result of LAST MODIFIED operation
properties:
@@ -1644,4 +1764,4 @@ components:
type: http
scheme: bearer
description: Use this if you know the temporary json webtoken.
bearerFormat: JWT
bearerFormat: JWT
+4 -4
View File
@@ -1,14 +1,14 @@
'use strict';
function init () {
const _ = require('lodash');
function init (env) {
const ipDelayList = {};
const DELAY_ON_FAIL = 5000;
const DELAY_ON_FAIL = _.get(env, 'settings.authFailDelay') || 5000;
const FAIL_AGE = 60000;
const sleep = require('util').promisify(setTimeout);
ipDelayList.addFailedRequest = function addFailedRequest (ip) {
const ipString = String(ip);
let entry = ipDelayList[ipString];
+21 -18
View File
@@ -4,20 +4,20 @@ const _ = require('lodash');
const jwt = require('jsonwebtoken');
const shiroTrie = require('shiro-trie');
const ipdelaylist = require('./delaylist')();
const consts = require('./../constants');
const sleep = require('util').promisify(setTimeout);
const addFailedRequest = ipdelaylist.addFailedRequest;
const shouldDelayRequest = ipdelaylist.shouldDelayRequest;
const requestSucceeded = ipdelaylist.requestSucceeded;
function getRemoteIP (req) {
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
}
function init (env, ctx) {
const ipdelaylist = require('./delaylist')(env, ctx);
const addFailedRequest = ipdelaylist.addFailedRequest;
const shouldDelayRequest = ipdelaylist.shouldDelayRequest;
const requestSucceeded = ipdelaylist.requestSucceeded;
var authorization = {};
var storage = authorization.storage = require('./storage')(env, ctx);
var defaultRoles = (env.settings.authDefaultRoles || '').split(/[, :]/);
@@ -96,7 +96,7 @@ function init (env, ctx) {
}
function authorizeAdminSecret (secret) {
return (env.api_secret && env.api_secret.length > 12) ? (secret === env.api_secret) : false;
return env.enclave.isApiKey(secret);
}
authorization.seenPermissions = [];
@@ -185,7 +185,7 @@ function init (env, ctx) {
// Tokens have to be well formed JWTs
try {
const verified = await jwt.verify(data.token, env.api_secret);
const verified = env.enclave.verifyJWT(data.token);
token = verified.accessToken;
} catch (err) {}
@@ -206,7 +206,13 @@ function init (env, ctx) {
console.error('Resolving secret/token to permissions failed');
addFailedRequest(data.ip);
if (callback) { callback('All validation failed', {}); }
ctx.bus.emit('admin-notify', {
title: ctx.language.translate('Failed authentication')
, message: ctx.language.translate('A device at IP address %1 attempted authenticating with Nightscout with wrong credentials. Check if you have an uploader setup with wrong API_SECRET or token?', data.ip)
});
if (callback) { callback('All validation failed', {}); }
return {};
};
@@ -231,7 +237,7 @@ function init (env, ctx) {
/**
* Check if the client has a permission execute an action,
* based on an API_SECRET or JWT in the request.
* based on an API KEY or JWT in the request.
*
* Used to authorize API calls
*
@@ -275,8 +281,9 @@ function init (env, ctx) {
*/
authorization.authorize = function authorize (accessToken) {
let userToken = accessToken
const decodedToken = jwt.decode(accessToken);
let userToken = accessToken;
const decodedToken = env.enclave.verifyJWT(accessToken);
if (decodedToken && decodedToken.accessToken) {
userToken = decodedToken.accessToken;
@@ -286,18 +293,14 @@ function init (env, ctx) {
var authorized = null;
if (subject) {
var token = jwt.sign({ accessToken: subject.accessToken }, env.api_secret, { expiresIn: '8h' });
//decode so we can tell the client the issued and expired times
var decoded = jwt.decode(token);
const token = env.enclave.signJWT({ accessToken: subject.accessToken });
const decoded = env.enclave.verifyJWT(token);
var roles = _.uniq(subject.roles.concat(defaultRoles));
authorized = {
token
, sub: subject.name
// not sending roles to client to prevent us from treating them as magic
// instead group permissions by role so the we can create correct shiros on the client
, permissionGroups: _.map(roles, storage.roleToPermissions)
, iat: decoded.iat
, exp: decoded.exp
+2 -5
View File
@@ -151,12 +151,9 @@ function init (env, ctx) {
}
storage.subjects = _.map(results, function eachSubject (subject) {
if (env.api_secret) {
var shasum = crypto.createHash('sha1');
shasum.update(env.api_secret);
shasum.update(subject._id.toString());
if (env.enclave.isApiKeySet()) {
subject.digest = env.enclave.getSubjectHash(subject._id.toString());
var abbrev = subject.name.toLowerCase().replace(/[\W]/g, '').substring(0, 10);
subject.digest = shasum.digest('hex');
subject.accessToken = abbrev + '-' + subject.digest.substring(0, 16);
subject.accessTokenDigest = storage.getSHA1(subject.accessToken);
}
+103
View File
@@ -0,0 +1,103 @@
'use strict';
function init (client, $) {
var notifies = {};
client.notifies = notifies;
notifies.notifies = [];
notifies.drawer = $('#adminNotifiesDrawer');
notifies.button = $('#adminnotifies');
notifies.updateAdminNotifies = function updateAdminNotifies() {
var src = '/api/v1/adminnotifies?t=' + new Date().getTime();
$.ajax({
method: 'GET'
, url: src
, headers: client.headers()
}).done(function success (results) {
if (results.message) {
var m = results.message;
client.notifies.notifies = m.notifies;
client.notifies.notifyCount = m.notifyCount;
if (m.notifyCount > 0) {
notifies.button.show();
}
}
window.setTimeout(notifies.updateAdminNotifies, 1000*60);
}).fail(function fail () {
console.error('Failed to load notifies');
window.setTimeout(notifies.updateAdminNotifies, 1000*60);
});
}
notifies.updateAdminNotifies();
function wrapmessage(title, message, count, ago, persistent) {
let html = '<hr><p><b>' + title + '</b></p><p class="adminNotifyMessage">' + message + '</p>';
let additional = '';
if (count > 1) additional += client.translate('Event repeated %1 times.', count) + ' ';
let units = client.translate('minutes');
if (ago > 60) {
ago = ago / 60;
ago = Math.round((ago + Number.EPSILON) * 10) / 10;
units = client.translate('hours');
}
if (ago == 0) { ago = client.translate('less than 1'); }
if (!persistent && ago) additional += client.translate('Last recorded %1 %2 ago.', ago, units);
if (additional) html += '<p class="adminNotifyMessageAdditionalInfo">' + additional + '</p>'
return html;
}
notifies.prepare = function prepare() {
var translate = client.translate;
var html = '<div id="adminNotifyContent">';
var messages = client.notifies.notifies;
var messageCount = client.notifies.notifyCount;
if (messages && messages.length > 0) {
html += '<p><b>' + translate('You have administration messages') + '</b></p>';
for(var i = 0 ; i < messages.length; i++) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
var m = messages[i];
const ago = Math.round((Date.now() - m.lastRecorded) / 60000);
html += wrapmessage(translate(m.title), translate(m.message), m.count, ago, m.persistent);
}
} else {
if (messageCount > 0) {
html = wrapmessage(translate('Admin messages in queue'), translate('Please sign in using the API_SECRET to see your administration messages'));
} else {
html = wrapmessage(translate('Queue empty'), translate('There are no admin messages in queue'));
}
}
html += '<hr></div>';
notifies.drawer.html(html);
}
function maybePrevent (event) {
if (event) {
event.preventDefault();
}
}
notifies.toggleDrawer = function toggleDrawer (event) {
client.browserUtils.toggleDrawer('#adminNotifiesDrawer', notifies.prepare);
maybePrevent(event);
};
notifies.button.click(notifies.toggleDrawer);
notifies.button.css('color','red');
return notifies;
}
module.exports = init;
+20 -10
View File
@@ -252,6 +252,7 @@ function init (client, $) {
var html = '<table style="float:right;margin-right:20px;font-size:12px">';
var carbs = 0;
for (var fi = 0; fi < record.foods.length; fi++) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
var f = record.foods[fi];
carbs += f.carbs * f.portions;
html += '<tr>';
@@ -426,6 +427,7 @@ function init (client, $) {
if (record.foods.length) {
var gisum = 0;
for (var fi = 0; fi < record.foods.length; fi++) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
var f = record.foods[fi];
record.carbs += f.carbs * f.portions;
gisum += f.carbs * f.portions * f.gi;
@@ -576,7 +578,7 @@ function init (client, $) {
foods = [];
$('#bc_addfoodarea').css('display', '');
} else {
var qp = quickpicks[qpiselected];
var qp = quickpicks[parseInt(qpiselected)];
foods = _.cloneDeep(qp.foods);
$('#bc_addfoodarea').css('display', 'none');
}
@@ -589,18 +591,20 @@ function init (client, $) {
var qpiselected = $('#bc_quickpick').val();
if (qpiselected >= 0) {
var qp = quickpicks[qpiselected];
var qp = quickpicks[parseInt(qpiselected)];
if (qp.hideafteruse) {
qp.hidden = true;
var apisecrethash = localStorage.getItem('apisecrethash');
var dataJson = JSON.stringify(qp, null, ' ');
var xhr = new XMLHttpRequest();
xhr.open('PUT', '/api/v1/food/', true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.setRequestHeader('api-secret', apisecrethash);
xhr.send(dataJson);
$.ajax({
method: 'PUT'
, url: '/api/v1/food/'
, headers: client.headers()
, data: qp
}).done(function treatmentSaved (response) {
console.info('quick pick saved', response);
}).fail(function treatmentSaveFail (response) {
console.info('quick pick failed to save', response);
});
}
}
@@ -650,6 +654,7 @@ function init (client, $) {
});
$('#bc_quickpick').empty().append('<option value="-1">' + translate('(none)') + '</option>');
for (var i = 0; i < records.length; i++) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
var r = records[i];
$('#bc_quickpick').append('<option value="' + i + '">' + r.name + ' (' + r.carbs + ' g)</option>');
}
@@ -694,6 +699,7 @@ function init (client, $) {
}
$('#bc_data').empty();
for (var i = 0; i < foodlist.length; i++) {
/* eslint-disable security/detect-object-injection */ // verified false positive
if (filter.category !== '' && foodlist[i].category !== filter.category) { continue; }
if (filter.subcategory !== '' && foodlist[i].subcategory !== filter.subcategory) { continue; }
if (filter.name !== '' && foodlist[i].name.toLowerCase().indexOf(filter.name.toLowerCase()) < 0) { continue; }
@@ -703,6 +709,7 @@ function init (client, $) {
o += foodlist[i].unit + ' | ';
o += 'Carbs: ' + foodlist[i].carbs + ' g';
$('#bc_data').append('<option value="' + i + '">' + o + '</option>');
/* eslint-enable security/detect-object-injection */ // verified false positive
}
$('#bc_addportions').val('1');
@@ -726,8 +733,11 @@ function init (client, $) {
var index = $('#bc_data').val();
var portions = parseFloat($('#bc_addportions').val().replace(',', '.'));
if (index !== null && !isNaN(portions) && portions > 0) {
index = parseInt(index);
/* eslint-disable security/detect-object-injection */ // verified false positive
foodlist[index].portions = portions;
foods.push(_.cloneDeep(foodlist[index]));
/* eslint-enable security/detect-object-injection */ // verified false positive
$(this).dialog('close');
boluscalc.calculateInsulin();
}
+42 -6
View File
@@ -13,6 +13,29 @@ function init (client, serverSettings, $) {
var storage = Storages.localStorage;
var settings = require('../settings')();
function updateBolusRender () {
var bolusSettings = client.settings.extendedSettings.bolus || {};
var allRenderOverOptions = [5, 1, 0.5, 0.1];
if (_.isNumber(bolusSettings.renderOver) && bolusSettings.renderOver > 0 && bolusSettings.renderOver < Number.MAX_SAFE_INTEGER) {
allRenderOverOptions.push(_.toNumber(bolusSettings.renderOver));
}
var sortedRenderOverOptions = _.chain(allRenderOverOptions).uniq().sort().reverse().value();
_.forEach(sortedRenderOverOptions, function (optionValue) {
$('#bolusRenderOver').append(
$('<option></option>')
.attr('value', optionValue)
.text(client.translate('%1 U and Over', { params: [optionValue] }))
);
});
$('#bolusRenderOver').val(String(bolusSettings.renderOver || 0.5));
$('#bolusRenderFormat').val(bolusSettings.renderFormat ? bolusSettings.renderFormat : 'default');
$('#bolusRenderFormatSmall').val(bolusSettings.renderFormatSmall ? bolusSettings.renderFormatSmall : 'default');
}
function loadForm () {
var utils = client.utils;
var language = client.language;
@@ -70,7 +93,7 @@ function init (client, serverSettings, $) {
$('#basalrender').val(settings.extendedSettings.basal ? settings.extendedSettings.basal.render : 'none');
$('#bolusrender').val(settings.extendedSettings.bolus ? settings.extendedSettings.bolus.render : 'all');
updateBolusRender();
if (settings.timeFormat === 24) {
$('#24-browser').prop('checked', true);
@@ -147,6 +170,7 @@ function init (client, serverSettings, $) {
});
//if there is a token, append it to each of the links in the hamburger menu
/* eslint-disable security/detect-possible-timing-attacks */ // verified false positive
if (token != '') {
token = '?token=' + token;
$('#reportlink').attr('href', 'report' + token);
@@ -162,7 +186,7 @@ function init (client, serverSettings, $) {
storage.remove(name);
});
storage.remove('basalrender');
storage.remove('bolusrender');
storage.remove('bolus');
event.preventDefault();
client.browserUtils.reload();
});
@@ -192,6 +216,7 @@ function init (client, serverSettings, $) {
function storeInBrowser (data) {
Object.keys(data).forEach(k => {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
storage.set(k, data[k]);
});
}
@@ -215,7 +240,11 @@ function init (client, serverSettings, $) {
, language: $('#language').val()
, scaleY: $('#scaleY').val()
, basalrender: $('#basalrender').val()
, bolusrender: $('#bolusrender').val()
, bolus: {
renderOver: $('#bolusRenderOver').val()
, renderFormat: $('#bolusRenderFormat').val()
, renderFormatSmall: $('#bolusRenderFormatSmall').val()
}
, showPlugins: checkedPluginNames()
, storageVersion: STORAGE_VERSION
});
@@ -252,6 +281,7 @@ function init (client, serverSettings, $) {
try {
settings.eachSetting(function setEach (name) {
var stored = storage.get(name);
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
return stored !== undefined && stored !== null ? stored : serverSettings.settings[name];
});
@@ -276,11 +306,17 @@ function init (client, serverSettings, $) {
settings.extendedSettings.basal.render = basalStored !== null ? basalStored : settings.extendedSettings.basal.render;
if (!settings.extendedSettings.bolus) {
settings.extendedSettings.bolus = {};
settings.extendedSettings.bolus = {
renderOver: 0
, renderFormat: 'default'
, renderFormatSmall: 'default'
};
}
var bolusStored = storage.get('bolusrender');
settings.extendedSettings.bolus.render = bolusStored !== null ? bolusStored : settings.extendedSettings.bolus.render;
var bolusStored = storage.get('bolus');
settings.extendedSettings.bolus.renderOver = bolusStored !== null ? _.toNumber(bolusStored.renderOver) : settings.extendedSettings.bolus.renderOver;
settings.extendedSettings.bolus.renderFormat = bolusStored !== null ? bolusStored.renderFormat : settings.extendedSettings.bolus.renderFormat;
settings.extendedSettings.bolus.renderFormatSmall = bolusStored !== null ? bolusStored.renderFormatSmall : settings.extendedSettings.bolus.renderFormatSmall;
} catch (err) {
console.error(err);
-10
View File
@@ -18,16 +18,6 @@ function init ($) {
, opacity: 0.75
};
var querystring = queryParms();
if (querystring.notify) {
showNotification(querystring.notify, querystring.notifytype);
}
if (querystring.drawer) {
openDrawer('#drawer');
}
$('#drawerToggle').click(function(event) {
toggleDrawer('#drawer');
event.preventDefault();
+37 -5
View File
@@ -52,7 +52,7 @@ function init (client, $) {
submitHooks = {};
_.forEach(careportal.allEventTypes, function each (event) {
inputMatrix[event.val] = _.pick(event, ['otp','remoteCarbs', 'remoteAbsorption', 'remoteBolus', 'bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'reasons', 'targets']);
inputMatrix[event.val] = _.pick(event, ['otp','remoteCarbs', 'remoteAbsorption', 'remoteBolus', 'bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'sensor', 'reasons', 'targets']);
submitHooks[event.val] = event.submitHook;
});
}
@@ -76,6 +76,13 @@ function init (client, $) {
}
}
// validate the eventType input - should never hit this but bail if we do
if (!Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) {
maybePrevent(event);
return;
}
/* eslint-disable security/detect-object-injection */ // verified false positive by check above
var reasons = inputMatrix[eventType]['reasons'];
$('#reasonLabel').css('display', displayType(reasons && reasons.length > 0));
$('#targets').css('display', displayType(inputMatrix[eventType]['targets']));
@@ -92,6 +99,8 @@ function init (client, $) {
$('#proteinGivenLabel').css('display', displayType(inputMatrix[eventType]['protein']));
$('#fatGivenLabel').css('display', displayType(inputMatrix[eventType]['fat']));
$('#sensorInfo').css('display', displayType(inputMatrix[eventType]['sensor']));
$('#durationLabel').css('display', displayType(inputMatrix[eventType]['duration']));
$('#percentLabel').css('display', displayType(inputMatrix[eventType]['percent'] && $('#absolute').val() === ''));
$('#absoluteLabel').css('display', displayType(inputMatrix[eventType]['absolute'] && $('#percent').val() === ''));
@@ -115,19 +124,28 @@ function init (client, $) {
resetIfHidden(inputMatrix[eventType]['carbs'], '#carbsGiven');
resetIfHidden(inputMatrix[eventType]['protein'], '#proteinGiven');
resetIfHidden(inputMatrix[eventType]['fat'], '#fatGiven');
resetIfHidden(inputMatrix[eventType]['sensor'], '#sensorCode');
resetIfHidden(inputMatrix[eventType]['sensor'], '#transmitterId');
resetIfHidden(inputMatrix[eventType]['duration'], '#duration');
resetIfHidden(inputMatrix[eventType]['absolute'], '#absolute');
resetIfHidden(inputMatrix[eventType]['percent'], '#percent');
resetIfHidden(inputMatrix[eventType]['prebolus'], '#preBolus');
resetIfHidden(inputMatrix[eventType]['split'], '#insulinSplitNow');
resetIfHidden(inputMatrix[eventType]['split'], '#insulinSplitExt');
/* eslint-enable security/detect-object-injection */ // verified false positive
maybePrevent(event);
};
careportal.reasonable = function reasonable () {
var eventType = $('#eventType').val();
var reasons = inputMatrix[eventType]['reasons'];
var reasons = [];
// validate the eventType input before getting the reasons list
if (Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
reasons = inputMatrix[eventType]['reasons'];
}
var selected = $('#reason').val();
var reason = _.find(reasons, function matches (r) {
@@ -213,6 +231,8 @@ function init (client, $) {
$('#carbsGiven').val('');
$('#proteinGiven').val('');
$('#fatGiven').val('');
$('#sensorCode').val('');
$('#transmitterId').val('');
$('#insulinGiven').val('');
$('#duration').val('');
$('#percent').val('');
@@ -244,6 +264,8 @@ function init (client, $) {
, carbs: $('#carbsGiven').val()
, protein: $('#proteinGiven').val()
, fat: $('#fatGiven').val()
, sensorCode: $('#sensorCode').val()
, transmitterId: $('#transmitterId').val()
, insulin: $('#insulinGiven').val()
, duration: times.msecs(parse_duration($('#duration').val())).mins < 1 ? $('#duration').val() : times.msecs(parse_duration($('#duration').val())).mins
, percent: $('#percent').val()
@@ -259,7 +281,13 @@ function init (client, $) {
delete data.preBolus;
}
var reasons = inputMatrix[eventType]['reasons'];
var reasons = [];
// validate the eventType input before getting the reasons list
if (Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
reasons = inputMatrix[eventType]['reasons'];
}
var reason = _.find(reasons, function matches (r) {
return r.name === selectedReason;
});
@@ -307,9 +335,11 @@ function init (client, $) {
let d = {};
Object.keys(data).forEach(function(key) {
/* eslint-disable security/detect-object-injection */ // verified false positive
if (data[key] !== "" && data[key] !== null) {
d[key] = data[key]
}
d[key] = data[key]
}
/* eslint-enable security/detect-object-injection */ // verified false positive
});
return d;
@@ -415,6 +445,8 @@ function init (client, $) {
pushIf(data.carbs, translate('Carbs Given') + ': ' + data.carbs);
pushIf(data.protein, translate('Protein Given') + ': ' + data.protein);
pushIf(data.fat, translate('Fat Given') + ': ' + data.fat);
pushIf(data.sensorCode, translate('Sensor Code') + ': ' + data.sensorCode);
pushIf(data.transmitterId, translate('Transmitter ID') + ': ' + data.transmitterId);
pushIf(data.insulin, translate('Insulin Given') + ': ' + data.insulin);
pushIf(data.eventType === 'Combo Bolus', translate('Combo Bolus') + ': ' + data.splitNow + '% : ' + data.splitExt + '%');
pushIf(data.duration, translate('Duration') + ': ' + data.duration + ' ' + translate('mins'));
+1
View File
@@ -703,6 +703,7 @@ function init (client, d3, $) {
var pointTypes = client.settings.showForecast.split(' ');
var points = pointTypes.reduce( function (points, type) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
return points.concat(client.sbx.pluginBase.forecastPoints[type] || []);
}, [] );
+8 -4
View File
@@ -90,11 +90,15 @@ client.render = function render () {
for (let param in faceParams) {
if (param === '0') {
bgColor = (faceParams[param].substr(0, 1) === 'c'); // do we want colorful background?
alwaysShowTime = (faceParams[param].substr(1, 1) === 'y'); // always show "stale time" text?
staleMinutes = (faceParams[param].substr(2, 2) - 0 >= 0) ? faceParams[param].substr(2, 2) : 13; // threshold value (0=never)
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
let faceParam = faceParams[param];
bgColor = (faceParam.substr(0, 1) === 'c'); // do we want colorful background?
alwaysShowTime = (faceParam.substr(1, 1) === 'y'); // always show "stale time" text?
staleMinutes = (faceParam.substr(2, 2) - 0 >= 0) ? faceParam.substr(2, 2) : 13; // threshold value (0=never)
} else if (!clockCreated) {
let div = '<div class="' + faceParams[param].substr(0, 2) + '"' + ((faceParams[param].substr(2, 2) - 0 > 0) ? ' style="' + ((faceParams[param].substr(0, 2) === 'ar') ? 'height' : 'font-size') + ':' + faceParams[param].substr(2, 2) + 'vmin"' : '') + '></div>';
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
let faceParam = faceParams[param];
let div = '<div class="' + faceParam.substr(0, 2) + '"' + ((faceParam.substr(2, 2) - 0 > 0) ? ' style="' + ((faceParam.substr(0, 2) === 'ar') ? 'height' : 'font-size') + ':' + faceParam.substr(2, 2) + 'vmin"' : '') + '></div>';
$inner.append(div);
}
}
+6 -1
View File
@@ -234,9 +234,14 @@ d3locales.locale = function locale (language) {
,bg: 'bg_BG'
};
var loc = 'en_US';
if (mapper[language]) {
// validate the eventType input before getting the reasons list
if (Object.prototype.hasOwnProperty.call(mapper, language)) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
loc = mapper[language];
}
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
return d3locales[loc];
};
+4 -4
View File
@@ -42,20 +42,20 @@ hashauth.init = function init (client, $) {
return;
}
if (message.message === 'OK') {
if (response.message === 'OK' || message.message === 'OK') {
hashauth.authenticated = true;
console.log('Authentication passed.');
next(true);
return;
}
console.log('Authentication failed.', response);
console.log('Authentication failed!', response);
hashauth.removeAuthentication();
next(false);
return;
}).fail(function verifyfail (err) {
console.log('Authentication failed.', err);
console.log('Authentication failure', err);
hashauth.removeAuthentication();
next(false);
});
@@ -113,7 +113,7 @@ hashauth.init = function init (client, $) {
, buttons: [
{
id: 'requestauthenticationdialog-btn'
, text: translate('Update')
, text: translate('Authenticate')
, click: function() {
var dialog = this;
hashauth.processSecret($('#apisecret').val(), $('#storeapisecret').is(':checked'), function done (close) {
+14 -7
View File
@@ -237,6 +237,8 @@ client.load = function load (serverSettings, callback) {
//After plugins are initialized with browser settings;
browserSettings.loadAndWireForm();
client.adminnotifies = require('./adminnotifiesclient')(client, $);
if (serverSettings && serverSettings.authorized) {
client.authorized = serverSettings.authorized;
client.authorized.lat = Date.now();
@@ -266,6 +268,8 @@ client.load = function load (serverSettings, callback) {
$('#treatmentDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('careportal') > -1);
$('#boluscalcDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('boluscalc') > -1);
if (isAuthenticated) client.notifies.updateAdminNotifies();
// Edit mode
editButton.toggle(client.settings.editMode && treatmentUpdateAllowed);
editButton.click(function editModeClick (event) {
@@ -441,11 +445,6 @@ client.load = function load (serverSettings, callback) {
}
}
function updateBolusRenderOver () {
var bolusRenderOver = (client.settings.bolusRenderOver || 1) + ' U and Over';
$('#bolusRenderOver').text(bolusRenderOver);
}
function alarmingNow () {
return container.hasClass('alarming');
}
@@ -872,9 +871,15 @@ client.load = function load (serverSettings, callback) {
function getClientAlarm (level, group) {
var key = level + '-' + group;
var alarm = clientAlarms[key];
var alarm = null;
// validate the key before getting the alarm
if (Object.prototype.hasOwnProperty.call(clientAlarms, key)) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
alarm = clientAlarms[key];
}
if (!alarm) {
alarm = { level: level, group: group };
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
clientAlarms[key] = alarm;
}
return alarm;
@@ -964,6 +969,7 @@ client.load = function load (serverSettings, callback) {
document.addEventListener(visibilityChange, function visibilityChanged () {
var prevHidden = client.documentHidden;
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
client.documentHidden = document[hidden];
if (prevHidden && !client.documentHidden) {
@@ -1003,6 +1009,7 @@ client.load = function load (serverSettings, callback) {
$('.bgButton').click(function(e) {
if (alarmingNow()) {
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive
silenceDropdown.open(e);
}
});
@@ -1017,6 +1024,7 @@ client.load = function load (serverSettings, callback) {
Storages.localStorage.set('focusHours', hours);
refreshChart();
} else {
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive
viewDropdown.open(e);
}
});
@@ -1301,7 +1309,6 @@ client.load = function load (serverSettings, callback) {
prepareEntries();
updateTitle();
updateBolusRenderOver();
// Don't invoke D3 in headless mode
+40 -8
View File
@@ -7,20 +7,33 @@ var TWO_DAYS = 172800000;
function mergeDataUpdate (isDelta, cachedDataArray, receivedDataArray, maxAge) {
function nsArrayDiff (oldArray, newArray) {
var seen = [];
var knownMills = [];
var l = oldArray.length;
for (var i = 0; i < l; i++) {
/* eslint-disable security/detect-object-injection */ // verified false positive
if (oldArray[i] !== null) {
seen.push(oldArray[i].mills);
knownMills.push(oldArray[i].mills);
}
/* eslint-enable security/detect-object-injection */ // verified false positive
}
var result = [];
var result = {
updates: [],
new: []
};
l = newArray.length;
for (var j = 0; j < l; j++) {
if (!seen.includes(newArray[j].mills)) {
result.push(newArray[j]); //console.log('delta data found');
/* eslint-disable security/detect-object-injection */ // verified false positive
var item = newArray[j];
var millsSeen = knownMills.includes(item.mills);
if (!millsSeen) {
result.new.push(item);
} else {
result.updates.push(item);
}
}
return result;
@@ -40,16 +53,33 @@ function mergeDataUpdate (isDelta, cachedDataArray, receivedDataArray, maxAge) {
var mAge = (isNaN(maxAge) || maxAge == null) ? TWO_DAYS : maxAge;
var twoDaysAgo = new Date().getTime() - mAge;
for (var i = 0; i < cachedDataArray.length; i++) {
var i;
for (i = cachedDataArray.length -1; i >= 0; i--) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
var element = cachedDataArray[i];
if (element !== null && element !== undefined && element.mills <= twoDaysAgo) {
cachedDataArray.splice(i, 0);
cachedDataArray.splice(i, 1);
}
}
// If this is delta, calculate the difference, merge and sort
var diff = nsArrayDiff(cachedDataArray, receivedDataArray);
return cachedDataArray.concat(diff).sort(function(a, b) {
// if there's updated elements, replace those in place
if (diff.updates.length > 0) {
for (i = 0; i < diff.updates.length; i++) {
var e = diff.updates[i];
for (var j = 0; j < cachedDataArray.length; j++) {
if (e.mills == cachedDataArray[j].mills) {
cachedDataArray.splice(j,1,e);
}
}
}
}
// merge new items in
return cachedDataArray.concat(diff.new).sort(function(a, b) {
return a.mills - b.mills;
});
}
@@ -70,12 +100,14 @@ function mergeTreatmentUpdate (isDelta, cachedDataArray, receivedDataArray) {
var l = receivedDataArray.length;
var m = cachedDataArray.length;
for (var i = 0; i < l; i++) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
var no = receivedDataArray[i];
if (!no.action) {
cachedDataArray.push(no);
continue;
}
for (var j = 0; j < m; j++) {
/* eslint-disable security/detect-object-injection */ // verified false positive
if (no._id === cachedDataArray[j]._id) {
if (no.action === 'remove') {
cachedDataArray.splice(j, 1);
+21 -11
View File
@@ -255,6 +255,8 @@ function init (client, d3) {
(durationText ? '<strong>' + translate('Duration') + ':</strong> ' + durationText + '<br/>' : '') +
(d.insulinNeedsScaleFactor ? '<strong>' + translate('Insulin Scale Factor') + ':</strong> ' + d.insulinNeedsScaleFactor * 100 + '%<br/>' : '') +
(correctionRangeText ? '<strong>' + translate('Correction Range') + ':</strong> ' + correctionRangeText + '<br/>' : '') +
(d.transmitterId ? '<strong>' + translate('Transmitter ID') + ':</strong> ' + d.transmitterId + '<br/>' : '') +
(d.sensorCode ? '<strong>' + translate('Sensor Code') + ':</strong> ' + d.sensorCode + '<br/>' : '') +
(d.notes ? '<strong>' + translate('Notes') + ':</strong> ' + d.notes : '');
}
@@ -527,7 +529,7 @@ function init (client, d3) {
};
}
function prepareArc (treatment, radius, renderBasal) {
function prepareArc (treatment, radius, bolusSettings) {
var arc_data = [
// white carb half-circle on top
{ 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': radius.R1 }
@@ -564,12 +566,14 @@ function init (client, d3) {
if (treatment.insulin > 0) {
var dosage_units = '' + Math.round(treatment.insulin * 100) / 100;
var format = treatment.insulin < bolusSettings.renderOver ? bolusSettings.renderFormatSmall : bolusSettings.renderFormat;
if (renderBasal === 'all-remove-zero-u') {
if (_.includes(['concise', 'minimal'], format)) {
dosage_units = (dosage_units + "").replace(/^0/, "");
}
var unit_of_measurement = (renderBasal === 'all-remove-zero-u' ? '' : ' U'); // One international unit of insulin (1 IU) is shown as '1 U'
var unit_of_measurement = (format === 'minimal' ? '' : ' U'); // One international unit of insulin (1 IU) is shown as '1 U'
arc_data[3].element = dosage_units + unit_of_measurement;
}
@@ -614,6 +618,7 @@ function init (client, d3) {
if (treatment.boluscalc.foods && treatment.boluscalc.foods.length) {
html += '<table><tr><td><strong>' + translate('Food') + '</strong></td></tr>';
for (var fi = 0; fi < treatment.boluscalc.foods.length; fi++) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
var f = treatment.boluscalc.foods[fi];
html += '<tr>';
html += '<td>' + f.name + '</td>';
@@ -630,7 +635,7 @@ function init (client, d3) {
var glucose = treatment.glucose;
if (client.settings.units != client.ddata.profile.getUnits()) {
glucose *= (client.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL);
var decimals = (client.settings.units === 'mmol' ? 10 : 1);
const decimals = (client.settings.units === 'mmol' ? 10 : 1);
glucose = Math.round(glucose * decimals) / decimals;
}
@@ -641,7 +646,7 @@ function init (client, d3) {
(treatment.protein ? '<strong>' + translate('Protein') + ':</strong> ' + treatment.protein + '<br/>' : '') +
(treatment.fat ? '<strong>' + translate('Fat') + ':</strong> ' + treatment.fat + '<br/>' : '') +
(treatment.absorptionTime > 0 ? '<strong>' + translate('Absorption Time') + ':</strong> ' + (Math.round(treatment.absorptionTime / 60.0 * 10) / 10) + 'h' + '<br/>' : '') +
(treatment.insulin ? '<strong>' + translate('Insulin') + ':</strong> ' + treatment.insulin + '<br/>' : '') +
(treatment.insulin ? '<strong>' + translate('Insulin') + ':</strong> ' + utils.toRoundedStr(treatment.insulin, 2) + '<br/>' : '') +
(treatment.enteredinsulin ? '<strong>' + translate('Combo Bolus') + ':</strong> ' + treatment.enteredinsulin + 'U, ' + treatment.splitNow + '% : ' + treatment.splitExt + '%, ' + translate('Duration') + ': ' + treatment.duration + '<br/>' : '') +
(treatment.glucose ? '<strong>' + translate('BG') + ':</strong> ' + glucose + (treatment.glucoseType ? ' (' + translate(treatment.glucoseType) + ')' : '') + '<br/>' : '') +
(treatment.enteredBy ? '<strong>' + translate('Entered By') + ':</strong> ' + treatment.enteredBy + '<br/>' : '') +
@@ -990,7 +995,8 @@ function init (client, d3) {
renderer.drawTreatments = function drawTreatments (client) {
var treatmentCount = 0;
var renderBasal = client.settings.extendedSettings.bolus.render;
var bolusSettings = client.settings.extendedSettings.bolus || {};
chart().focus.selectAll('.draggable-treatment').remove();
_.forEach(client.ddata.treatments, function eachTreatment (d) {
@@ -999,17 +1005,21 @@ function init (client, d3) {
// add treatment bubbles
_.forEach(client.ddata.treatments, function eachTreatment (d) {
var showLabels = ( !d.carbs && ( ( renderBasal == 'none') || ( renderBasal === 'over' && d.insulin < client.settings.bolusRenderOver) ) ) ? false : true;
var showLabels = d.carbs || d.insulin;
if (d.insulin && d.insulin < bolusSettings.renderOver && bolusSettings.renderFormatSmall == 'hidden') {
showLabels = false;
}
renderer.drawTreatment(d, {
scale: renderer.bubbleScale()
, showLabels: showLabels
, treatments: treatmentCount
}, client.sbx.data.profile.getCarbRatio(new Date()),
renderBasal);
}
, client.sbx.data.profile.getCarbRatio(new Date())
, bolusSettings);
});
};
renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio, renderBasal) {
renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio, bolusSettings) {
if (!treatment.carbs && !treatment.protein && !treatment.fat && !treatment.insulin) {
return;
}
@@ -1027,7 +1037,7 @@ function init (client, d3) {
return;
}
var arc = prepareArc(treatment, radius, renderBasal);
var arc = prepareArc(treatment, radius, bolusSettings);
var treatmentDots = appendTreatments(treatment, arc);
appendLabels(treatmentDots, arc, opts);
};
+9 -2
View File
@@ -66,16 +66,23 @@ module.exports = function calcDelta (oldData, newData) {
return result;
}
function genKey(o) {
let r = o.mills;
r += o.sgv ? 'sgv' + o.sgv : '';
r += o.mgdl ? 'sgv' + o.mgdl : '';
return r;
}
function nsArrayDiff(oldArray, newArray) {
var seen = {};
var l = oldArray.length;
for (var i = 0; i < l; i++) {
seen[oldArray[i].mills] = true;
seen[genKey(oldArray[i])] = true;
}
var result = [];
l = newArray.length;
for (var j = 0; j < l; j++) {
if (!Object.prototype.hasOwnProperty.call(seen, newArray[j].mills)) {
if (!Object.prototype.hasOwnProperty.call(seen, genKey(newArray[j]))) {
result.push(newArray[j]);
}
}
+9 -7
View File
@@ -1,13 +1,13 @@
'use strict';
var init = function init () {
//for the tests window isn't the global object
var $ = window.$;
var _ = window._;
var Nightscout = window.Nightscout;
var client = Nightscout.client;
(function () {
client.init(function loaded () {
var translate = client.translate;
@@ -146,7 +146,7 @@ client.init(function loaded () {
$('#fe_filter_subcategory').empty().append(new Option(translate('(none)'),''));
if (filter.category !== '') {
for (s in categories[filter.category]) {
if (categories[filter.category].hasOwnProperty(s)) {
if (Object.prototype.hasOwnProperty.call(categories[filter.category],s)) {
$('#fe_filter_subcategory').append(new Option(s,s));
}
}
@@ -162,7 +162,7 @@ client.init(function loaded () {
$('#fe_subcategory_list').empty().append(new Option(translate('(none)'),''));
if (foodrec.category !== '') {
for (s in categories[foodrec.category]) {
if (categories[foodrec.category].hasOwnProperty(s)) {
if (Object.prototype.hasOwnProperty.call(categories[foodrec.category],s)) {
$('#fe_subcategory_list').append(new Option(s,s));
}
}
@@ -198,7 +198,7 @@ client.init(function loaded () {
$('#fe_filter_category').empty().append(new Option(translate('(none)'),''));
$('#fe_category_list').empty().append(new Option(translate('(none)'),''));
for (var s in categories) {
if (categories.hasOwnProperty(s)) {
if (Object.prototype.hasOwnProperty.call(categories,s)) {
$('#fe_filter_category').append(new Option(s,s));
$('#fe_category_list').append(new Option(s,s));
}
@@ -398,7 +398,7 @@ client.init(function loaded () {
function savePortions(event) {
var index = $(this).attr('index');
var findex = $(this).attr('findex');
var val = parseFloat($(this).val().replace(/\,/g,'.'));
var val = parseFloat($(this).val().replace(/,/g,'.'));
foodquickpick[index].foods[findex].portions=val;
calculateCarbs(index);
drawQuickpick();
@@ -677,4 +677,6 @@ client.init(function loaded () {
}
}
});
})();
};
module.exports = init;
+62 -33
View File
@@ -2,9 +2,9 @@
var _ = require('lodash');
function init(fs) {
function init (fs) {
function language() {
function language () {
return language;
}
@@ -16,7 +16,7 @@ function init(fs) {
, { code: 'cs', file: 'cs_CZ', language: 'Čeština', speechCode: 'cs-CZ' }
, { code: 'de', file: 'de_DE', language: 'Deutsch', speechCode: 'de-DE' }
, { code: 'dk', file: 'da_DK', language: 'Dansk', speechCode: 'dk-DK' }
, { code: 'el', file: 'el_GR', language: 'Ελληνικά', speechCode: 'el-GR'}
, { code: 'el', file: 'el_GR', language: 'Ελληνικά', speechCode: 'el-GR' }
, { code: 'en', file: 'en_US', language: 'English', speechCode: 'en-US' }
, { code: 'es', file: 'es_ES', language: 'Español', speechCode: 'es-ES' }
, { code: 'fi', file: 'fi_FI', language: 'Suomi', speechCode: 'fi-FI' }
@@ -30,38 +30,40 @@ function init(fs) {
, { code: 'nb', file: 'nb_NO', language: 'Norsk (Bokmål)', speechCode: 'no-NO' }
, { code: 'nl', file: 'nl_NL', language: 'Nederlands', speechCode: 'nl-NL' }
, { code: 'pl', file: 'pl_PL', language: 'Polski', speechCode: 'pl-PL' }
, { code: 'pt', file: 'pt_BR', language: 'Português (Brasil)', speechCode: 'pt-BR' }
, { code: 'pt', file: 'pt_PT', language: 'Português', speechCode: 'pt-PT' }
, { code: 'br', file: 'pt_BR', language: 'Português (Brasil)', speechCode: 'pt-BR' }
, { code: 'ro', file: 'ro_RO', language: 'Română', speechCode: 'ro-RO' }
, { code: 'ru', file: 'ru_RU', language: 'Русский', speechCode: 'ru-RU' }
, { code: 'sk', file: 'sl_SL', language: 'Slovenčina', speechCode: 'sk-SK' }
, { code: 'sk', file: 'sk_SK', language: 'Slovenčina', speechCode: 'sk-SK' }
, { code: 'sl', file: 'sl_SL', language: 'Slovenščina', speechCode: 'sl-SL' }
, { code: 'sv', file: 'sv_SE', language: 'Svenska', speechCode: 'sv-SE' }
, { code: 'tr', file: 'tr_TR', language: 'Türkçe', speechCode: 'tr-TR' }
, { code: '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' }
];
var translations = {};
language.translations = translations;
language.offerTranslations = function offerTranslations(localization) {
language.offerTranslations = function offerTranslations (localization) {
translations = localization;
language.translations = translations;
}
// case sensitive
language.translateCS = function translateCaseSensitive(text) {
language.translateCS = function translateCaseSensitive (text) {
if (translations[text]) {
return translations[text];
}
// console.log('localization:', text, 'not found');
// console.log('localization:', text, 'not found');
return text;
};
// case insensitive
language.translateCI = function translateCaseInsensitive(text) {
language.translateCI = function translateCaseInsensitive (text) {
var utext = text.toUpperCase();
_.forEach(translations, function (ts, key) {
_.forEach(translations, function(ts, key) {
var ukey = key.toUpperCase();
if (ukey === utext) {
text = ts;
@@ -70,69 +72,96 @@ function init(fs) {
return text;
};
language.translate = function translate(text, options) {
language.translate = function translate (text, options) {
var translated;
if (options && options.ci) {
translated = language.translateCI(text);
} else {
translated = language.translateCS(text);
}
if (options && options.params) {
for (var i = 0; i < options.params.length; i++) {
// eslint-disable-next-line no-useless-escape
var r = new RegExp('\%' + (i+1), 'g');
translated = translated.replace(r, options.params[i]);
var hasCI = false;
var hasParams = false;
if (options) {
hasCI = Object.prototype.hasOwnProperty.call(options,'ci');
hasParams = Object.prototype.hasOwnProperty.call(options,'params');
}
var keys = hasParams ? options.params : null;
if (options && !hasCI && !hasParams) {
keys = [];
for (var i = 1; i < arguments.length; i++) {
keys.push(arguments[i]);
}
}
if (options && (hasCI || hasParams) && arguments.length > 2) {
if (!keys) keys = [];
for (i = 2; i < arguments.length; i++) {
keys.push(arguments[i]);
}
}
if (keys) {
for (i = 0; i < keys.length; i++) {
/* eslint-disable-next-line no-useless-escape, security/detect-non-literal-regexp */ // validated false positive
var r = new RegExp('\%' + (i + 1), 'g');
translated = translated.replace(r, keys[i]);
}
}
return translated;
};
language.DOMtranslate = function DOMtranslate($) {
language.DOMtranslate = function DOMtranslate ($) {
// do translation of static text on load
$('.translate').each(function () {
$('.translate').each(function() {
$(this).text(language.translate($(this).text()));
});
$('.titletranslate, .tip').each(function () {
$(this).attr('title',language.translate($(this).attr('title')));
$(this).attr('original-title',language.translate($(this).attr('original-title')));
$(this).attr('placeholder',language.translate($(this).attr('placeholder')));
});
});
$('.titletranslate, .tip').each(function() {
$(this).attr('title', language.translate($(this).attr('title')));
$(this).attr('original-title', language.translate($(this).attr('original-title')));
$(this).attr('placeholder', language.translate($(this).attr('placeholder')));
});
};
language.getFilename = function getFilename(code) {
language.getFilename = function getFilename (code) {
if (code == 'en') {
return 'en/en.json';
}
let file;
language.languages.forEach(function (l) {
language.languages.forEach(function(l) {
if (l.code == code) file = l.file;
});
return file + '.json';
}
// this is a server only call and needs fs by reference as the class is also used in the client
language.loadLocalization = function loadLocalization(fs, path) {
language.loadLocalization = function loadLocalization (fs, path) {
let filename = './translations/' + this.getFilename(this.lang);
if (path) filename = path.resolve(__dirname, filename);
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive; well defined set of values
const l = fs.readFileSync(filename);
this.offerTranslations(JSON.parse(l));
}
language.set = function set(newlang) {
language.set = function set (newlang) {
language.lang = newlang;
language.languages.forEach(function (l) {
language.languages.forEach(function(l) {
if (l.code === language.lang && l.speechCode) language.speechCode = l.speechCode;
});
return language();
};
language.get = function get(lang) {
language.get = function get (lang) {
var r;
language.languages.forEach(function (l) {
language.languages.forEach(function(l) {
if (l.code === lang) r = l;
});
return r;
+22
View File
@@ -0,0 +1,22 @@
'use strict';
function init () {
var bolus = {
name: 'bolus'
, label: 'Bolus'
, pluginType: 'fake'
};
bolus.getPrefs = function getPrefs(sbx) {
return {
renderFormat: sbx.extendedSettings.renderFormat ? sbx.extendedSettings.renderFormat : 'default'
, renderOver: sbx.extendedSettings.renderOver ? sbx.extendedSettings.renderOver : 0
, notifyOver: sbx.extendedSettings.notifyOver ? sbx.extendedSettings.notifyOver : 0
};
};
return bolus;
}
module.exports = init;
+21 -21
View File
@@ -15,87 +15,87 @@ function init() {
return [
{ val: '<none>'
, name: '<none>'
, bg: true, insulin: true, carbs: true, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: true, carbs: true, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'BG Check'
, name: 'BG Check'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Snack Bolus'
, name: 'Snack Bolus'
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Meal Bolus'
, name: 'Meal Bolus'
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Correction Bolus'
, name: 'Correction Bolus'
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Carb Correction'
, name: 'Carb Correction'
, bg: true, insulin: false, carbs: true, protein: true, fat: true, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: true, protein: true, fat: true, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Combo Bolus'
, name: 'Combo Bolus'
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: true, percent: false, absolute: false, profile: false, split: true
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: true, percent: false, absolute: false, profile: false, split: true, sensor: false
}
, { val: 'Announcement'
, name: 'Announcement'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Note'
, name: 'Note'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Question'
, name: 'Question'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Exercise'
, name: 'Exercise'
, bg: false, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false
, bg: false, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Site Change'
, name: 'Pump Site Change'
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Sensor Start'
, name: 'CGM Sensor Start'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: true
}
, { val: 'Sensor Change'
, name: 'CGM Sensor Insert'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: true
}
, { val: 'Sensor Stop'
, name: 'CGM Sensor Stop'
, bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Pump Battery Change'
, name: 'Pump Battery Change'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Insulin Change'
, name: 'Insulin Cartridge Change'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Temp Basal Start'
, name: 'Temp Basal Start'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: true, absolute: true, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: true, absolute: true, profile: false, split: false, sensor: false
}
, { val: 'Temp Basal End'
, name: 'Temp Basal End'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false, sensor: false
}
, { val: 'Profile Switch'
, name: 'Profile Switch'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: true, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: true, split: false, sensor: false
}
, { val: 'D.A.D. Alert'
, name: 'D.A.D. Alert'
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
}
];
+2 -1
View File
@@ -46,6 +46,7 @@ function init (ctx) {
, require('./insulinage')(ctx)
, require('./batteryage')(ctx)
, require('./basalprofile')(ctx)
, require('./bolus')(ctx) // fake plugin to hold extended settings
, require('./boluscalc')(ctx) // fake plugin to show/hide
, require('./profile')(ctx) // fake plugin to hold extended settings
, require('./speech')(ctx)
@@ -128,7 +129,7 @@ function init (ctx) {
};
//these plugins are either always on or have custom settings
plugins.specialPlugins = 'ar2 bgnow delta direction timeago upbat rawbg errorcodes profile';
plugins.specialPlugins = 'ar2 bgnow delta direction timeago upbat rawbg errorcodes profile bolus';
plugins.shownPlugins = function(sbx) {
return _filter(enabledPlugins, function filterPlugins (plugin) {
+25 -7
View File
@@ -34,6 +34,14 @@ function init (ctx) {
var retroFields = cleanList(sbx.extendedSettings.retroFields);
retroFields = isEmpty(retroFields) ? ['reservoir', 'battery'] : retroFields;
var profile = sbx.data.profile;
var warnBattQuietNight = sbx.extendedSettings.warnBattQuietNight;
if (warnBattQuietNight && (!profile || !profile.hasData() || !profile.getTimezone())) {
console.warn('PUMP_WARN_BATT_QUIET_NIGHT requires a treatment profile with time zone set to obtain user time zone');
warnBattQuietNight = false;
}
return {
fields: fields
, retroFields: retroFields
@@ -47,6 +55,9 @@ function init (ctx) {
, urgentBattP: sbx.extendedSettings.urgentBattP || 20
, warnOnSuspend: sbx.extendedSettings.warnOnSuspend || false
, enableAlerts: sbx.extendedSettings.enableAlerts || false
, warnBattQuietNight: warnBattQuietNight || false
, dayStart: sbx.settings.dayStart
, dayEnd: sbx.settings.dayEnd
};
};
@@ -225,7 +236,11 @@ function init (ctx) {
function updateReservoir (prefs, result) {
if (result.reservoir) {
result.reservoir.label = 'Reservoir';
result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
if (result.reservoir_display_override) {
result.reservoir.display = result.reservoir_display_override;
} else {
result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
}
if (result.reservoir.value < prefs.urgentRes) {
result.reservoir.level = levels.URGENT;
result.reservoir.message = 'URGENT: Pump Reservoir Low';
@@ -242,17 +257,17 @@ function init (ctx) {
}
}
function updateBattery (type, prefs, result) {
function updateBattery (type, prefs, result, batteryWarn) {
if (result.battery) {
result.battery.label = 'Battery';
result.battery.display = result.battery.value + type;
var urgent = type === 'v' ? prefs.urgentBattV : prefs.urgentBattP;
var warn = type === 'v' ? prefs.warnBattV : prefs.warnBattP;
if (result.battery.value < urgent) {
if (result.battery.value < urgent && batteryWarn) {
result.battery.level = levels.URGENT;
result.battery.message = 'URGENT: Pump Battery Low';
} else if (result.battery.value < warn) {
} else if (result.battery.value < warn && batteryWarn) {
result.battery.level = levels.WARN;
result.battery.message = 'Warning, Pump Battery Low';
} else {
@@ -296,11 +311,14 @@ function init (ctx) {
function prepareData (prop, prefs, sbx) {
var pump = (prop && prop.pump) || { };
var time = (sbx.data.profile && sbx.data.profile.getTimezone()) ? moment(sbx.time).tz(sbx.data.profile.getTimezone()) : moment(sbx.time);
var now = time.hours() + time.minutes() / 60.0 + time.seconds() / 3600.0;
var batteryWarn = !(prefs.warnBattQuietNight && (now < prefs.dayStart || now > prefs.dayEnd));
var result = {
level: levels.NONE
, clock: pump.clock ? { value: moment(pump.clock) } : null
, reservoir: pump.reservoir || pump.reservoir === 0 ? { value: pump.reservoir } : null
, reservoir_display_override: pump.reservoir_display_override || null
, manufacturer: pump.manufacturer
, model: pump.model
, extended: pump.extended || null
@@ -312,10 +330,10 @@ function init (ctx) {
if (pump.battery && pump.battery.percent) {
result.battery = { value: pump.battery.percent, unit: 'percent' };
updateBattery('%', prefs, result);
updateBattery('%', prefs, result, batteryWarn);
} else if (pump.battery && pump.battery.voltage) {
result.battery = { value: pump.battery.voltage, unit: 'volts'};
updateBattery('v', prefs, result);
updateBattery('v', prefs, result, batteryWarn);
}
result.device = { label: translate('Device'), display: prop.device };
+6
View File
@@ -184,6 +184,12 @@ function init(ctx) {
if (!_.isEmpty(latest[event].notes)) {
info.push({label: translate('Notes'), value: latest[event].notes});
}
if (!_.isEmpty(latest[event].transmitterId)) {
info.push({label: translate('Transmitter ID'), value: latest[event].transmitterId});
}
if (!_.isEmpty(latest[event].sensorCode)) {
info.push({label: translate('Sensor Code'), value: latest[event].sensorCode});
}
}
});
+5
View File
@@ -20,12 +20,17 @@ function init(ctx) {
function filterTreatments (sbx) {
var treatments = sbx.data.treatments;
var includeBolusesOver = sbx.extendedSettings.includeBolusesOver || 0;
treatments = _.filter(treatments, function notOpenAPS (treatment) {
var ok = true;
var enteredBy = treatment.enteredBy;
if (enteredBy && (enteredBy.indexOf('openaps://') === 0 || enteredBy.indexOf('loop://') === 0)) {
ok = _.indexOf(MANUAL_TREATMENTS, treatment.eventType) >= 0;
}
if (ok && _.isNumber(treatment.insulin) && _.includes(['Meal Bolus', 'Correction Bolus'], treatment.eventType)) {
ok = treatment.insulin >= includeBolusesOver;
}
return ok;
});
@@ -1,5 +1,6 @@
(function () {
'use strict';
'use strict';
var init = function init () {
//for the tests window isn't the global object
var $ = window.$;
var _ = window._;
@@ -99,12 +100,12 @@
_.each(mongoprofile.store, function eachStoredProfile (p) {
// allign with default profile
for (var key in defaultprofile) {
if (defaultprofile.hasOwnProperty(key) && !p.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(defaultprofile,key) && !Object.prototype.hasOwnProperty.call(p,key)) {
p[key] = defaultprofile[key];
}
}
for (key in p) {
if (p.hasOwnProperty(key) && !defaultprofile.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(p,key) && !Object.prototype.hasOwnProperty.call(defaultprofile,key)) {
delete p[key];
}
}
@@ -231,7 +232,7 @@
$('#pe_profiles').empty();
for (var key in record.store) {
if (record.store.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(record.store,key)) {
$('#pe_profiles').append('<option value="' + key + '">' + key + '</option>');
}
}
@@ -655,7 +656,7 @@
var adjustedRecord = _.cloneDeep(record);
for (var key in adjustedRecord.store) {
if (adjustedRecord.store.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(adjustedRecord.store,key)) {
var profile = adjustedRecord.store[key];
if (!profile.perGIvalues) {
delete profile.perGIvalues;
@@ -706,7 +707,7 @@
function getFirstAvailableProfile(record) {
var availableProfiles = [];
for (var key in record.store) {
if (record.store.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(record.store,key)) {
if (key !== currentprofile) {
availableProfiles.push(key);
}
@@ -721,4 +722,6 @@
}
}
});
})();
};
module.exports = init;
+4 -2
View File
@@ -7,10 +7,10 @@ var times = require('./times');
var cacheTTL = 5000;
var prevBasalTreatment = null;
var cache = new c.Cache();
function init (profileData) {
var cache = new c.Cache();
var profile = {};
profile.clear = function clear() {
@@ -19,6 +19,8 @@ function init (profileData) {
prevBasalTreatment = null;
}
profile.clear();
profile.loadData = function loadData (profileData) {
if (profileData && profileData.length) {
profile.data = profile.convertToProfileStore(profileData);
@@ -36,7 +38,7 @@ function init (profileData) {
var newObject = {};
newObject.defaultProfile = 'Default';
newObject.store = {};
newObject.startDate = profile.startDate;
newObject.startDate = profile.startDate ? profile.startDate : '1980-01-01';
newObject._id = profile._id;
newObject.convertedOnTheFly = true;
delete profile.startDate;
+5 -4
View File
@@ -1,6 +1,3 @@
// TODO:
// - bypass nightmode in reports
// - on save/delete treatment ctx.bus.emit('data-received'); is not enough. we must add something like 'data-updated'
var init = function init () {
'use strict';
@@ -249,7 +246,11 @@ var init = function init () {
options.cob = true;
options.openAps = true;
}
options.bgcheck = $('#rp_optionsbgcheck').is(':checked');
options.othertreatments = $('#rp_optionsothertreatments').is(':checked');
const reportStorage = require('./reportstorage');
reportStorage.saveProps(options);
var matchesneeded = 0;
// date range
+37
View File
@@ -0,0 +1,37 @@
const storage = require('js-storage').localStorage;
const COOKIE_KEY = 'reportProperties';
const defaultValues = {
insulin: true,
carbs: true,
basal: true,
notes: false,
food: true,
raw: false,
iob: false,
cob: false,
predicted: false,
openAps: false,
insulindistribution: true,
predictedTruncate: true,
bgcheck: true,
othertreatments: false
};
let cachedProps;
const saveProps = function (props) {
let propsToSave = {};
for (const prop in props) {
if (!Object.prototype.hasOwnProperty.call(defaultValues, prop))
continue;
propsToSave[prop] = props[prop];
}
storage.set(COOKIE_KEY, propsToSave);
};
const getValue = function (p) {
if (!cachedProps)
cachedProps = storage.get(COOKIE_KEY) || defaultValues;
return cachedProps[p];
};
module.exports = {saveProps: saveProps, getValue: getValue};
+28 -16
View File
@@ -18,22 +18,25 @@ function init () {
module.exports = init;
daytoday.html = function html (client) {
const reportStorage = require('../report/reportstorage');
var translate = client.translate;
var ret =
'<h2>' + translate('Day to day') + '</h2>' +
'<b>' + translate('To see this report, press SHOW while in this view') + '</b><br>' +
translate('Display') + ': ' +
'<label><input type="checkbox" id="rp_optionsinsulin" checked><span style="color:blue;opacity:0.5">' + translate('Insulin') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionscarbs" checked><span style="color:red;opacity:0.5">' + translate('Carbs') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionsbasal" checked><span style="color:#0099ff;opacity:0.5">' + translate('Basal rate') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionsnotes">' + translate('Notes') + '</label>' +
'<label><input type="checkbox" id="rp_optionsfood" checked>' + translate('Food') + '</label>' +
'<label><input type="checkbox" id="rp_optionsraw"><span style="color:gray;opacity:1">' + translate('Raw') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionsiob"><span style="color:blue;opacity:0.5">' + translate('IOB') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionscob"><span style="color:red;opacity:0.5">' + translate('COB') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionspredicted"><span style="color:sienna;opacity:0.5">' + translate('Predictions') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionsopenaps"><span style="color:sienna;opacity:0.5">' + translate('OpenAPS') + '</span></label>' +
'<label><input type="checkbox" id="rp_optionsdistribution" checked><span style="color:blue;opacity:0.5">' + translate('Insulin distribution') + '</span></label>' +
`<label><input type="checkbox" id="rp_optionsinsulin" ${reportStorage.getValue('insulin') ? "checked" : ""}><span style="color:blue;opacity:0.5"> ${translate('Insulin')} </span></label>` +
`<label><input type="checkbox" id="rp_optionscarbs" ${reportStorage.getValue('carbs') ? "checked" : ""}><span style="color:red;opacity:0.5">${translate('Carbs')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsbasal" ${reportStorage.getValue('basal') ? "checked" : ""}><span style="color:#0099ff;opacity:0.5">${translate('Basal rate')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsnotes" ${reportStorage.getValue('notes') ? "checked" : ""}>${translate('Notes')}</label>` +
`<label><input type="checkbox" id="rp_optionsfood" ${reportStorage.getValue('food') ? "checked" : ""}>${translate('Food')}</label>` +
`<label><input type="checkbox" id="rp_optionsraw" ${reportStorage.getValue('raw') ? "checked" : ""}><span style="color:gray;opacity:1">${translate('Raw')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsiob" ${reportStorage.getValue('iob') ? "checked" : ""}><span style="color:blue;opacity:0.5">${translate('IOB')}</span></label>` +
`<label><input type="checkbox" id="rp_optionscob" ${reportStorage.getValue('cob') ? "checked" : ""}><span style="color:red;opacity:0.5">${translate('COB')}</span></label>` +
`<label><input type="checkbox" id="rp_optionspredicted" ${reportStorage.getValue('predicted') ? "checked" : ""}><span style="color:sienna;opacity:0.5">${translate('Predictions')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsopenaps" ${reportStorage.getValue('openAps') ? "checked" : ""}><span style="color:sienna;opacity:0.5">${translate('OpenAPS')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsdistribution" ${reportStorage.getValue('insulindistribution') ? "checked" : ""}><span style="color:blue;opacity:0.5">${translate('Insulin distribution')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsbgcheck" ${reportStorage.getValue('bgcheck') ? "checked" : ""}><span style="color:#ff0000;opacity:0.5">${translate('BG Check')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsothertreatments" ${reportStorage.getValue('othertreatments') ? "checked" : ""}>${translate('View all treatments')}</span></label>` +
'&nbsp;' + translate('Size') +
' <select id="rp_size">' +
' <option x="800" y="250">800x250px</option>' +
@@ -48,9 +51,9 @@ daytoday.html = function html (client) {
translate('Linear') + '</label>' +
'<label><input type="radio" name="rp_scale" id="rp_log">' +
translate('Logarithmic') + '</label>' +
'<div id="rp_predictedSettings" style="display:none">' +
`<div id="rp_predictedSettings" ${reportStorage.getValue('predicted') ? '' : 'style="display:none"'}>` +
translate('Truncate predictions: ') +
'<input type="checkbox" id="rp_optionsPredictedTruncate" checked>' +
`<input type="checkbox" id="rp_optionsPredictedTruncate" ${reportStorage.getValue('predictedTruncate') ? "checked" : ""}>` +
'<br>' +
translate('Predictions offset') + ': ' +
'<b><label id="rp_predictedOffset"></label> minutes</b>' +
@@ -762,7 +765,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
}
if (treatment.insulin && options.insulin) {
var dataLabel = client.utils.toFixedMin(treatment.insulin,2)+ 'U';
var dataLabel = client.utils.toRoundedStr(treatment.insulin, 2)+ 'U';
context.append('rect')
.attr('y', yInsulinScale(treatment.insulin))
.attr('height', chartHeight - yInsulinScale(treatment.insulin))
@@ -872,7 +875,16 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
.attr('y', yScale2(client.utils.scaleMgdl(414)) + padding.top)
.attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left)
.text(treatment.reason);
} else if (!treatment.duration) {
} else if (treatment.eventType === 'BG Check' && !treatment.duration && options.bgcheck) {
context.append('circle')
.attr('cx', xScale2(treatment.mills) + padding.left)
.attr('cy', yScale2(scaledTreatmentBG(treatment, data.sgv)) + padding.top)
.attr('fill', 'red')
.style('opacity', 1)
.attr('stroke-width', 1)
.attr('stroke', 'darkred')
.attr('r', 4);
} else if (!treatment.duration && options.othertreatments) {
// other treatments without duration
context.append('circle')
.attr('cx', xScale2(treatment.mills) + padding.left)
@@ -889,7 +901,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
.attr('y', yScale2(scaledTreatmentBG(treatment, data.sgv)) + padding.top - 10)
.attr('x', xScale2(treatment.mills) + padding.left + 10)
.text(translate(client.careportal.resolveEventName(treatment.eventType)));
} else if (treatment.duration) {
} else if (treatment.duration && options.othertreatments) {
// other treatments with duration
context.append('rect')
.attr('x', xScale2(treatment.mills) + padding.left)
+15 -17
View File
@@ -42,24 +42,22 @@ loopalyzer.html = function html (client) {
ret += '<br/>';
ret += '<span id="rp_loopalyzertimeshiftinput">'; /* So we can show only if viewing multiple days style="display:none;" */
ret += '<input type="checkbox" id="rp_loopalyzertimeshift">';
ret += translate('Timeshift on meals larger than') + ' ';
ret += '<input type="number" style="width: 3.5em" value="10" id="rp_loopalyzermincarbs">' + ' ' + translate('g carbs');
ret += '&nbsp;' + translate('consumed between');
ret += ' <select id="rp_loopalyzert1">';
for (let i = 0; i < 24; i++) {
const H = (i < 10 ? '0' : '') + i;
ret += ' <option t1="' + H + ':00"' + (i == 6 ? ' selected' : '') + '>' + H + ':00</option>';
ret += ' <option t1="' + H + ':30">' + H + ':30</option>';
let numberInput = '<input type="number" style="width: 3.5em" value="10" id="rp_loopalyzermincarbs">';
function genTimePicker(id) {
let timerPicker = ' <select id="' + id + '">';
for (let i = 0; i < 24; i++) {
const H = (i < 10 ? '0' : '') + i;
timerPicker += ' <option t1="' + H + ':00"' + (i == 6 ? ' selected' : '') + '>' + H + ':00</option>';
timerPicker += ' <option t1="' + H + ':30">' + H + ':30</option>';
}
timerPicker += '</select>';
return timerPicker;
}
ret += '</select>';
ret += ' ' + translate('and');
ret += ' <select id="rp_loopalyzert2">';
for (let i = 0; i < 24; i++) {
const H = (i < 10 ? '0' : '') + i;
ret += ' <option t2="' + H + ':00"' + (i == 9 ? ' selected' : '') + '>' + H + ':00</option>';
ret += ' <option t2="' + H + ':30">' + H + ':30</option>';
}
ret += '</select>';
ret += translate('Timeshift on meals larger than %1 g carbs consumed between %2 and %3', numberInput, genTimePicker('rp_loopalyzert1'), genTimePicker('rp_loopalyzert2'));
ret += '</span>'; /* timeShift */
ret += '<br/><br/>';
ret += '<input type="button" onclick="loopalyzerMoreBackward();" value="&lt;&lt;&lt;&nbsp;' + translate('Previous') + '">';
+47 -58
View File
@@ -4,16 +4,30 @@ const _get = require('lodash/get');
const express = require('express');
const compression = require('compression');
const bodyParser = require('body-parser');
const randomToken = require('random-token');
const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
function resolvePath(filePath) {
if (fs.existsSync(filePath)) return filePath;
let p = path.join(__dirname, filePath);
if (fs.existsSync(p)) return p;
p = path.join(process.cwd(), filePath);
if (fs.existsSync(p)) return p;
return require.resolve(filePath);
}
function create (env, ctx) {
var app = express();
var appInfo = env.name + ' ' + env.version;
app.set('title', appInfo);
app.enable('trust proxy'); // Allows req.secure test on heroku https connections.
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
var insecureUseHttp = env.insecureUseHttp;
var secureHstsHeader = env.secureHstsHeader;
if (!insecureUseHttp) {
@@ -66,7 +80,7 @@ function create (env, ctx) {
, reportOnly: secureCspReportOnly
};
}
console.info('Enabled SECURE_HSTS_HEADER (HTTP Strict Transport Security)');
const helmet = require('helmet');
@@ -101,42 +115,27 @@ function create (env, ctx) {
}
app.set('view engine', 'ejs');
// this allows you to render .html files as templates in addition to .ejs
app.engine('html', require('ejs').renderFile);
app.set("views", path.join(__dirname, "views/"));
app.set("views", resolvePath('/views'));
let cacheBuster = 'developmentMode';
let lastModified = new Date();
let busterPath = '/tmp/cacheBusterToken';
if (process.env.NODE_ENV !== 'development') {
busterPath = process.cwd() + busterPath;
} else {
busterPath = __dirname + busterPath;
}
if (fs.existsSync(busterPath)) {
cacheBuster = fs.readFileSync(busterPath).toString().trim();
var stats = fs.statSync(busterPath);
lastModified = stats.mtime;
}
let cacheBuster = process.env.NODE_ENV == 'development' ? 'developmentMode': randomToken(16);
app.locals.cachebuster = cacheBuster;
let lastModified = new Date();
app.get("/robots.txt", (req, res) => {
res.setHeader('Content-Type', 'text/plain');
res.send(['User-agent: *','Disallow: /'].join('\n'));
});
const swcontent = fs.readFileSync(resolvePath('/views/service-worker.js'), { encoding: 'utf-8' });
app.get("/sw.js", (req, res) => {
res.setHeader('Content-Type', 'application/javascript');
if (process.env.NODE_ENV !== 'development') {
res.setHeader('Last-Modified', lastModified.toUTCString());
}
res.send(ejs.render(fs.readFileSync(
require.resolve(`${__dirname}/views/service-worker.js`),
{ encoding: 'utf-8' }),
{ locals: app.locals}
));
res.send(ejs.render(swcontent, { locals: app.locals} ));
});
// Allow static resources to be cached for week
@@ -147,19 +146,19 @@ function create (env, ctx) {
console.log('Development environment detected, setting static file cache age to 1 second');
}
var staticFiles = express.static(env.static_files, {
var staticFiles = express.static(resolvePath(env.static_files), {
maxAge
});
// serve the static content
app.use(staticFiles);
app.use('/translations', express.static('translations', {
app.use('/translations', express.static(resolvePath('/translations'), {
maxAge
}));
if (ctx.bootErrors && ctx.bootErrors.length > 0) {
const bootErrorView = require('./lib/server/booterror')(env, ctx);
const bootErrorView = require('./booterror')(env, ctx);
bootErrorView.setLocals(app.locals);
app.get('*', bootErrorView);
return app;
@@ -185,11 +184,11 @@ function create (env, ctx) {
///////////////////////////////////////////////////
// api and json object variables
///////////////////////////////////////////////////
const apiRoot = require('./lib/api/root')(env, ctx);
var api = require('./lib/api/')(env, ctx);
var api3 = require('./lib/api3/')(env, ctx);
var ddata = require('./lib/data/endpoints')(env, ctx);
var notificationsV2 = require('./lib/api/notifications-v2')(app, ctx);
const apiRoot = require('../api/root')(env, ctx);
var api = require('../api/')(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({
filter: function shouldCompress (req, res) {
@@ -242,22 +241,16 @@ function create (env, ctx) {
});
});
const clockviews = require('./lib/server/clocks.js')(env, ctx);
const clockviews = require('./clocks.js')(env, ctx);
clockviews.setLocals(app.locals);
app.use("/clock", clockviews);
app.use('/api', bodyParser({
limit: 1048576 * 50
}), apiRoot);
app.use('/api', apiRoot);
app.use('/api/v1', bodyParser({
limit: 1048576 * 50
}), api);
app.use('/api/v1', api);
app.use('/api/v2', bodyParser({
limit: 1048576 * 50
}), api);
app.use('/api/v2', api);
app.use('/api/v2/properties', ctx.properties);
app.use('/api/v2/authorization', ctx.authorization.endpoints);
@@ -269,14 +262,19 @@ function create (env, ctx) {
// pebble data
app.get('/pebble', ctx.pebble);
const swaggerjson = fs.readFileSync(resolvePath(__dirname + '/swagger.json'), { encoding: 'utf-8' });
const swaggeryaml = fs.readFileSync(resolvePath(__dirname + '/swagger.yaml'), { encoding: 'utf-8' });
// expose swagger.json
app.get('/swagger.json', function(req, res) {
res.sendFile(__dirname + '/swagger.json');
res.setHeader("Content-Type", 'application/json');
res.send(swaggerjson);
});
// expose swagger.yaml
app.get('/swagger.yaml', function(req, res) {
res.sendFile(__dirname + '/swagger.yaml');
res.setHeader("Content-Type", 'text/vnd.yaml');
res.send(swaggeryaml);
});
// API docs
@@ -284,7 +282,7 @@ function create (env, ctx) {
const swaggerUi = require('swagger-ui-express');
const swaggerUseSchema = schema => (...args) => swaggerUi.setup(schema)(...args);
const swaggerDocument = require('./swagger.json');
const swaggerDocumentApiV3 = require('./lib/api3/swagger.json');
const swaggerDocumentApiV3 = require('../api3/swagger.json');
app.use('/api-docs', swaggerUi.serve, swaggerUseSchema(swaggerDocument));
app.use('/api3-docs', swaggerUi.serve, swaggerUseSchema(swaggerDocumentApiV3));
@@ -297,7 +295,6 @@ function create (env, ctx) {
// if production, rely on postinstall script to run packaging for us
app.locals.bundle = '/bundle';
app.locals.mode = 'production';
if (process.env.NODE_ENV === 'development') {
@@ -308,7 +305,7 @@ function create (env, ctx) {
app.locals.bundle = '/devbundle';
const webpack = require('webpack');
var webpack_conf = require('./webpack.config');
var webpack_conf = require('../../webpack.config');
const middleware = require('webpack-dev-middleware');
const compiler = webpack(webpack_conf);
@@ -316,7 +313,6 @@ function create (env, ctx) {
middleware(compiler, {
// webpack-dev-middleware options
publicPath: webpack_conf.output.publicPath
, lazy: false
})
);
@@ -326,16 +322,9 @@ function create (env, ctx) {
}
// Production bundling
var tmpFiles;
if (fs.existsSync(process.cwd() + '/tmp/cacheBusterToken')) {
tmpFiles = express.static('tmp', {
maxAge: maxAge
});
} else {
tmpFiles = express.static(__dirname + '/tmp', {
maxAge: maxAge
});
}
const tmpFiles = express.static(resolvePath('/tmp/public'), {
maxAge: maxAge
});
// serve the static content
app.use('/bundle', tmpFiles);
@@ -356,7 +345,7 @@ function create (env, ctx) {
, coffee_match: /coffeescript/
, json_match: /json/
, cssmin: myCssmin
, cache: __dirname + '/tmp'
, cache: resolvePath('/tmp/public')
, onerror: undefined
, }));
+22 -17
View File
@@ -1,8 +1,7 @@
'use strict';
var _ = require('lodash');
var UPDATE_THROTTLE = 5000;
const _ = require('lodash');
const UPDATE_THROTTLE = 5000;
function boot (env, language) {
@@ -11,23 +10,20 @@ function boot (env, language) {
console.log('Executing startBoot');
ctx.runtimeState = 'booting';
ctx.bus = require('../bus')(env.settings, ctx);
ctx.adminnotifies = require('../adminnotifies')(ctx);
if (env.notifies) {
for (var i = 0; i < env.notifies.length; i++) {
ctx.adminnotifies.addNotify(env.notifies[i]);
}
}
next();
}
//////////////////////////////////////////////////
// Check Node version.
// Latest Node 8 LTS and Latest Node 10 LTS are recommended and supported.
// Latest Node version on Azure is tolerated, but not recommended
// Latest Node (non LTS) version works, but is not recommended
// Latest Node 10 to 14 LTS are recommended and supported.
// Older Node versions or Node versions with known security issues will not work.
// More explicit:
// < 8 does not work, not supported
// >= 8.15.1 works, supported and recommended
// == 9.x does not work, not supported
// == 10.15.2 works, not fully supported and not recommended (Azure version)
// >= 10.16.0 works, supported and recommended
// == 11.x does not work, not supported
// >= 12.6.0 does work, not recommended, will not be supported. We only support Node LTS releases
///////////////////////////////////////////////////
function checkNodeVersion (ctx, next) {
@@ -56,7 +52,7 @@ function boot (env, language) {
console.log('Executing checkEnv');
ctx.language = language;
if (env.err) {
if (env.err.length > 0) {
ctx.bootErrors = ctx.bootErrors || [ ];
ctx.bootErrors.push({'desc': 'ENV Error', err: env.err});
}
@@ -116,11 +112,20 @@ function boot (env, language) {
err: 'MONGODB_URI setting is missing, cannot connect to database'});
}
if (!env.api_secret) {
if (!env.enclave.isApiKeySet()) {
ctx.bootErrors.push({'desc': 'Mandatory setting missing',
err: 'API_SECRET setting is missing, cannot enable REST API'});
}
if (env.settings.authDefaultRoles == 'readable') {
const message = {
title: "Nightscout readable by world"
,message: "Your Nightscout installation is readable by anyone who knows the web page URL. Please consider closing access to the site by following the instructions in the <a href=\"http://nightscout.github.io/nightscout/admin_tools/#turn-off-unauthorized-access\" target=\"_new\">Nightscout documentation</a>."
,persistent: true
};
ctx.adminnotifies.addNotify(message);
}
next();
}
@@ -215,11 +220,11 @@ function boot (env, language) {
ctx.food = require('./food')(env, ctx);
ctx.pebble = require('./pebble')(env, ctx);
ctx.properties = require('../api/properties')(env, ctx);
ctx.bus = require('../bus')(env.settings, ctx);
ctx.ddata = require('../data/ddata')();
ctx.cache = require('./cache')(env,ctx);
ctx.dataloader = require('../data/dataloader')(env, ctx);
ctx.notifications = require('../notifications')(env, ctx);
ctx.purifier = require('./purifier')(env,ctx);
if (env.settings.isEnabled('alexa') || env.settings.isEnabled('googlehome')) {
ctx.virtAsstBase = require('../plugins/virtAsstBase')(env, ctx);
+81
View File
@@ -0,0 +1,81 @@
'use strict;'
const path = require('path');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const fs = require('fs');
// this is a class for holding potentially sensitive data in the app
// the class also implement functions to use the data, so the data is not shared outside the class
const init = function init () {
const enclave = {};
const secrets = {};
const apiKey = Symbol('api-secret');
const apiKeySHA1 = Symbol('api-secretSHA1');
const apiKeySHA512 = Symbol('api-secretSHA512');
const jwtKey = Symbol('jwtkey');
let apiKeySet = false;
function readKey (filename) {
let filePath = path.resolve(__dirname + '/../../tmp/' + filename);
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath).toString().trim();
}
console.error('Key file ', filePath, 'not found');
return null;
}
secrets[jwtKey] = readKey('randomString');
function genHash(data, algorihtm) {
const hash = crypto.createHash(algorihtm);
data = hash.update(data, 'utf-8');
return data.digest('hex');
}
enclave.setApiKey = function setApiKey (keyValue) {
if (keyValue.length < 12) return;
apiKeySet = true;
secrets[apiKey] = keyValue;
secrets[apiKeySHA1] = genHash(keyValue,'sha1');
secrets[apiKeySHA512] = genHash(keyValue,'sha512');
}
enclave.isApiKeySet = function isApiKeySet () {
return isApiKeySet;
}
enclave.isApiKey = function isApiKey (keyValue) {
return keyValue == secrets[apiKeySHA1] || keyValue == secrets[apiKeySHA512];
}
enclave.setJWTKey = function setJWTKey (keyValue) {
secrets[jwtKey] = keyValue;
}
enclave.signJWT = function signJWT(token, lifetime) {
const lt = lifetime ? lifetime : '8h';
return jwt.sign(token, secrets[jwtKey], { expiresIn: lt });
}
enclave.verifyJWT = function verifyJWT(tokenString) {
try {
return jwt.verify(tokenString, secrets[jwtKey]);
} catch(err) {
return null;
}
}
enclave.getSubjectHash = function getSubjectHash(id) {
var shasum = crypto.createHash('sha1');
shasum.update(secrets[apiKeySHA1]);
shasum.update(id);
return shasum.digest('hex');
}
return enclave;
}
module.exports = init;
+65 -44
View File
@@ -1,62 +1,68 @@
'use strict';
var _each = require('lodash/each');
var _trim = require('lodash/trim');
var _forIn = require('lodash/forIn');
var _startsWith = require('lodash/startsWith');
var _camelCase = require('lodash/camelCase');
const _each = require('lodash/each');
const _trim = require('lodash/trim');
const _forIn = require('lodash/forIn');
const _startsWith = require('lodash/startsWith');
const _camelCase = require('lodash/camelCase');
const enclave = require('./enclave');
var fs = require('fs');
var crypto = require('crypto');
var consts = require('./lib/constants');
const mongoParser = require('mongo-url-parser');
var env = {
settings: require('./lib/settings')()
const stringEntropy = require('fast-password-entropy')
const fs = require('fs');
const crypto = require('crypto');
const consts = require('../constants');
const env = {
settings: require('../settings')()
};
var shadowEnv;
// Module to constrain all config and environment parsing to one spot.
// See README.md for info about all the supported ENV VARs
function config ( ) {
function config () {
// Assume users will typo whitespaces into keys and values
shadowEnv = {};
Object.keys(process.env).forEach((key, index) => {
shadowEnv[_trim(key)] = _trim(process.env[key]);
shadowEnv[_trim(key)] = _trim(process.env[key]);
});
env.PORT = readENV('PORT', 1337);
env.HOSTNAME = readENV('HOSTNAME', null);
env.IMPORT_CONFIG = readENV('IMPORT_CONFIG', null);
env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/');
env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', '/static');
env.debug = {
minify: readENVTruthy('DEBUG_MINIFY', true)
};
if (env.err) {
delete env.err;
}
env.err = [];
env.notifies = [];
env.enclave = enclave();
setSSL();
setStorage();
setAPISecret();
setVersion();
setStorage();
updateSettings();
return env;
}
function setSSL() {
function setSSL () {
env.SSL_KEY = readENV('SSL_KEY');
env.SSL_CERT = readENV('SSL_CERT');
env.SSL_CA = readENV('SSL_CA');
env.ssl = false;
if (env.SSL_KEY && env.SSL_CERT) {
env.ssl = {
key: fs.readFileSync(env.SSL_KEY), cert: fs.readFileSync(env.SSL_CERT)
key: fs.readFileSync(env.SSL_KEY)
, cert: fs.readFileSync(env.SSL_CERT)
};
if (env.SSL_CA) {
env.ca = fs.readFileSync(env.SSL_CA);
@@ -66,13 +72,13 @@ function setSSL() {
env.insecureUseHttp = readENVTruthy("INSECURE_USE_HTTP", false);
env.secureHstsHeader = readENVTruthy("SECURE_HSTS_HEADER", true);
env.secureHstsHeaderIncludeSubdomains = readENVTruthy("SECURE_HSTS_HEADER_INCLUDESUBDOMAINS", false);
env.secureHstsHeaderPreload= readENVTruthy("SECURE_HSTS_HEADER_PRELOAD", false);
env.secureHstsHeaderPreload = readENVTruthy("SECURE_HSTS_HEADER_PRELOAD", false);
env.secureCsp = readENVTruthy("SECURE_CSP", false);
env.secureCspReportOnly = readENVTruthy("SECURE_CSP_REPORT_ONLY", false);
}
// A little ugly, but we don't want to read the secret into a var
function setAPISecret() {
function setAPISecret () {
var useSecret = (readENV('API_SECRET') && readENV('API_SECRET').length > 0);
//TODO: should we clear API_SECRET from process env?
env.api_secret = null;
@@ -81,22 +87,39 @@ function setAPISecret() {
if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) {
var msg = ['API_SECRET should be at least', consts.MIN_PASSPHRASE_LENGTH, 'characters'].join(' ');
console.error(msg);
env.err = {desc: msg};
env.err.push({ desc: msg });
} else {
var shasum = crypto.createHash('sha1');
shasum.update(readENV('API_SECRET'));
env.api_secret = shasum.digest('hex');
const apiSecret = readENV('API_SECRET');
delete process.env.API_SECRET;
env.enclave.setApiKey(apiSecret);
var testresult = stringEntropy(apiSecret);
console.log('API_SECRET has', testresult, 'bits of entropy');
if (testresult < 60) {
env.notifies.push({ persistent: true, title: 'Security issue', message: 'Weak API_SECRET detected. Please use a mix of small and CAPITAL letters, numbers and non-alphanumeric characters such as !#%&/ to reduce the risk of unauthorized access. The minimum length of the API_SECRET is 12 characters.' });
}
if (env.storageURI) {
const parsedURL = mongoParser(env.storageURI);
if (parsedURL.auth && parsedURL.auth.password == apiSecret) {
env.notifies.push({ persistent: true, title: 'Security issue', message: 'MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.' });
}
}
}
}
}
function setVersion() {
var software = require('./package.json');
function setVersion () {
var software = require('../../package.json');
env.version = software.version;
env.name = software.name;
}
function setStorage() {
function setStorage () {
env.storageURI = readENV('STORAGE_URI') || readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI') || readENV('MONGODB_URI');
env.entries_collection = readENV('ENTRIES_COLLECTION') || readENV('MONGO_COLLECTION', 'entries');
env.authentication_collections_prefix = readENV('MONGO_AUTHENTICATION_COLLECTIONS_PREFIX', 'auth_');
@@ -110,14 +133,14 @@ function setStorage() {
// TODO: clean up a bit
// Some people prefer to use a json configuration file instead.
// This allows a provided json config to override environment variables
var DB = require('./database_configuration.json'),
DB_URL = DB.url ? DB.url : env.storageURI,
DB_COLLECTION = DB.collection ? DB.collection : env.entries_collection;
var DB = require('../../database_configuration.json')
, DB_URL = DB.url ? DB.url : env.storageURI
, DB_COLLECTION = DB.collection ? DB.collection : env.entries_collection;
env.storageURI = DB_URL;
env.entries_collection = DB_COLLECTION;
}
function updateSettings() {
function updateSettings () {
var envNameOverrides = {
UNITS: 'DISPLAY_UNITS'
@@ -141,12 +164,12 @@ function updateSettings() {
}
}
function readENV(varName, defaultValue) {
function readENV (varName, defaultValue) {
//for some reason Azure uses this prefix, maybe there is a good reason
var value = shadowEnv['CUSTOMCONNSTR_' + varName]
|| shadowEnv['CUSTOMCONNSTR_' + varName.toLowerCase()]
|| shadowEnv[varName]
|| shadowEnv[varName.toLowerCase()];
var value = shadowEnv['CUSTOMCONNSTR_' + varName] ||
shadowEnv['CUSTOMCONNSTR_' + varName.toLowerCase()] ||
shadowEnv[varName] ||
shadowEnv[varName.toLowerCase()];
if (varName == 'DISPLAY_UNITS') {
if (value && value.toLowerCase().includes('mmol')) {
@@ -159,11 +182,9 @@ function readENV(varName, defaultValue) {
return value != null ? value : defaultValue;
}
function readENVTruthy(varName, defaultValue) {
function readENVTruthy (varName, defaultValue) {
var value = readENV(varName, defaultValue);
if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; }
else if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; }
else { value=defaultValue }
if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } else if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } else { value = defaultValue }
return value;
}
@@ -173,13 +194,13 @@ function findExtendedSettings (envs) {
extended.devicestatus = {};
extended.devicestatus.advanced = true;
extended.devicestatus.days = 1;
if(shadowEnv['DEVICESTATUS_DAYS'] && shadowEnv['DEVICESTATUS_DAYS'] == '2') extended.devicestatus.days = 1;
if (shadowEnv['DEVICESTATUS_DAYS'] && shadowEnv['DEVICESTATUS_DAYS'] == '2') extended.devicestatus.days = 1;
function normalizeEnv (key) {
return key.toUpperCase().replace('CUSTOMCONNSTR_', '');
}
_each(env.settings.enable, function eachEnable(enable) {
_each(env.settings.enable, function eachEnable (enable) {
if (_trim(enable)) {
_forIn(envs, function eachEnvPair (value, key) {
var env = normalizeEnv(key);
@@ -199,6 +220,6 @@ function findExtendedSettings (envs) {
}
});
return extended;
}
}
module.exports = config;
+36
View File
@@ -0,0 +1,36 @@
'use strict';
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
function init (env, ctx) {
const purifier = {};
function iterate (obj) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (typeof obj[property] == 'object')
iterate(obj[property]);
else
if (isNaN(obj[property])) {
const clean = DOMPurify.sanitize(obj[property]);
if (obj[property] !== clean) {
obj[property] = clean;
}
}
}
}
}
purifier.purifyObject = function purifyObject (obj) {
return iterate(obj);
}
return purifier;
}
module.exports = init;
+3 -3
View File
@@ -28,7 +28,7 @@
const fs = require('fs');
const env = require('./env')( );
const language = require('./lib/language')();
const language = require('../language')();
const translate = language.set(env.settings.language).translate;
language.loadLocalization(fs);
@@ -47,7 +47,7 @@ function create (app) {
return transport.createServer(app);
}
require('./lib/server/bootevent')(env, language).boot(function booted (ctx) {
require('./bootevent')(env, language).boot(function booted (ctx) {
console.log('Boot event processing completed');
@@ -68,7 +68,7 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) {
///////////////////////////////////////////////////
// setup socket io for data and message transmission
///////////////////////////////////////////////////
var websocket = require('./lib/server/websocket')(env, ctx, server);
var websocket = require('./websocket')(env, ctx, server);
ctx.bus.on('data-processed', function() {
websocket.update();
+9 -1
View File
@@ -8,7 +8,7 @@
"info": {
"title": "Nightscout API",
"description": "Own your DData with the Nightscout API",
"version": "14.1.0",
"version": "14.2.0",
"license": {
"name": "AGPL 3",
"url": "https://www.gnu.org/licenses/agpl.txt"
@@ -1192,6 +1192,14 @@
"type": "string",
"description": "The units for the glucose value, mg/dl or mmol."
},
"transmitterId": {
"type": "string",
"description": "The transmitter ID of the transmitter being started."
},
"sensorCode": {
"type": "string",
"description": "The code used to start a Dexcom G6 sensor."
},
"notes": {
"type": "string",
"description": "Description/notes of treatment."
+7 -1
View File
@@ -4,7 +4,7 @@ servers:
info:
title: Nightscout API
description: Own your DData with the Nightscout API
version: 14.1.0
version: 14.2.0
license:
name: AGPL 3
url: 'https://www.gnu.org/licenses/agpl.txt'
@@ -891,6 +891,12 @@ components:
units:
type: string
description: 'The units for the glucose value, mg/dl or mmol.'
transmitterId:
type: string
description: 'The transmitter ID of the transmitter being started.'
sensorCode:
type: string
description: 'The code used to start a Dexcom G6 sensor.'
notes:
type: string
description: Description/notes of treatment.
+44 -11
View File
@@ -37,11 +37,13 @@ function init (env, ctx, server) {
// This is little ugly copy but I was unable to pass testa after making module from status and share with /api/v1/status
function status () {
var versionNum = 0;
var verParse = /(\d+)\.(\d+)\.(\d+)*/.exec(env.version);
const vString = '' + env.version;
const verParse = vString.split('.');
if (verParse) {
versionNum = 10000 * parseInt(verParse[1]) + 100 * parseInt(verParse[2]) + 1 * parseInt(verParse[3]);
versionNum = 10000 * Number(verParse[0]) + 100 * Number(verParse[1]) + 1 * Number(verParse[2]);
}
var apiEnabled = env.api_secret ? true : false;
var apiEnabled = env.enclave.isApiKeySet();
var activeProfile = ctx.ddata.lastProfileFromSwitch;
@@ -84,6 +86,9 @@ function init (env, ctx, server) {
}
function verifyAuthorization (message, ip, callback) {
if (!message) message = {};
ctx.authorization.resolve({ api_secret: message.secret, token: message.token, ip: ip }, function resolved (err, result) {
if (err) {
@@ -252,9 +257,7 @@ function init (env, ctx, server) {
}
var objId = new ObjectID(data._id);
ctx.store.collection(collection).update(
{ '_id': objId },
{ $unset: data.data }
ctx.store.collection(collection).update({ '_id': objId }, { $unset: data.data }
, function(err, results) {
if (!err) {
@@ -270,7 +273,7 @@ function init (env, ctx, server) {
}
});
}
});
});
if (callback) {
callback({ result: 'success' });
@@ -320,7 +323,13 @@ function init (env, ctx, server) {
// try to find exact match
ctx.store.collection(collection).find(query).toArray(function findResult (err, array) {
if (err || array.length > 0) {
if (err) {
console.error(err);
callback([]);
return;
}
if (array.length > 0) {
console.log(LOG_DEDUP + 'Exact match');
if (callback) {
callback([array[0]]);
@@ -363,7 +372,14 @@ function init (env, ctx, server) {
// try to find similiar
ctx.store.collection(collection).find(query_similiar).toArray(function findSimiliarResult (err, array) {
// if found similiar just update date. next time it will match exactly
if (err || array.length > 0) {
if (err) {
console.error(err);
callback([]);
return;
}
if (array.length > 0) {
console.log(LOG_DEDUP + 'Found similiar', array[0]);
array[0].created_at = data.data.created_at;
var objId = new ObjectID(array[0]._id);
@@ -408,14 +424,22 @@ function init (env, ctx, server) {
// try to find exact match
ctx.store.collection(collection).find(queryDev).toArray(function findResult (err, array) {
if (err || array.length > 0) {
if (err) {
console.error(err);
callback([]);
return;
}
if (array.length > 0) {
console.log(LOG_DEDUP + 'Devicestatus exact match');
if (callback) {
callback([array[0]]);
}
return;
}
});
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
if (err != null && err.message) {
console.log('devicestatus insertion error: ', err.message);
@@ -439,7 +463,7 @@ function init (env, ctx, server) {
console.log(data.collection + ' insertion error: ', err.message);
return;
}
ctx.bus.emit('data-update', {
type: data.collection
, op: 'update'
@@ -501,6 +525,15 @@ function init (env, ctx, server) {
socket.on('authorize', function authorize (message, callback) {
const remoteIP = socket.request.connection.remoteAddress;
verifyAuthorization(message, remoteIP, function verified (err, authorization) {
if (err) {
console.log('Websocket authorization failed:', err);
socket.disconnect();
return;
}
socket.emit('connected');
socketAuthorization = authorization;
clientType = message.client;
history = message.history || 48; //default history is 48 hours
+5 -2
View File
@@ -8,6 +8,8 @@ function init () {
var settings = {
units: 'mg/dl'
, timeFormat: 12
, dayStart: 7.0
, dayEnd: 21.0
, nightMode: false
, editMode: true
, showRawbg: 'never'
@@ -50,7 +52,6 @@ function init () {
, deNormalizeDates: false
, showClockDelta: false
, showClockLastTime: false
, bolusRenderOver: 1
, frameUrl1: ''
, frameUrl2: ''
, frameUrl3: ''
@@ -67,6 +68,7 @@ function init () {
, frameName6: ''
, frameName7: ''
, frameName8: ''
, authFailDelay: 5000
};
var secureSettings = [
@@ -102,6 +104,7 @@ function init () {
, bgLow: mapNumber
, bgTargetTop: mapNumber
, bgTargetBottom: mapNumber
, authFailDelay: mapNumber
};
function filterObj(obj, secureKeys) {
@@ -166,7 +169,7 @@ function init () {
}
//TODO: getting sent in status.json, shouldn't be
settings.DEFAULT_FEATURES = ['bgnow', 'delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile', 'dbsize', 'runtimestate', 'basal', 'careportal'];
settings.DEFAULT_FEATURES = ['bgnow', 'delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile', 'bolus', 'dbsize', 'runtimestate', 'basal', 'careportal'];
var wasSet = [];
+2 -5
View File
@@ -24,10 +24,7 @@ function init(env, cb, forceNewConnection) {
}
console.log('Setting up new connection to MongoDB');
const timeout = 10 * 1000;
const options = {
connectTimeoutMS: timeout,
socketTimeoutMS: timeout,
useNewUrlParser: true,
useUnifiedTopology: true,
};
@@ -38,7 +35,7 @@ function init(env, cb, forceNewConnection) {
try {
await mongo.client.connect();
console.log('Successfully established a connected to MongoDB');
console.log('Successfully established connection to MongoDB');
const dbName = mongo.client.s.options.dbName;
mongo.db = mongo.client.db(dbName);
@@ -69,7 +66,7 @@ function init(env, cb, forceNewConnection) {
setTimeout(connect_with_retry, timeout, i + 1);
if (i == 1) cb(new Error('MongoDB connection failed! Double check the MONGODB_URI setting in Heroku.'), null);
} else {
cb(new Error('MONGODB_URI ' + env.storageURI + ' seems invalid: ' + err.message));
cb(new Error('MONGODB_URI seems invalid: ' + err.message));
}
}
};
+7 -3
View File
@@ -34,12 +34,16 @@ function init(ctx) {
}
};
utils.toFixedMin = function toFixedMin(value,digits) {
/**
* Round the number to maxDigits places, return a string
* that truncates trailing zeros
*/
utils.toRoundedStr = function toRoundedStr (value, maxDigits) {
if (!value) {
return '0';
}
var mult = Math.pow(10,digits);
var fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult;
const mult = Math.pow(10, maxDigits);
const fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult;
if (isNaN(fixed)) return '0';
return String(fixed);
};
+2 -1
View File
@@ -11,4 +11,5 @@ ALARM_TYPES="predict"
LANGUAGE=en
INSECURE_USE_HTTP=true
PORT=1337
NODE_ENV=development
NODE_ENV=development
AUTH_FAIL_DELAY=50
+3294 -2306
View File
File diff suppressed because it is too large Load Diff
+44 -26
View File
@@ -1,6 +1,6 @@
{
"name": "nightscout",
"version": "14.1.0",
"version": "14.2.0",
"description": "Nightscout acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime.",
"license": "AGPL-3.0",
"author": "Nightscout Team",
@@ -26,22 +26,30 @@
"url": "https://github.com/nightscout/cgm-remote-monitor/issues"
},
"scripts": {
"start": "node server.js",
"test": "env-cmd -f ./my.test.env mocha --exit tests/*.test.js",
"test-single": "env-cmd -f ./my.test.env mocha --exit tests/$TEST.test.js",
"test-ci": "env-cmd -f ./ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --exit tests/*.test.js",
"start": "node lib/server/server.js",
"test": "env-cmd -f ./my.test.env mocha --require ./tests/hooks.js -exit ./tests/*.test.js",
"test-single": "env-cmd -f ./my.test.env mocha --require ./tests/hooks.js --exit ./tests/$TEST.test.js",
"test-ci": "env-cmd -f ./ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --require ./tests/hooks.js --exit ./tests/*.test.js",
"env": "env",
"postinstall": "webpack --mode production --config webpack.config.js && npm run-script update-buster",
"bundle": "webpack --mode production --config webpack.config.js && npm run-script update-buster",
"bundle-dev": "webpack --mode development --config webpack.config.js && npm run-script update-buster",
"postinstall": "webpack --mode production --config webpack.config.js && npm run-script generate-keys",
"bundle": "webpack --mode production --config webpack.config.js && npm run-script generate-keys",
"bundle-dev": "webpack --mode development --config webpack.config.js && npm run-script generate-keys",
"bundle-analyzer": "webpack --mode development --config webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json",
"update-buster": "node bin/generateCacheBuster.js >tmp/cacheBusterToken",
"generate-keys": "node bin/generateRandomString.js >tmp/randomString",
"coverage": "cat ./coverage/lcov.info | env-cmd -f ./ci.test.env codacy-coverage",
"dev": "env-cmd -f ./my.env nodemon server.js 0.0.0.0",
"prod": "env-cmd -f ./my.prod.env node 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",
"prod": "env-cmd -f ./my.prod.env node lib/server/server.js 0.0.0.0",
"lint": "eslint lib"
},
"main": "server.js",
"main": "lib/server/server.js",
"nodemonConfig": {
"ignore": [
"tests/*",
"node_modules/*",
"bin/*"
]
},
"config": {
"blanket": {
"pattern": [
@@ -62,7 +70,9 @@
},
"dependencies": {
"@babel/core": "^7.11.1",
"@babel/preset-env": "^7.11.0",
"@babel/preset-env": "^7.12.11",
"acorn": "^8.0.5",
"acorn-jsx": "^5.3.1",
"apn": "^2.2.0",
"async": "^0.9.2",
"babel-loader": "^8.1.0",
@@ -70,26 +80,30 @@
"body-parser": "^1.19.0",
"bootevent": "0.0.1",
"braces": "^3.0.2",
"buffer": "^6.0.3",
"compression": "^1.7.4",
"css-loader": "^1.0.1",
"crypto-browserify": "^3.12.0",
"css-loader": "^5.0.1",
"cssmin": "^0.4.3",
"csv-stringify": "^5.5.1",
"d3": "^5.16.0",
"dompurify": "^2.2.6",
"easyxml": "^2.0.1",
"ejs": "^2.7.4",
"errorhandler": "^1.5.1",
"event-stream": "3.3.4",
"expose-loader": "^0.7.5",
"expose-loader": "^2.0.0",
"express": "^4.17.1",
"express-minify": "^1.0.0",
"file-loader": "^3.0.1",
"fast-password-entropy": "^1.1.1",
"file-loader": "^6.2.0",
"flot": "^0.8.3",
"helmet": "^4.0.0",
"jquery": "^3.5.1",
"jquery-ui-bundle": "^1.12.1-migrate",
"jquery.tooltips": "^1.0.0",
"js-storage": "^1.1.0",
"jsdom": "~11.11.0",
"jsdom": "^11.11.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.20",
"memory-cache": "^0.2.0",
@@ -99,11 +113,13 @@
"moment-locales-webpack-plugin": "^1.2.0",
"moment-timezone": "^0.5.31",
"moment-timezone-data-webpack-plugin": "^1.3.0",
"mongo-url-parser": "^1.0.1",
"mongodb": "^3.6.0",
"mongomock": "^0.1.2",
"node-cache": "^4.2.1",
"parse-duration": "^0.1.3",
"pem": "^1.14.4",
"process": "^0.11.10",
"pushover-notifications": "^1.2.2",
"random-token": "0.0.8",
"request": "^2.88.2",
@@ -112,33 +128,35 @@
"shiro-trie": "^0.4.9",
"simple-statistics": "^0.7.0",
"socket.io": "~2.1.1",
"stream-browserify": "^3.0.0",
"style-loader": "^0.23.1",
"swagger-ui-dist": "^3.32.1",
"swagger-ui-express": "^4.1.4",
"terser": "^3.17.0",
"traverse": "^0.6.6",
"uuid": "^3.4.0",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
"webpack": "^5.20.2",
"webpack-cli": "^4.5.0"
},
"devDependencies": {
"@types/tough-cookie": "^4.0.0",
"axios": "^0.21.1",
"babel-eslint": "^10.1.0",
"benv": "^3.3.0",
"codacy-coverage": "^3.4.0",
"csv-parse": "^4.12.0",
"env-cmd": "^10.1.0",
"eslint": "^6.8.0",
"eslint-loader": "^2.2.1",
"eslint": "^7.19.0",
"eslint-plugin-security": "^1.4.0",
"eslint-webpack-plugin": "^2.4.3",
"mocha": "^8.1.1",
"nodemon": "^1.19.4",
"nyc": "^14.1.1",
"should": "^13.2.3",
"supertest": "^3.4.2",
"terser-webpack-plugin": "^1.4.5",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-dev-middleware": "^3.7.2",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-dev-middleware": "^4.1.0",
"webpack-hot-middleware": "^2.25.0",
"xml2js": "^0.4.23"
},
"browserslist": "> 0.25%, not dead"
"browserslist": "> 0.25%, not dead, ios_saf 10"
}
+15 -2
View File
@@ -49,7 +49,7 @@ input[type=number]:invalid {
text-decoration: underline;
}
#treatmentDrawer {
#treatmentDrawer, #adminNotifiesDrawer {
background-color: #666;
border-left: 1px solid #999;
box-shadow: inset 4px 4px 5px 0 rgba(50, 50, 50, 0.75);
@@ -66,6 +66,19 @@ input[type=number]:invalid {
z-index: 1;
}
#adminNotifyContent {
margin: 10px;
}
.adminNotifyMessage {
margin-left: 10px;
}
.adminNotifyMessageAdditionalInfo {
margin-left: 10px;
font-size: 11px;
}
#treatmentDrawer input {
box-sizing: border-box;
}
@@ -214,7 +227,7 @@ h1, legend,
padding: 0 15px 0 40px;
position: relative;
align-items: center;
background: url(/images/logo2.png) no-repeat 3px center #333;
background: url("../images/logo2.png") no-repeat 3px center #333;
border-bottom: 1px solid #999;
justify-content: space-between;
}
+5 -4
View File
@@ -9,15 +9,15 @@
font-style: normal;
}
/*
/*
Icon font for additional plugin icons.
Please read assets/fonts/README.md about update process
*/
@font-face {
font-family: 'pluginicons';
/* Plugin Icons font files content (from WOFF and SVG icon files, base64 encoded) */
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAWAAAsAAAAABTQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDxIE8mNtYXAAAAFoAAAAVAAAAFQXVdKJZ2FzcAAAAbwAAAAIAAAACAAAABBnbHlmAAABxAAAAUgAAAFIFA4eR2hlYWQAAAMMAAAANgAAADYXVLrVaGhlYQAAA0QAAAAkAAAAJAdQA8ZobXR4AAADaAAAABQAAAAUCY4AAGxvY2EAAAN8AAAADAAAAAwAKAC4bWF4cAAAA4gAAAAgAAAAIAAJAFxuYW1lAAADqAAAAbYAAAG2DBt7mXBvc3QAAAVgAAAAIAAAACAAAwAAAAMCxwGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA6QEDwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADgAAAAKAAgAAgACAAEAIOkB//3//wAAAAAAIOkB//3//wAB/+MXAwADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAwAA/8ADjgPAABsAOgBZAAABIgcOAQcGFRQXHgEXFjMyNz4BNzY1NCcuAScmARUUFx4BFxYzMjc+ATc2PQEUBw4BBwYjIicuAScmNREVFBceARcWMzI3PgE3Nj0BFAcOAQcGIyInLgEnJjUBx15TU3skJCQke1NTXl5TU3wjJCQjfFNT/dskJHtTU15eU1N8IyQkI3xTU15eU1N7JCQkJHtTU15eU1N8IyQkI3xTU15eU1N7JCQDwBISPikpMC8pKj0SEhISPSopLzApKT4SEv6rqy8qKT4SEhISPikqL6svKik+EhISEj4pKi/+46owKSk+EhISEj4pKTCqLykqPhESEhE+KikvAAAAAAEAAAABAABgRbaTXw889QALBAAAAAAA2lO7LAAAAADaU7ssAAD/wAOOA8AAAAAIAAIAAAAAAAAAAQAAA8D/wAAABAAAAAAAA44AAQAAAAAAAAAAAAAAAAAAAAUEAAAAAAAAAAAAAAACAAAAA44AAAAAAAAACgAUAB4ApAABAAAABQBaAAMAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEACwAAAAEAAAAAAAIABwCEAAEAAAAAAAMACwBCAAEAAAAAAAQACwCZAAEAAAAAAAUACwAhAAEAAAAAAAYACwBjAAEAAAAAAAoAGgC6AAMAAQQJAAEAFgALAAMAAQQJAAIADgCLAAMAAQQJAAMAFgBNAAMAAQQJAAQAFgCkAAMAAQQJAAUAFgAsAAMAAQQJAAYAFgBuAAMAAQQJAAoANADUcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzVmVyc2lvbiAxLjAAVgBlAHIAcwBpAG8AbgAgADEALgAwcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzUmVndWxhcgBSAGUAZwB1AGwAYQBycGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzRm9udCBnZW5lcmF0ZWQgYnkgSWNvTW9vbi4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('woff'),
url(data:application/font-svg;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIiA+DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+DQo8bWV0YWRhdGE+R2VuZXJhdGVkIGJ5IEljb01vb248L21ldGFkYXRhPg0KPGRlZnM+DQo8Zm9udCBpZD0icGx1Z2luaWNvbnMiIGhvcml6LWFkdi14PSIxMDI0Ij4NCjxmb250LWZhY2UgdW5pdHMtcGVyLWVtPSIxMDI0IiBhc2NlbnQ9Ijk2MCIgZGVzY2VudD0iLTY0IiAvPg0KPG1pc3NpbmctZ2x5cGggaG9yaXotYWR2LXg9IjEwMjQiIC8+DQo8Z2x5cGggdW5pY29kZT0iJiN4MjA7IiBob3Jpei1hZHYteD0iNTEyIiBkPSIiIC8+DQo8Z2x5cGggdW5pY29kZT0iJiN4ZTkwMTsiIGdseXBoLW5hbWU9ImRhdGFiYXNlIiBob3Jpei1hZHYteD0iOTEwIiBkPSJNNDU1LjExMSA5NjBjLTI1MS40NDkgMC00NTUuMTExLTEwMS44MzEtNDU1LjExMS0yMjcuNTU2czIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTYgNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1Ni0yMDMuNjYyIDIyNy41NTYtNDU1LjExMSAyMjcuNTU2ek0wIDYxOC42Njd2LTE3MC42NjdjMC0xMjUuNzI0IDIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTZzNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1NnYxNzAuNjY3YzAtMTI1LjcyNC0yMDMuNjYyLTIyNy41NTYtNDU1LjExMS0yMjcuNTU2cy00NTUuMTExIDEwMS44MzEtNDU1LjExMSAyMjcuNTU2ek0wIDMzNC4yMjJ2LTE3MC42NjdjMC0xMjUuNzI0IDIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTZzNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1NnYxNzAuNjY3YzAtMTI1LjcyNC0yMDMuNjYyLTIyNy41NTYtNDU1LjExMS0yMjcuNTU2cy00NTUuMTExIDEwMS44MzEtNDU1LjExMSAyMjcuNTU2eiIgLz4NCjwvZm9udD48L2RlZnM+PC9zdmc+) format('svg');
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAcEAAsAAAAABrgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDxIFc2NtYXAAAAFoAAAAXAAAAFzpVumzZ2FzcAAAAcQAAAAIAAAACAAAABBnbHlmAAABzAAAAuwAAALs3l4nFmhlYWQAAAS4AAAANgAAADYbA9uPaGhlYQAABPAAAAAkAAAAJAfCA8dobXR4AAAFFAAAABgAAAAYDY4AAGxvY2EAAAUsAAAADgAAAA4BngC4bWF4cAAABTwAAAAgAAAAIAAMAJRuYW1lAAAFXAAAAYYAAAGGmUoJ+3Bvc3QAAAbkAAAAIAAAACAAAwAAAAMDLwGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA6RoDwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEAEAAAAAMAAgAAgAEAAEAIOkB6Rr//f//AAAAAAAg6QHpGv/9//8AAf/jFwMW6wADAAEAAAAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAADAAD/wAOOA8AAGwA6AFkAAAEiBw4BBwYVFBceARcWMzI3PgE3NjU0Jy4BJyYBFRQXHgEXFjMyNz4BNzY9ARQHDgEHBiMiJy4BJyY1ERUUFx4BFxYzMjc+ATc2PQEUBw4BBwYjIicuAScmNQHHXlNTeyQkJCR7U1NeXlNTfCMkJCN8U1P92yQke1NTXl5TU3wjJCQjfFNTXl5TU3skJCQke1NTXl5TU3wjJCQjfFNTXl5TU3skJAPAEhI+KSkwLykqPRISEhI9KikvMCkpPhIS/qurLyopPhISEhI+KSovqy8qKT4SEhISPikqL/7jqjApKT4SEhISPikpMKovKSo+ERISET4qKS8AAAAABQAAAAIEAAOAACoATgBjAG0AkQAAATQnLgEnJic4ATEjMAcOAQcGBw4BFRQWFxYXHgEXFjEzMDQxMjc+ATc2NQMiJicuAScuATU0Njc+ATc+ATMyFhceARceARUUBgcOAQcOAQE0NjcOASMqATEHFRcwMjMyFhcuARcnEx4BPwE+AScBIiYnLgEnLgE1NDY3PgE3PgEzMhYXHgEXHgEVFAYHDgEHDgEEAAoLIxgYG1MiI35XWGkGCAgGaVhXfiMiUxsYGCMLCp8HDgQJEggSEhISCBIJBA4HBw4ECRIIERMTEQgSCQQO/ZQFBiRCJjMRNzcRMyZCJAYFdIBSAxYMdgwJBwF2AwUCAwcDBwcHBwMHAwIFAwMFAQQHAwcHBwcDBwQBBQITS0JDYx0cARgYQSMiFiJRLi9RIhUjIkIYGAEdHWNCQkz+ygsECyAVLndCQncuFCEKBQsLBQohFC53QkJ3LhUgCwQLATYnSyMFBV9YXwUFI0uuGP6/DQsFMAQXDAFCBQEEDQgRLhoZLhIIDAQCBAQCBAwIEi4ZGi4RCA0EAQUAAQAAAAAAAPoSCcNfDzz1AAsEAAAAAADb8suJAAAAANvyy4kAAP/ABAADwAAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAAEAAABAAAAAAAAAAAAAAAAAAAABgQAAAAAAAAAAAAAAAIAAAADjgAABAAAAAAAAAAACgAUAB4ApAF2AAAAAQAAAAYAkgAFAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGljb21vb24AaQBjAG8AbQBvAG8AblZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGljb21vb24AaQBjAG8AbQBvAG8Abmljb21vb24AaQBjAG8AbQBvAG8AblJlZ3VsYXIAUgBlAGcAdQBsAGEAcmljb21vb24AaQBjAG8AbQBvAG8AbkZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('woff'),
url(data:application/font-svg;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiID4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8bWV0YWRhdGE+R2VuZXJhdGVkIGJ5IEljb01vb248L21ldGFkYXRhPgo8ZGVmcz4KPGZvbnQgaWQ9Imljb21vb24iIGhvcml6LWFkdi14PSIxMDI0Ij4KPGZvbnQtZmFjZSB1bml0cy1wZXItZW09IjEwMjQiIGFzY2VudD0iOTYwIiBkZXNjZW50PSItNjQiIC8+CjxtaXNzaW5nLWdseXBoIGhvcml6LWFkdi14PSIxMDI0IiAvPgo8Z2x5cGggdW5pY29kZT0iJiN4MjA7IiBob3Jpei1hZHYteD0iNTEyIiBkPSIiIC8+CjxnbHlwaCB1bmljb2RlPSImI3hlOTAxOyIgZ2x5cGgtbmFtZT0iZGF0YWJhc2UiIGhvcml6LWFkdi14PSI5MTAiIGQ9Ik00NTUuMTExIDk2MGMtMjUxLjQ0OSAwLTQ1NS4xMTEtMTAxLjgzMS00NTUuMTExLTIyNy41NTZzMjAzLjY2Mi0yMjcuNTU2IDQ1NS4xMTEtMjI3LjU1NiA0NTUuMTExIDEwMS44MzEgNDU1LjExMSAyMjcuNTU2LTIwMy42NjIgMjI3LjU1Ni00NTUuMTExIDIyNy41NTZ6TTAgNjE4LjY2N3YtMTcwLjY2N2MwLTEyNS43MjQgMjAzLjY2Mi0yMjcuNTU2IDQ1NS4xMTEtMjI3LjU1NnM0NTUuMTExIDEwMS44MzEgNDU1LjExMSAyMjcuNTU2djE3MC42NjdjMC0xMjUuNzI0LTIwMy42NjItMjI3LjU1Ni00NTUuMTExLTIyNy41NTZzLTQ1NS4xMTEgMTAxLjgzMS00NTUuMTExIDIyNy41NTZ6TTAgMzM0LjIyMnYtMTcwLjY2N2MwLTEyNS43MjQgMjAzLjY2Mi0yMjcuNTU2IDQ1NS4xMTEtMjI3LjU1NnM0NTUuMTExIDEwMS44MzEgNDU1LjExMSAyMjcuNTU2djE3MC42NjdjMC0xMjUuNzI0LTIwMy42NjItMjI3LjU1Ni00NTUuMTExLTIyNy41NTZzLTQ1NS4xMTEgMTAxLjgzMS00NTUuMTExIDIyNy41NTZ6IiAvPgo8Z2x5cGggdW5pY29kZT0iJiN4ZTkxYTsiIGdseXBoLW5hbWU9Im5vdGlmaWVzIiBkPSJNMTAyNCA1MzAuNzQ0YzAgMjAwLjkyNi01OC43OTIgMzYzLjkzOC0xMzEuNDgyIDM2NS4yMjYgMC4yOTIgMC4wMDYgMC41NzggMC4wMzAgMC44NzIgMC4wMzBoLTgyLjk0MmMwIDAtMTk0LjgtMTQ2LjMzNi00NzUuMjMtMjAzLjc1NC04LjU2LTQ1LjI5Mi0xNC4wMzAtOTkuMjc0LTE0LjAzMC0xNjEuNTAyczUuNDY2LTExNi4yMDggMTQuMDMwLTE2MS41YzI4MC40MjgtNTcuNDE4IDQ3NS4yMy0yMDMuNzU2IDQ3NS4yMy0yMDMuNzU2aDgyLjk0MmMtMC4yOTIgMC0wLjU3OCAwLjAyNC0wLjg3MiAwLjAzMiA3Mi42OTYgMS4yODggMTMxLjQ4MiAxNjQuMjk4IDEzMS40ODIgMzY1LjIyNHpNODY0LjgyNCAyMjAuNzQ4Yy05LjM4MiAwLTE5LjUzMiA5Ljc0Mi0yNC43NDYgMTUuNTQ4LTEyLjYzIDE0LjA2NC0yNC43OTIgMzUuOTYtMzUuMTg4IDYzLjMyOC0yMy4yNTYgNjEuMjMyLTM2LjA2NiAxNDMuMzEtMzYuMDY2IDIzMS4xMjQgMCA4Ny44MSAxMi44MSAxNjkuODkgMzYuMDY2IDIzMS4xMjIgMTAuMzk0IDI3LjM2OCAyMi41NjIgNDkuMjY2IDM1LjE4OCA2My4zMjggNS4yMTQgNS44MTIgMTUuMzY0IDE1LjU1MiAyNC43NDYgMTUuNTUyIDkuMzggMCAxOS41MzYtOS43NDQgMjQuNzQ0LTE1LjU1MiAxMi42MzQtMTQuMDY0IDI0Ljc5Ni0zNS45NTggMzUuMTg4LTYzLjMyOCAyMy4yNTgtNjEuMjMgMzYuMDY4LTE0My4zMTIgMzYuMDY4LTIzMS4xMjIgMC04Ny44MDQtMTIuODEtMTY5Ljg4OC0zNi4wNjgtMjMxLjEyNC0xMC4zOS0yNy4zNjgtMjIuNTYyLTQ5LjI2NC0zNS4xODgtNjMuMzI4LTUuMjA4LTUuODA2LTE1LjM2LTE1LjU0OC0yNC43NDQtMTUuNTQ4ek0yNTEuODEyIDUzMC43NDRjMCA1MS45NSAzLjgxIDEwMi40MyAxMS4wNTIgMTQ5LjA5NC00Ny4zNzItNi41NTQtODguOTQyLTEwLjMyNC0xNDAuMzQtMTAuMzI0LTY3LjA1OCAwLTY3LjA1OCAwLTY3LjA1OCAwbC01NS40NjYtOTQuNjg2di04OC4xN2w1NS40Ni05NC42ODZjMCAwIDAgMCA2Ny4wNjAgMCA1MS4zOTggMCA5Mi45NjgtMy43NzQgMTQwLjM0LTEwLjMyNC03LjIzNiA0Ni42NjQtMTEuMDQ4IDk3LjE0Ni0xMS4wNDggMTQ5LjA5NnpNMzY4LjE1IDMxNy44MjhsLTEyNy45OTggMjQuNTEgODEuODQyLTMyMS41NDRjNC4yMzYtMTYuNjM0IDIwLjc0NC0yNS4wMzggMzYuNjg2LTE4LjY1NGwxMTguNTU2IDQ3LjQ1MmMxNS45NDQgNi4zNzYgMjIuMzI4IDIzLjk2NCAxNC4xOTYgMzkuMDg0bC0xMjMuMjgyIDIyOS4xNTJ6TTg2NC44MjQgNDExLjI3Yy0zLjYxOCAwLTcuNTI4IDMuNzU0LTkuNTM4IDUuOTkyLTQuODcgNS40Mi05LjU1NiAxMy44Ni0xMy41NjIgMjQuNDA4LTguOTYyIDIzLjYtMTMuOSA1NS4yMzQtMTMuOSA4OS4wNzhzNC45MzggNjUuNDc4IDEzLjkgODkuMDc4YzQuMDA2IDEwLjU0OCA4LjY5NiAxOC45ODggMTMuNTYyIDI0LjQwOCAyLjAxMCAyLjI0IDUuOTIgNS45OTQgOS41MzggNS45OTQgMy42MTYgMCA3LjUzLTMuNzU2IDkuNTM4LTUuOTk0IDQuODctNS40MiA5LjU1Ni0xMy44NTggMTMuNTYtMjQuNDA4IDguOTY0LTIzLjU5OCAxMy45MDItNTUuMjM0IDEzLjkwMi04OS4wNzggMC0zMy44NDItNC45MzgtNjUuNDc4LTEzLjkwMi04OS4wNzgtNC4wMDQtMTAuNTQ4LTguNjk2LTE4Ljk4OC0xMy41Ni0yNC40MDgtMi4wMDgtMi4yMzgtNS45Mi01Ljk5Mi05LjUzOC01Ljk5MnoiIC8+CjwvZm9udD48L2RlZnM+PC9zdmc+) format('svg');
font-weight: normal;
font-style: normal;
}
@@ -61,7 +61,7 @@
[class^="plugicon-"]:before, [class*=" plugicon-"]:before {
font-family: "pluginicons";
}
.icon-volume:before { content: '\e800'; }
.icon-plus:before { content: '\e801'; }
.icon-edit:before { content: '\e802'; }
@@ -85,6 +85,7 @@
/* Plugin Icons id-s (copy from generated icon style.css) */
.plugicon-database:before { content: "\e901"; }
.plugicon-notifies:before { content: "\e91a"; }
html, body {
margin: 0;
+6
View File
@@ -0,0 +1,6 @@
'use strict';
$(document).ready(function() {
console.log('Application got ready event');
window.Nightscout.foodclient();
});
+6
View File
@@ -0,0 +1,6 @@
'use strict';
$(document).ready(function() {
console.log('Application got ready event');
window.Nightscout.profileclient();
});
-4
View File
@@ -1,4 +0,0 @@
/* Modernizr 2.8.3 (Custom Build) | MIT & BSD
* Build: http://modernizr.com/download/#-touch-teststyles-prefixes
*/
;window.Modernizr=function(a,b,c){function v(a){i.cssText=a}function w(a,b){return v(l.join(a+";")+(b||""))}function x(a,b){return typeof a===b}function y(a,b){return!!~(""+a).indexOf(b)}function z(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:x(f,"function")?f.bind(d||b):f}return!1}var d="2.8.3",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l=" -webkit- -moz- -o- -ms- ".split(" "),m={},n={},o={},p=[],q=p.slice,r,s=function(a,c,d,e){var h,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:g+(d+1),l.appendChild(j);return h=["&#173;",'<style id="s',g,'">',a,"</style>"].join(""),l.id=g,(m?l:n).innerHTML+=h,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=f.style.overflow,f.style.overflow="hidden",f.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),f.style.overflow=k),!!i},t={}.hasOwnProperty,u;!x(t,"undefined")&&!x(t.call,"undefined")?u=function(a,b){return t.call(a,b)}:u=function(a,b){return b in a&&x(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:s(["@media (",l.join("touch-enabled),("),g,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c};for(var A in m)u(m,A)&&(r=A.toLowerCase(),e[r]=m[A](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)u(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},v(""),h=j=null,e._version=d,e._prefixes=l,e.testStyles=s,e}(this,this.document);
-671
View File
@@ -1,671 +0,0 @@
/**
* CONTENTS
*
* #Introduction........Naming conventions used throughout the code.
*
* #SETTINGS
* Variables............Globally-available variables and config.
*
* #TOOLS
* Mixins...............Useful mixins.
*
* #GENERIC
* Demo styles..........Styles for demo only (consider removing these).
*
* #BASE
* Raw styles...........The very basic component wrapper.
* Modifiers............The basic styles dependant on component placement.
* Debuggers............The basic styles dependant on component placement.
*
* #BUTTONS
* Base..................Wrapping and constraining every button.
* Modifiers.............Styles that depends on state and settings.
* Animations............Main animations of the component.
* Debuggers.............Styles for development.
*
* #LABELS
* Base..................Wrapping and constraining every label.
* Modifiers.............Styles that depends on state and settings.
* Debuggers.............Styles for development.
*
* #DEVELOPMENT
* In development........These styles are in development and not yet finalised
* Debuggers.............Helper styles and flags for development.
*/
/*------------------------------------*\
#Introduction
\*------------------------------------*/
/**
* The code AND the comments use naming conventions to refer to each part of
* the UI put in place by this component. If you see that somewhere they are
* not followed please consider a Pull Request. The naming conventions are:
*
* "Component" : the widget itself as a whole. This is the last time it will be
* called anything different than "component". So, stay away from
* "widget", "button" or anything else when referring to the
* Component in general.
*
* "Main Button" : the button that is always in view. Hovering or clicking on it
* will reveal the child buttons.
*
* "Child buttons" : if you've read the previous point you know what they are.
* Did you read the previous point? :)
*
* "Label(s)" : the tooltip that fades in when hovering over a button.
/*------------------------------------*\
#SETTINGS | Variables
\*------------------------------------*/
/**
* These variables are the default styles that serve as fallback and can be
* easily customised at compile time.
* Consider overriding them in your own style sheets rather than editing them
* here. Refer to the docs for more info.
*/
/* COLORS ----------------------------*/
/* EFFECTS ---------------------------*/
/* SPEEDS ----------------------------*/
/* SIZES -----------------------------*/
/* SPACING ---------------------------*/
/* OTHER VARIABLES -------------------*/
/*------------------------------------*\
#BASE | Raw styles
\*------------------------------------*/
/**
* The very core styling of the button.
* These styles are shared by every instance of the button.
* Styles placed here should NOT care about placement in the screen,
* options chosen by the user or state of the button.
*/
.mfb-component--tl, .mfb-component--tr, .mfb-component--bl, .mfb-component--br {
box-sizing: border-box;
margin: 25px;
position: fixed;
white-space: nowrap;
z-index: 30;
padding-left: 0;
list-style: none; }
.mfb-component--tl *, .mfb-component--tr *, .mfb-component--bl *, .mfb-component--br *, .mfb-component--tl *:before, .mfb-component--tr *:before, .mfb-component--bl *:before, .mfb-component--br *:before, .mfb-component--tl *:after, .mfb-component--tr *:after, .mfb-component--bl *:after, .mfb-component--br *:after {
box-sizing: inherit; }
/*------------------------------------*\
#BASE | Modifiers
\*------------------------------------*/
/**
* These styles depends on the placement of the button.
* Styles can be:
* 1. Top-left: modified by the " --tl " suffix.
* 2. Top-right: modified by the " --tr " suffix.
* 3. Bottom-left: modified by the " --bl " suffix.
* 4. Bottom-right: modified by the " --br " suffix.
*/
.mfb-component--tl {
left: 0;
top: 0; }
.mfb-component--tr {
right: 0;
top: 0; }
.mfb-component--bl {
left: 0;
bottom: 0; }
.mfb-component--br {
right: 0;
bottom: 0; }
/*------------------------------------*\
#BUTTONS | Base
\*------------------------------------*/
.mfb-component__button--main, .mfb-component__button--child {
background-color: #E40A5D;
display: inline-block;
position: relative;
border: none;
border-radius: 50%;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
cursor: pointer;
outline: none;
padding: 0;
position: relative;
-webkit-user-drag: none;
color: #f1f1f1; }
/**
* This is the unordered list for the list items that contain
* the child buttons.
*
*/
.mfb-component__list {
list-style: none;
margin: 0;
padding: 0; }
.mfb-component__list > li {
display: block;
position: absolute;
top: 0;
right: 1px;
padding: 10px 0;
margin: -10px 0; }
/**
* These are the basic styles for all the icons inside the main button
*/
.mfb-component__icon, .mfb-component__main-icon--active,
.mfb-component__main-icon--resting, .mfb-component__child-icon {
position: absolute;
font-size: 18px;
text-align: center;
line-height: 56px;
width: 100%; }
.mfb-component__wrap {
padding: 25px;
margin: -25px; }
[data-mfb-toggle="hover"]:hover .mfb-component__icon, [data-mfb-toggle="hover"]:hover .mfb-component__main-icon--active,
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon--resting, [data-mfb-toggle="hover"]:hover .mfb-component__child-icon,
[data-mfb-state="open"] .mfb-component__icon,
[data-mfb-state="open"] .mfb-component__main-icon--active,
[data-mfb-state="open"] .mfb-component__main-icon--resting,
[data-mfb-state="open"] .mfb-component__child-icon {
-webkit-transform: scale(1) rotate(0deg);
transform: scale(1) rotate(0deg); }
/*------------------------------------*\
#BUTTONS | Modifiers
\*------------------------------------*/
.mfb-component__button--main {
height: 56px;
width: 56px;
z-index: 20; }
.mfb-component__button--child {
height: 56px;
width: 56px; }
.mfb-component__main-icon--active,
.mfb-component__main-icon--resting {
-webkit-transform: scale(1) rotate(360deg);
transform: scale(1) rotate(360deg);
-webkit-transition: -webkit-transform 150ms cubic-bezier(0.4, 0, 1, 1);
transition: transform 150ms cubic-bezier(0.4, 0, 1, 1); }
.mfb-component__child-icon,
.mfb-component__child-icon {
line-height: 56px;
font-size: 18px; }
.mfb-component__main-icon--active {
opacity: 0; }
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon,
[data-mfb-state="open"] .mfb-component__main-icon {
-webkit-transform: scale(1) rotate(0deg);
transform: scale(1) rotate(0deg); }
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon--resting,
[data-mfb-state="open"] .mfb-component__main-icon--resting {
opacity: 0;
position: absolute !important; }
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon--active,
[data-mfb-state="open"] .mfb-component__main-icon--active {
opacity: 1; }
/*------------------------------------*\
#BUTTONS | Animations
\*------------------------------------*/
/**
* SLIDE IN + FADE
* When hovering the main button, the child buttons slide out from beneath
* the main button while transitioning from transparent to opaque.
*
*/
.mfb-component--tl.mfb-slidein .mfb-component__list li,
.mfb-component--tr.mfb-slidein .mfb-component__list li {
opacity: 0;
transition: all 0.5s; }
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li,
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li,
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li {
opacity: 1; }
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(70px);
transform: translateY(70px); }
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(140px);
transform: translateY(140px); }
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(210px);
transform: translateY(210px); }
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(280px);
transform: translateY(280px); }
.mfb-component--bl.mfb-slidein .mfb-component__list li,
.mfb-component--br.mfb-slidein .mfb-component__list li {
opacity: 0;
transition: all 0.5s; }
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li,
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li,
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li {
opacity: 1; }
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(-70px);
transform: translateY(-70px); }
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(-140px);
transform: translateY(-140px); }
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(-210px);
transform: translateY(-210px); }
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(-280px);
transform: translateY(-280px); }
/**
* SLIDE IN SPRING
* Same as slide-in but with a springy animation.
*
*/
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li,
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li {
opacity: 0;
transition: all 0.5s;
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); }
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(1) {
transition-delay: 0.05s; }
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(2) {
transition-delay: 0.1s; }
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(3) {
transition-delay: 0.15s; }
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(4) {
transition-delay: 0.2s; }
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li,
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li,
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li {
opacity: 1; }
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
transition-delay: 0.05s;
-webkit-transform: translateY(70px);
transform: translateY(70px); }
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
transition-delay: 0.1s;
-webkit-transform: translateY(140px);
transform: translateY(140px); }
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
transition-delay: 0.15s;
-webkit-transform: translateY(210px);
transform: translateY(210px); }
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
transition-delay: 0.2s;
-webkit-transform: translateY(280px);
transform: translateY(280px); }
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li,
.mfb-component--br.mfb-slidein-spring .mfb-component__list li {
opacity: 0;
transition: all 0.5s;
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); }
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(1) {
transition-delay: 0.05s; }
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(2) {
transition-delay: 0.1s; }
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(3) {
transition-delay: 0.15s; }
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(4) {
transition-delay: 0.2s; }
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li,
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li,
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li {
opacity: 1; }
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
transition-delay: 0.05s;
-webkit-transform: translateY(-70px);
transform: translateY(-70px); }
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
transition-delay: 0.1s;
-webkit-transform: translateY(-140px);
transform: translateY(-140px); }
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
transition-delay: 0.15s;
-webkit-transform: translateY(-210px);
transform: translateY(-210px); }
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
transition-delay: 0.2s;
-webkit-transform: translateY(-280px);
transform: translateY(-280px); }
/**
* ZOOM-IN
* When hovering the main button, the child buttons grow
* from zero to normal size.
*
*/
.mfb-component--tl.mfb-zoomin .mfb-component__list li,
.mfb-component--tr.mfb-zoomin .mfb-component__list li {
-webkit-transform: scale(0);
transform: scale(0); }
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(70px) scale(0);
transform: translateY(70px) scale(0);
transition: all 0.5s;
transition-delay: 0.15s; }
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(140px) scale(0);
transform: translateY(140px) scale(0);
transition: all 0.5s;
transition-delay: 0.1s; }
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(210px) scale(0);
transform: translateY(210px) scale(0);
transition: all 0.5s;
transition-delay: 0.05s; }
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(280px) scale(0);
transform: translateY(280px) scale(0);
transition: all 0.5s;
transition-delay: 0s; }
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(70px) scale(1);
transform: translateY(70px) scale(1);
transition-delay: 0.05s; }
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(140px) scale(1);
transform: translateY(140px) scale(1);
transition-delay: 0.1s; }
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(210px) scale(1);
transform: translateY(210px) scale(1);
transition-delay: 0.15s; }
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(280px) scale(1);
transform: translateY(280px) scale(1);
transition-delay: 0.2s; }
.mfb-component--bl.mfb-zoomin .mfb-component__list li,
.mfb-component--br.mfb-zoomin .mfb-component__list li {
-webkit-transform: scale(0);
transform: scale(0); }
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(-70px) scale(0);
transform: translateY(-70px) scale(0);
transition: all 0.5s;
transition-delay: 0.15s; }
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(-140px) scale(0);
transform: translateY(-140px) scale(0);
transition: all 0.5s;
transition-delay: 0.1s; }
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(-210px) scale(0);
transform: translateY(-210px) scale(0);
transition: all 0.5s;
transition-delay: 0.05s; }
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(-280px) scale(0);
transform: translateY(-280px) scale(0);
transition: all 0.5s;
transition-delay: 0s; }
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(-70px) scale(1);
transform: translateY(-70px) scale(1);
transition-delay: 0.05s; }
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(-140px) scale(1);
transform: translateY(-140px) scale(1);
transition-delay: 0.1s; }
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(-210px) scale(1);
transform: translateY(-210px) scale(1);
transition-delay: 0.15s; }
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(-280px) scale(1);
transform: translateY(-280px) scale(1);
transition-delay: 0.2s; }
/**
* FOUNTAIN
* When hovering the main button the child buttons
* jump into view from outside the viewport
*/
.mfb-component--tl.mfb-fountain .mfb-component__list li,
.mfb-component--tr.mfb-fountain .mfb-component__list li {
-webkit-transform: scale(0);
transform: scale(0); }
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(-70px) scale(0);
transform: translateY(-70px) scale(0);
transition: all 0.5s;
transition-delay: 0.15s; }
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(-140px) scale(0);
transform: translateY(-140px) scale(0);
transition: all 0.5s;
transition-delay: 0.1s; }
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(-210px) scale(0);
transform: translateY(-210px) scale(0);
transition: all 0.5s;
transition-delay: 0.05s; }
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(-280px) scale(0);
transform: translateY(-280px) scale(0);
transition: all 0.5s;
transition-delay: 0s; }
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(70px) scale(1);
transform: translateY(70px) scale(1);
transition-delay: 0.05s; }
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(140px) scale(1);
transform: translateY(140px) scale(1);
transition-delay: 0.1s; }
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(210px) scale(1);
transform: translateY(210px) scale(1);
transition-delay: 0.15s; }
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(280px) scale(1);
transform: translateY(280px) scale(1);
transition-delay: 0.2s; }
.mfb-component--bl.mfb-fountain .mfb-component__list li,
.mfb-component--br.mfb-fountain .mfb-component__list li {
-webkit-transform: scale(0);
transform: scale(0); }
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(70px) scale(0);
transform: translateY(70px) scale(0);
transition: all 0.5s;
transition-delay: 0.15s; }
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(140px) scale(0);
transform: translateY(140px) scale(0);
transition: all 0.5s;
transition-delay: 0.1s; }
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(210px) scale(0);
transform: translateY(210px) scale(0);
transition: all 0.5s;
transition-delay: 0.05s; }
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(280px) scale(0);
transform: translateY(280px) scale(0);
transition: all 0.5s;
transition-delay: 0s; }
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
-webkit-transform: translateY(-70px) scale(1);
transform: translateY(-70px) scale(1);
transition-delay: 0.05s; }
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
-webkit-transform: translateY(-140px) scale(1);
transform: translateY(-140px) scale(1);
transition-delay: 0.1s; }
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
-webkit-transform: translateY(-210px) scale(1);
transform: translateY(-210px) scale(1);
transition-delay: 0.15s; }
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
-webkit-transform: translateY(-280px) scale(1);
transform: translateY(-280px) scale(1);
transition-delay: 0.2s; }
/*------------------------------------*\
#LABELS | base
\*------------------------------------*/
/**
* These are the labels associated to each button,
* exposed only when hovering the related button.
* They are called labels but are in fact data-attributes of
* each button (an anchor tag).
*/
[data-mfb-label]:after {
content: attr(data-mfb-label);
opacity: 0;
transition: all 0.5s;
background: rgba(0, 0, 0, 0.4);
padding: 4px 10px;
border-radius: 3px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
font-weight: normal;
pointer-events: none;
line-height: normal;
position: absolute;
top: 50%;
margin-top: -11px;
transition: all 0.5s; }
[data-mfb-toggle="hover"] [data-mfb-label]:hover:after,
[data-mfb-state="open"] [data-mfb-label]:after {
content: attr(data-mfb-label);
opacity: 1;
transition: all 0.3s; }
/*------------------------------------*\
#LABELS | Modifiers
\*------------------------------------*/
.mfb-component--br [data-mfb-label]:after, .mfb-component--tr [data-mfb-label]:after {
content: attr(data-mfb-label);
right: 70px; }
.mfb-component--br .mfb-component__list [data-mfb-label]:after, .mfb-component--tr .mfb-component__list [data-mfb-label]:after {
content: attr(data-mfb-label);
right: 70px; }
.mfb-component--tl [data-mfb-label]:after, .mfb-component--bl [data-mfb-label]:after {
content: attr(data-mfb-label);
left: 70px; }
.mfb-component--tl .mfb-component__list [data-mfb-label]:after, .mfb-component--bl .mfb-component__list [data-mfb-label]:after {
content: attr(data-mfb-label);
left: 70px; }
/*------------------------------------*\
#DEVELOPMENT | In development
\*------------------------------------*/
/**
* This part is where unfinished code should stay.
* When a feature is ready(sh) move these styles to their proper place.
*/
/*------------------------------------*\
#DEVELOPMENT | Debuggers
\*------------------------------------*/
/**
* These are mainly helpers for development. They do not have to end up
* in production but it's handy to keep them when developing.
*/
/**
* Apply this class to the html tag when developing the slide-in button
*/
/*# sourceMappingURL=mfb.css.map */
File diff suppressed because one or more lines are too long
-98
View File
@@ -1,98 +0,0 @@
/**
* Material floating button
* By: Nobita
* Repo and docs: https://github.com/nobitagit/material-floating-button
*
* License: MIT
*/
// build script hook - don't remove
;(function ( window, document, undefined ) {
'use strict';
/**
* Some defaults
*/
var clickOpt = 'click',
hoverOpt = 'hover',
toggleMethod = 'data-mfb-toggle',
menuState = 'data-mfb-state',
isOpen = 'open',
isClosed = 'closed',
mainButtonClass = 'mfb-component__button--main';
/**
* Internal references
*/
var elemsToClick,
elemsToHover,
mainButton,
target,
currentState;
/**
* For every menu we need to get the main button and attach the appropriate evt.
*/
function attachEvt( elems, evt ){
for( var i = 0, len = elems.length; i < len; i++ ){
mainButton = elems[i].querySelector('.' + mainButtonClass);
mainButton.addEventListener( evt , toggleButton, false);
}
}
/**
* Remove the hover option, set a click toggle and a default,
* initial state of 'closed' to menu that's been targeted.
*/
function replaceAttrs( elems ){
for( var i = 0, len = elems.length; i < len; i++ ){
elems[i].setAttribute( toggleMethod, clickOpt );
elems[i].setAttribute( menuState, isClosed );
}
}
function getElemsByToggleMethod( selector ){
return document.querySelectorAll('[' + toggleMethod + '="' + selector + '"]');
}
/**
* The open/close action is performed by toggling an attribute
* on the menu main element.
*
* First, check if the target is the menu itself. If it's a child
* keep walking up the tree until we found the main element
* where we can toggle the state.
*/
function toggleButton( evt ){
target = evt.target;
while ( target && !target.getAttribute( toggleMethod ) ){
target = target.parentNode;
if(!target) { return; }
}
currentState = target.getAttribute( menuState ) === isOpen ? isClosed : isOpen;
target.setAttribute(menuState, currentState);
}
/**
* On touch enabled devices we assume that no hover state is possible.
* So, we get the menu with hover action configured and we set it up
* in order to make it usable with tap/click.
**/
if ( window.Modernizr && Modernizr.touch ){
elemsToHover = getElemsByToggleMethod( hoverOpt );
replaceAttrs( elemsToHover );
}
elemsToClick = getElemsByToggleMethod( clickOpt );
attachEvt( elemsToClick, 'click' );
// build script hook - don't remove
})( window, document );
-1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
!function(a,b){"use strict";function c(a,b){for(var c=0,d=a.length;d>c;c++)i=a[c].querySelector("."+r),i.addEventListener(b,f,!1)}function d(a){for(var b=0,c=a.length;c>b;b++)a[b].setAttribute(n,l),a[b].setAttribute(o,q)}function e(a){return b.querySelectorAll("["+n+'="'+a+'"]')}function f(a){for(j=a.target;j&&!j.getAttribute(n);)if(j=j.parentNode,!j)return;k=j.getAttribute(o)===p?q:p,j.setAttribute(o,k)}var g,h,i,j,k,l="click",m="hover",n="data-mfb-toggle",o="data-mfb-state",p="open",q="closed",r="mfb-component__button--main";a.Modernizr&&Modernizr.touch&&(h=e(m),d(h)),g=e(l),c(g,"click")}(window,document);

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