diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ece619271b..5be3a99d625 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,7 @@ Some simple rules, that will make it easier to maintain our codebase: * A space before function parameters, such as: `function boom (name, callback) { }`, this makes searching for calls easier * Name your callback functions, such as `boom('the name', function afterBoom ( result ) { }` * Don't include author names in the header of your files, if you need to give credit to someone else do it in the commit comment. +* Use single quotes. * Use the comma first style, for example: ```javascript diff --git a/README.md b/README.md index 327042697ae..a3c248c7fbb 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,14 @@ low values, which can be cleared by any watcher of the data. Community maintained fork of the [original cgm-remote-monitor][original]. +[![Coverage Status](https://coveralls.io/repos/github/nightscout/cgm-remote-monitor/badge.svg?branch=dev)](https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=dev) + [build-img]: https://img.shields.io/travis/nightscout/cgm-remote-monitor.svg [build-url]: https://travis-ci.org/nightscout/cgm-remote-monitor [dependency-img]: https://img.shields.io/david/nightscout/cgm-remote-monitor.svg [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor -[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg -[coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master +[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/dev.svg +[coverage-url]: https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=dev [codacy-img]: https://www.codacy.com/project/badge/f79327216860472dad9afda07de39d3b [codacy-url]: https://www.codacy.com/app/Nightscout/cgm-remote-monitor [gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg @@ -84,6 +86,7 @@ Community maintained fork of the - [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge) - [`pump` (Pump Monitoring)](#pump-pump-monitoring) - [`openaps` (OpenAPS)](#openaps-openaps) + - [`loop` (Loop)](#loop-loop) - [Extended Settings](#extended-settings) - [Pushover](#pushover) - [IFTTT Maker](#ifttt-maker) @@ -142,11 +145,11 @@ The server status and settings are available from `/api/v1/status.json`. By default the `/entries` and `/treatments` APIs limit results to the the most recent 10 values from the last 2 days. You can get many more results, by using the `count`, `date`, `dateString`, and `created_at` parameters, depending on the type of data you're looking for. - + #### Example Queries (replace `http://localhost:1337` with your base url, YOUR-SITE) - + * 100's: `http://localhost:1337/api/v1/entries.json?find[sgv]=100` * BGs between 2 days: `http://localhost:1337/api/v1/entries/sgv.json?find[dateString][$gte]=2015-08-28&find[dateString][$lte]=2015-08-30` * Juice Box corrections in a year: `http://localhost:1337/api/v1/treatments.json?count=1000&find[carbs]=15&find[eventType]=Carb+Correction&find[created_at][$gte]=2015` @@ -171,13 +174,16 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `ENABLE` - Used to enable optional features, expects a space delimited list, such as: `careportal rawbg iob`, see [plugins](#plugins) below * `DISABLE` - Used to disable default features, expects a space delimited list, such as: `direction upbat`, see [plugins](#plugins) below * `API_SECRET` - A secret passphrase that must be at least 12 characters long, required to enable `POST` and `PUT`; also required for the Care Portal - * `TREATMENTS_AUTH` (`off`) - possible values `on` or `off`. When on device must be authenticated by entering `API_SECRET` to create treatments + * `AUTH_DEFAULT_ROLES` (`readable`) - possible values `readable`, `denied`, or any valid role + name. When `readable`, anyone can view Nightscout without a token. + Setting it to `denied` will require a token from every visit, using `status-only` will enable api-secret based login. + * `TREATMENTS_AUTH` (`on`) - possible values `on` or `off`. Deprecated, if set to `off` the `careportal` role will be added to `AUTH_DEFAULT_ROLES` ### Alarms These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser) - + * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's * `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent * `BG_TARGET_TOP` (`180`) - must be set using mg/dl units; the top of the target range, also used to draw the line on the chart @@ -220,12 +226,12 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm * `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled - * `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"` + * `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"` * `LANGUAGE` (`en`) - language of Nightscout. If not available english is used * `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide. * The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range. * 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. + * 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`. Enable or disable icon allowing enter treatments edit mode ### Plugins @@ -235,7 +241,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm The built-in/example plugins that are available by default are listed below. The plugins may still need to be enabled by adding to the `ENABLE` environment variable. #### Default Plugins - + These can be disabled by setting the `DISABLE` env var, for example `DISABLE="direction upbat"` ##### `delta` (BG Delta) @@ -286,7 +292,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm ##### `careportal` (Careportal) An optional form to enter treatments. - + ##### `boluscalc` (Bolus Wizard) ##### `food` (Custom Foods) @@ -375,15 +381,22 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm ##### `openaps` (OpenAPS) Integrated OpenAPS loop monitoring, uses these extended settings: * Requires `DEVICESTATUS_ADVANCED="true"` to be set - * `OPENAPS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when OpenAPS isn't looping. If OpenAPS is going to offline for a period of time, you can add an `OpenAPS Offline` event for the expected duration from Careportal to avoid getting alerts. - * `OPENAPS_WARN` (`30`) - The number of minutes since the last loop that needs to be exceed before an alert is triggered + * `OPENAPS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when OpenAPS isn't looping. If OpenAPS is going to offline for a period of time, you can add an `OpenAPS Offline` event for the expected duration from Careportal to avoid getting alerts. + * `OPENAPS_WARN` (`30`) - The number of minutes since the last loop that needs to be exceed before an alert is triggered * `OPENAPS_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceed before an urgent alarm is triggered * `OPENAPS_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display by default. Any of the following fields: `status-symbol`, `status-label`, `iob`, `meal-assist`, `freq`, and `rssi` * `OPENAPS_RETRO_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display in retro mode. Any of the above fields. - Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). - + +##### `loop` (Loop) + iOS Loop app monitoring, uses these extended settings: + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `LOOP_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when Loop isn't looping. + * `LOOP_WARN` (`30`) - The number of minutes since the last loop that needs to be exceeded before an alert is triggered + * `LOOP_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceeded before an urgent alarm is triggered + * Add `loop` to `SHOW_FORECAST` to show forecasted BG. + #### Extended Settings Some plugins support additional configuration using extra environment variables. These are prefixed with the name of the plugin and a `_`. For example setting `MYPLUGIN_EXAMPLE_VALUE=1234` would make `extendedSettings.exampleValue` available to the `MYPLUGIN` plugin. @@ -400,7 +413,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm You’ll need to [Create a Pushover Application](https://pushover.net/apps/build). You only need to set the Application name, you can ignore all the other settings, but setting an Icon is a nice touch. Maybe you'd like to use [this one](https://raw.githubusercontent.com/nightscout/cgm-remote-monitor/master/static/images/large.png)? Pushover is configured using the following Environment Variables: - + * `ENABLE` - `pushover` should be added to the list of plugin, for example: `ENABLE="pushover"`. * `PUSHOVER_API_TOKEN` - Used to enable pushover notifications, this token is specific to the application you create from in [Pushover](https://pushover.net/), ***[additional pushover information](#pushover)*** below. * `PUSHOVER_USER_KEY` - Your Pushover user key, can be found in the top left of the [Pushover](https://pushover.net/) site, this can also be a pushover delivery group key to send to a group rather than just a single user. This also supports a space delimited list of keys. To disable `INFO` level pushes set this to `off`. @@ -412,16 +425,16 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm If you never want to get info level notifications (treatments) use `PUSHOVER_USER_KEY="off"` If you never want to get an alarm via pushover use `PUSHOVER_ALARM_KEY="off"` If you never want to get an announcement via pushover use `PUSHOVER_ANNOUNCEMENT_KEY="off"` - + If only `PUSHOVER_USER_KEY` is set it will be used for all info notifications, alarms, and announcements For testing/development try [localtunnel](http://localtunnel.me/). #### IFTTT Maker In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker). - + With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more. - + 1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join) 2. Find your secret key on the [maker page](https://ifttt.com/maker) 3. Configure Nightscout by setting these environment variables: @@ -429,7 +442,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also support a space delimited list of keys. * `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. 4. [Create a recipe](https://ifttt.com/myrecipes/personal/new) or see [more detailed instructions](lib/plugins/maker-setup.md#create-a-recipe) - + Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are: * `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging. * `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green. @@ -441,7 +454,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm ### Treatment Profile Some of the [plugins](#plugins) make use of a treatment profile that can be edited using the Profile Editor, see the link in the Settings drawer on your site. - + Treatment Profile Fields: * `timezone` (Time Zone) - time zone local to the patient. *Should be set.* @@ -453,7 +466,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `basal` The basal rate set on the pump. * `target_high` - Upper target for correction boluses. * `target_low` - Lower target for correction boluses. - + Some example profiles are [here](example-profiles.md). ## Setting environment variables @@ -490,7 +503,7 @@ the web interface on [http://192.168.33.10:1337](http://192.168.33.10:1337) ## Installation on Windows -If you have access to local computing resources and want to maintain more control over your data, you can host Nightscout and its database outside of the cloud. Windows Server supports MongoDB, Node.js, and Nightscout [installed on a single system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10. +If you have access to local computing resources and want to maintain more control over your data, you can host Nightscout and its database outside of the cloud. Windows Server supports MongoDB, Node.js, and Nightscout [installed on a single system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10. More questions? --------------- diff --git a/app.js b/app.js index a59025526a0..58d93b77482 100644 --- a/app.js +++ b/app.js @@ -33,6 +33,7 @@ function create (env, ctx) { app.use('/api/v1', bodyParser({limit: 1048576 * 50 }), api); app.use('/api/v2/properties', ctx.properties); + app.use('/api/v2/authorization', ctx.authorization.endpoints); // pebble data app.get('/pebble', ctx.pebble); diff --git a/env.js b/env.js index b57645ed0de..81a60a64876 100644 --- a/env.js +++ b/env.js @@ -29,9 +29,6 @@ function config ( ) { setMongo(); updateSettings(); - // require authorization for entering treatments - env.treatments_auth = readENV('TREATMENTS_AUTH',false); - return env; } @@ -65,6 +62,12 @@ function setAPISecret() { var shasum = crypto.createHash('sha1'); shasum.update(readENV('API_SECRET')); env.api_secret = shasum.digest('hex'); + + if (!readENV('TREATMENTS_AUTH', true)) { + + } + + } } } @@ -103,6 +106,7 @@ function setMongo() { console.info('MQTT configured to use a custom client id, it will override the default: ', env.mqtt_client_id); } } + env.authentication_collections_prefix = readENV('MONGO_AUTHENTICATION_COLLECTIONS_PREFIX', 'auth_'); env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); env.profile_collection = readENV('MONGO_PROFILE_COLLECTION', 'profile'); env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); @@ -131,6 +135,13 @@ function updateSettings() { //should always find extended settings last env.extendedSettings = findExtendedSettings(process.env); + + if (!readENVTruthy('TREATMENTS_AUTH', true)) { + env.settings.authDefaultRoles = env.settings.authDefaultRoles || ""; + env.settings.authDefaultRoles += ' careportal'; + } + + } function readENV(varName, defaultValue) { @@ -140,12 +151,17 @@ function readENV(varName, defaultValue) { || process.env[varName] || process.env[varName.toLowerCase()]; - if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } - if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } return value != null ? value : defaultValue; } +function readENVTruthy(varName, defaultValue) { + var value = readENV(varName, defaultValue); + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + return value; +} + function findExtendedSettings (envs) { var extended = {}; diff --git a/lib/admin_plugins/cleanstatusdb.js b/lib/admin_plugins/cleanstatusdb.js index 86e61e987f8..4769a724789 100644 --- a/lib/admin_plugins/cleanstatusdb.js +++ b/lib/admin_plugins/cleanstatusdb.js @@ -27,11 +27,12 @@ cleanstatusdb.actions[0].init = function init(client, callback) { $status.hide().text(translate('Loading database ...')).fadeIn('slow'); $.ajax('/api/v1/devicestatus.json?count=500', { - success: function (records) { + headers: client.headers() + , success: function (records) { var recs = (records.length === 500 ? '500+' : records.length); $status.hide().text(translate('Database contains %1 records',{ params: [recs] })).fadeIn('slow'); - }, - error: function () { + } + , error: function () { $status.hide().text(translate('Error loading database')).fadeIn('slow'); } }).done(function () { if (callback) { callback(); } }); @@ -53,9 +54,7 @@ cleanstatusdb.actions[0].code = function deleteRecords(client, callback) { $.ajax({ method: 'DELETE' , url: '/api/v1/devicestatus/*' - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function success () { $status.hide().text(translate('All records removed ...')).fadeIn('slow'); if (callback) { diff --git a/lib/admin_plugins/futureitems.js b/lib/admin_plugins/futureitems.js index ab888aff0a6..04277e401c7 100644 --- a/lib/admin_plugins/futureitems.js +++ b/lib/admin_plugins/futureitems.js @@ -63,15 +63,16 @@ futureitems.actions[0].init = function init(client, callback) { $status.hide().text(translate('Loading database ...')).fadeIn('slow'); var nowiso = new Date().toISOString(); $.ajax('/api/v1/treatments.json?&find[created_at][$gte]=' + nowiso, { - success: function (records) { + headers: client.headers() + , success: function (records) { futureitems.treatmentrecords = records; $status.hide().text(translate('Database contains %1 future records',{ params: [records.length] })).fadeIn('slow'); var table = $('').css('margin-top','10px'); $('#admin_' + futureitems.name + '_0_html').append(table); showTreatments(records, table); futureitems.actions[0].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); - }, - error: function () { + } + , error: function () { $status.hide().text(translate('Error loading database')).fadeIn('slow'); futureitems.treatmentrecords = []; } @@ -94,9 +95,7 @@ futureitems.actions[0].code = function deleteRecords(client, callback) { $.ajax({ method: 'DELETE' , url: '/api/v1/treatments/' + _id - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function success () { $status.text(translate('Record %1 removed ...', { params: [_id] })); }).fail(function fail() { @@ -122,12 +121,13 @@ futureitems.actions[1].init = function init(client, callback) { $status.hide().text(translate('Loading database ...')).fadeIn('slow'); var now = new Date().getTime(); $.ajax('/api/v1/entries.json?&find[date][$gte]=' + now, { - success: function (records) { + headers: client.headers() + , success: function (records) { futureitems.entriesrecords = records; $status.hide().text(translate('Database contains %1 future records',{ params: [records.length] })).fadeIn('slow'); futureitems.actions[1].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); - }, - error: function () { + } + , error: function () { $status.hide().text(translate('Error loading database')).fadeIn('slow'); futureitems.entriesrecords = []; } @@ -150,9 +150,7 @@ futureitems.actions[1].code = function deleteRecords(client, callback) { $.ajax({ method: 'DELETE' , url: '/api/v1/entries/' + _id - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function success () { $status.text(translate('Record %1 removed ...', { params: [_id] })); }).fail(function fail() { diff --git a/lib/admin_plugins/index.js b/lib/admin_plugins/index.js index f7a5c74133d..c5e86cb5405 100644 --- a/lib/admin_plugins/index.js +++ b/lib/admin_plugins/index.js @@ -4,7 +4,9 @@ var _ = require('lodash'); function init() { var allPlugins = [ - require('./cleanstatusdb')() + require('./subjects')() + , require('./roles')() + , require('./cleanstatusdb')() , require('./futureitems')() ]; @@ -32,8 +34,10 @@ function init() { } var a = p.actions[i]; // add main plugin html - fs.append($('').css('text-decoration','underline').append(translate(a.name))); - fs.append('
'); + if (a.name) { + fs.append($('').css('text-decoration','underline').append(translate(a.name))); + fs.append('
'); + } fs.append($('').append(translate(a.description))); fs.append($('
').attr('id','admin_' + p.name + '_' + i + '_html')); fs.append($('
').css('margin-top','10px'); + $('#admin_' + roles.name + '_0_html').append(table).append(genDialog(client)); + reload(client, callback); + } + , preventClose: true + , code: function createNewRole (client, callback) { + var role = { }; + openDialog(role, client, callback); + } +}]; + +function createOrSaveRole (role, client, callback) { + + var method = _.isEmpty(role._id) ? 'POST' : 'PUT'; + + $.ajax({ + method: method + , url: '/api/v2/authorization/roles/' + , headers: client.headers() + , data: role + }).done(function success() { + reload(client, callback); + }).fail(function fail(err) { + console.error('Unable to ' + method + ' Subject', err.responseText); + window.alert(client.translate('Unable to ' + method + ' Subject')); + if (callback) { + callback(err); + } + }); +} + +function deleteRole (role, client, callback) { + $.ajax({ + method: 'DELETE' + , url: '/api/v2/authorization/roles/' + role._id + , headers: client.headers() + }).done(function success() { + reload(client, callback); + }).fail(function fail(err) { + console.error('Unable to delete Subject', err.responseText); + window.alert(client.translate('Unable to delete Subject')); + if (callback) { + callback(err); + } + }); +} + +function reload (client, callback) { + $.ajax({ + method: 'GET' + , url:'/api/v2/authorization/roles' + , headers: client.headers() + }).done(function success (records) { + roles.records = records; + $status.hide().text(client.translate('Database contains %1 roles',{ params: [records.length] })).fadeIn('slow'); + showRoles(records, client); + if (callback) { + callback(); + } + }).fail(function fail(err) { + $status.hide().text(client.translate('Error loading database')).fadeIn('slow'); + roles.records = []; + if (callback) { + callback(err); + } + }); +} + +function genDialog (client) { + var ret = + '' + ; + + return $(ret); +} + +function openDialog (role, client) { + $( '#editroledialog' ).dialog({ + width: 360 + , height: 360 + , buttons: [ + { text: client.translate('Save'), + class: 'leftButton', + click: function() { + + role.name = $('#edrole_name').val(); + role.permissions = + _.chain($('#edrole_permissions').val().toLowerCase().split(/[;, ]/)) + .map(_.trim) + .reject(_.isEmpty) + .sort() + .value(); + role.notes = $('#edrole_notes').val(); + + var self = this; + delete role.autoGenerated; + createOrSaveRole(role, client, function callback () { + $( self ).dialog('close'); + }); + } + }, + { text: client.translate('Cancel'), + click: function () { $( this ).dialog('close'); } + } + ] + , open : function() { + $(this).parent().css('box-shadow', '20px 20px 20px 0px black'); + $(this).parent().find('.ui-dialog-buttonset' ).css({'width':'100%','text-align':'right'}); + $(this).parent().find('button:contains("'+client.translate('Save')+'")').css({'float':'left'}); + $('#edrole_name').val(role.name || '').focus(); + $('#edrole_permissions').val(role.permissions ? role.permissions.join(' ') : ''); + $('#edrole_notes').val(role.notes || ''); + } + + }); + +} + +function showRole (role, table, client) { + var editIcon = $(''); + editIcon.click(function clicked ( ) { + openDialog(role, client); + }); + + var deleteIcon = ''; + if (role._id) { + deleteIcon = $(''); + deleteIcon.click(function clicked() { + var ok = window.confirm(client.translate('Are you sure you want to delete: ') + role.name); + if (ok) { + deleteRole(role, client); + } + }); + } + + table.append($('').css('background-color','#0f0f0f') + .append($('').css('background','#040404') + .append($('').css('background-color','#0f0f0f') + .append($('').css('background','#040404') + .append($('
').attr('width','20%').append(editIcon).append(deleteIcon).append(role.name)) + .append($('').attr('width','20%').append(_.isEmpty(role.permissions) ? '[none]' : role.permissions.join(' '))) + .append($('').attr('width','10%').append(role._id ? (role.notes ? role.notes : '') : '[system default]')) + ); +} + +function showRoles (roles, client) { + var table = $('#admin_roles_table'); + table.empty().append($('
').css('width','100px').attr('align','left').append(client.translate('Name'))) + .append($('').css('width','150px').attr('align','left').append(client.translate('Permissions'))) + .append($('').css('width','150px').attr('align','left').append(client.translate('Notes'))) + ); + for (var t=0; t').css('margin-top','10px'); + $('#admin_' + subjects.name + '_0_html').append(table).append(genDialog(client)); + reload(client, callback); + } + , preventClose: true + , code: function createNewSubject (client, callback) { + openDialog({}, client, callback); + } +}]; + +function createOrSaveSubject (subject, client, callback) { + + var method = _.isEmpty(subject._id) ? 'POST' : 'PUT'; + + $.ajax({ + method: method + , url: '/api/v2/authorization/subjects/' + , headers: client.headers() + , data: subject + }).done(function success() { + reload(client, callback); + }).fail(function fail(err) { + console.error('Unable to ' + method + ' Subject', err.responseText); + window.alert(client.translate('Unable to ' + method + ' Subject')); + if (callback) { + callback(); + } + }); +} + +function deleteSubject (subject, client, callback) { + $.ajax({ + method: 'DELETE' + , url: '/api/v2/authorization/subjects/' + subject._id + , headers: client.headers() + }).done(function success() { + reload(client, callback); + }).fail(function fail(err) { + console.error('Unable to delete Subject', err.responseText); + window.alert(client.translate('Unable to delete Subject')); + if (callback) { + callback(); + } + }); +} + +function reload (client, callback) { + $.ajax({ + method: 'GET' + , url:'/api/v2/authorization/subjects' + , headers: client.headers() + }).done(function success (records) { + subjects.records = records; + $status.hide().text(client.translate('Database contains %1 subjects',{ params: [records.length] })).fadeIn('slow'); + showSubjects(records, client); + if (callback) { + callback(); + } + }).fail(function fail(err) { + $status.hide().text(client.translate('Error loading database')).fadeIn('slow'); + subjects.records = []; + if (callback) { + callback(err); + } + }); +} + +function genDialog (client) { + var ret = + '' + ; + + return $(ret); +} + +function openDialog (subject, client) { + $( '#editsubjectdialog' ).dialog({ + width: 360 + , height: 300 + , buttons: [ + { text: client.translate('Save'), + class: 'leftButton', + click: function() { + subject.name = $('#edsub_name').val(); + subject.roles = + _.chain($('#edsub_roles').val().toLowerCase().split(/[;, ]/)) + .map(_.trim) + .reject(_.isEmpty) + .sort() + .value(); + subject.notes = $('#edsub_notes').val(); + + var self = this; + createOrSaveSubject(subject, client, function callback ( ) { + $( self ).dialog('close'); + }); + } + }, + { text: client.translate('Cancel'), + click: function () { $( this ).dialog('close'); } + } + ] + , open : function() { + $(this).parent().css('box-shadow', '20px 20px 20px 0px black'); + $(this).parent().find('.ui-dialog-buttonset' ).css({'width':'100%','text-align':'right'}); + $(this).parent().find('button:contains("'+client.translate('Save')+'")').css({'float':'left'}); + $('#edsub_name').val(subject.name || '').focus(); + $('#edsub_roles').val(subject.roles ? subject.roles.join(', ') : ''); + $('#edsub_notes').val(subject.notes || ''); + } + + }); + +} + +function showSubject (subject, table, client) { + var editIcon = $(''); + editIcon.click(function clicked ( ) { + openDialog(subject, client); + }); + var deleteIcon = $(''); + deleteIcon.click(function clicked ( ) { + var ok = window.confirm(client.translate('Are you sure you want to delete: ') + subject.name); + if (ok) { + deleteSubject(subject, client); + } + }); + table.append($('
').attr('width','20%').append(editIcon).append(deleteIcon).append(subject.name)) + .append($('').attr('width','20%').append(subject.roles ? subject.roles.join(', ') : '[none]')) + .append($('').attr('width','20%').append('' + subject.accessToken + '')) + .append($('').attr('width','10%').append(subject.notes ? subject.notes : '')) + ); +} + +function showSubjects (subjects, client) { + var table = $('#admin_subjects_table'); + table.empty().append($('
').css('width','100px').attr('align','left').append(client.translate('Name'))) + .append($('').css('width','150px').attr('align','left').append(client.translate('Roles'))) + .append($('').css('width','150px').attr('align','left').append(client.translate('Access Token'))) + .append($('').css('width','150px').attr('align','left').append(client.translate('Notes'))) + ); + for (var t=0; t 12) ? (secret === api_secret) : false; - res.sendJSONStatus(res, consts.HTTP_OK, authorized ? 'OK' : 'UNAUTHORIZED'); + ctx.authorization.resolveWithRequest(req, function resolved (err, result) { + + // this is used to see if req has api-secret equivalent authorization + var authorized = !err && + ctx.authorization.checkMultiple('*:*:create,update,delete', result.shiros) && //can write to everything + ctx.authorization.checkMultiple('admin:*:*:*', result.shiros); //full admin permissions too + + res.sendJSONStatus(res, consts.HTTP_OK, authorized ? 'OK' : 'UNAUTHORIZED'); + }); }); return api; diff --git a/lib/authorization/endpoints.js b/lib/authorization/endpoints.js new file mode 100644 index 00000000000..194a97b0113 --- /dev/null +++ b/lib/authorization/endpoints.js @@ -0,0 +1,119 @@ +'use strict'; + +var _ = require('lodash'); +var express = require('express'); + +var consts = require('./../constants'); + +function init (env, authorization) { + var endpoints = express( ); + + var wares = require('./../middleware/index')(env); + + endpoints.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + endpoints.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + endpoints.use(wares.bodyParser.json()); + // also support url-encoded content-type + endpoints.use(wares.bodyParser.urlencoded({ extended: true })); + + endpoints.get('/request/:accessToken', function requestAuthorize (req, res) { + var authorized = authorization.authorize(req.params.accessToken); + + if (authorized) { + res.json(authorized); + } else { + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + } + }); + + endpoints.get('/permissions', authorization.isPermitted('admin:api:permissions:read'), function getSubjects (req, res) { + res.json(authorization.seenPermissions); + }); + + endpoints.get('/permissions/trie', authorization.isPermitted('admin:api:permissions:read'), function getSubjects (req, res) { + res.json(authorization.expandedPermissions()); + }); + + endpoints.get('/subjects', authorization.isPermitted('admin:api:subjects:read'), function getSubjects (req, res) { + res.json(_.map(authorization.storage.subjects, function eachSubject (subject) { + return _.pick(subject, ['_id', 'name', 'accessToken', 'roles']); + })); + }); + + endpoints.post('/subjects', authorization.isPermitted('admin:api:subjects:create'), function createSubject (req, res) { + authorization.storage.createSubject(req.body, function created (err, created) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(created); + } + }); + }); + + endpoints.put('/subjects', authorization.isPermitted('admin:api:subjects:update'), function saveSubject (req, res) { + authorization.storage.saveSubject(req.body, function saved (err, saved) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(saved); + } + }); + }); + + endpoints.delete('/subjects/:_id', authorization.isPermitted('admin:api:subjects:delete'), function deleteSubject (req, res) { + authorization.storage.removeSubject(req.params._id, function deleted (err) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json({ }); + } + }); + }); + + endpoints.get('/roles', authorization.isPermitted('admin:api:roles:list'), function getRoles (req, res) { + res.json(authorization.storage.roles); + }); + + endpoints.post('/roles', authorization.isPermitted('admin:api:roles:create'), function createSubject (req, res) { + authorization.storage.createRole(req.body, function created (err, created) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(created); + } + }); + }); + + endpoints.put('/roles', authorization.isPermitted('admin:api:roles:update'), function saveRole (req, res) { + authorization.storage.saveRole(req.body, function saved (err, saved) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(saved); + } + }); + }); + + endpoints.delete('/roles/:_id', authorization.isPermitted('admin:api:roles:delete'), function deleteRole (req, res) { + authorization.storage.removeRole(req.params._id, function deleted (err) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json({ }); + } + }); + }); + + endpoints.get('/debug/check/:permission', function check (req, res, next) { + authorization.isPermitted(req.params.permission)(req, res, next); + }, function debug (req, res) { + res.json({check: true}); + }); + + + return endpoints; +} + +module.exports = init; diff --git a/lib/authorization/index.js b/lib/authorization/index.js new file mode 100644 index 00000000000..0c0c6f5f2a7 --- /dev/null +++ b/lib/authorization/index.js @@ -0,0 +1,254 @@ +'use strict'; + +var _ = require('lodash'); +var jwt = require('jsonwebtoken'); +var shiroTrie = require('shiro-trie'); + +var consts = require('./../constants'); + +var log_green = '\x1B[32m'; +var log_red = '\x1b[31m'; +var log_reset = '\x1B[0m'; +var LOG_GRANTED = log_green + 'GRANTED: ' + log_reset; +var LOG_DENIED = log_red + 'DENIED: ' + log_reset; + +function mkopts (opts) { + var options = opts && !_.isEmpty(opts) ? opts : { }; + if (!options.redirectDeniedURL) { + options.redirectDeniedURL = null; + } + return options; +} + +function getRemoteIP (req) { + return req.headers['x-forwarded-for'] || req.connection.remoteAddress; +} + +function init (env, ctx) { + var authorization = { }; + var storage = authorization.storage = require('./storage')(env, ctx); + var defaultRoles = (env.settings.authDefaultRoles || '').split(/[, :]/); + + function extractToken (req) { + var token; + var authorization = req.header('Authorization'); + + if (authorization) { + var parts = authorization.split(' '); + if (parts.length === 2 && parts[0] === 'Bearer') { + token = parts[1]; + } + } + + if (!token && req.auth_token) { + token = req.auth_token; + } + + if (!token) { + token = authorizeAccessToken(req); + } + + if (token) { + req.auth_token = token; + } + + return token; + } + + function authorizeAccessToken (req) { + + var accessToken = req.query.token; + + if (!accessToken && req.body) { + if (_.isArray(req.body) && req.body.length > 0 && req.body[0].token) { + accessToken = req.body[0].token; + delete req.body[0].token; + } else if (req.body.token) { + accessToken = req.body.token; + delete req.body.token; + } + } + + var authToken = null; + + if (accessToken) { + // make an auth token on the fly, based on an access token + var authed = authorization.authorize(accessToken); + if (authed && authed.token) { + authToken = authed.token; + } + } + + return authToken; + } + + function adminSecretFromRequest (req) { + var secret = req.query && req.query.secret ? req.query.secret : req.header('api-secret'); + + if (!secret && req.api_secret) { + //see if we already got the secret from the body, since it gets deleted + secret = req.api_secret; + } else if (!secret && req.body) { + // try to get the secret from the body, but don't leave it there + if (_.isArray(req.body) && req.body.length > 0 && req.body[0].secret) { + secret = req.body[0].secret; + delete req.body[0].secret; + } else if (req.body.secret) { + secret = req.body.secret; + delete req.body.secret; + } + } + + if (secret) { + // store the secret hash on the request since the req may get processed again + req.api_secret = secret; + } + + return secret; + } + + function authorizeAdminSecretWithRequest (req) { + return authorizeAdminSecret(adminSecretFromRequest(req)); + } + + function authorizeAdminSecret (secret) { + return (env.api_secret && env.api_secret.length > 12) ? (secret === env.api_secret) : false; + } + + authorization.seenPermissions = [ ]; + + authorization.expandedPermissions = function expandedPermissions ( ) { + var permissions = shiroTrie.new(); + permissions.add(authorization.seenPermissions); + return permissions; + }; + + authorization.resolveWithRequest = function resolveWithRequest (req, callback) { + authorization.resolve({ + api_secret: adminSecretFromRequest(req) + , token: extractToken(req) + }, callback); + }; + + authorization.checkMultiple = function checkMultiple(permission, shiros) { + var found = _.find(shiros, function checkEach (shiro) { + return shiro && shiro.check(permission); + }); + return _.isObject(found); + }; + + authorization.resolve = function resolve (data, callback) { + + if (authorizeAdminSecret(data.api_secret)) { + var admin = shiroTrie.new(); + admin.add(['*']); + return callback(null, { shiros: [ admin ] }); + } + + var defaultShiros = storage.rolesToShiros(defaultRoles); + + if (data.token) { + jwt.verify(data.token, env.api_secret, function result(err, verified) { + if (err) { + return callback(err, { shiros: [ ] }); + } else { + var resolved = storage.resolveSubjectAndPermissions(verified.accessToken); + var shiros = resolved.shiros.concat(defaultShiros); + return callback(null, { shiros: shiros, subject: resolved.subject }); + } + }); + } else { + return callback(null, { shiros: defaultShiros }); + } + + }; + + authorization.isPermitted = function isPermitted (permission, opts) { + + + opts = mkopts(opts); + authorization.seenPermissions = _.chain(authorization.seenPermissions) + .push(permission) + .sort() + .uniq() + .value(); + + function check(req, res, next) { + + var remoteIP = getRemoteIP(req); + + if (authorizeAdminSecretWithRequest(req)) { + console.log(LOG_GRANTED, remoteIP, 'api-secret', permission); + next( ); + return; + } + + var token = extractToken(req); + var defaultShiros = storage.rolesToShiros(defaultRoles); + + if (token) { + jwt.verify(token, env.api_secret, function result(err, verified) { + if (err) { + console.info('Error verifying Authorized Token', err); + res.status(consts.HTTP_UNAUTHORIZED).send('Unauthorized - Invalid/Missing'); + } else { + var resolved = storage.resolveSubjectAndPermissions(verified.accessToken); + if (authorization.checkMultiple(permission, resolved.shiros)) { + console.log(LOG_GRANTED, remoteIP, verified.accessToken , permission); + next(); + } else if (authorization.checkMultiple(permission, defaultShiros)) { + console.log(LOG_GRANTED, remoteIP, verified.accessToken, permission, 'default'); + next( ); + } else { + console.log(LOG_DENIED, remoteIP, verified.accessToken, permission); + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + } + } + }); + } else { + if (authorization.checkMultiple(permission, defaultShiros)) { + console.log(LOG_GRANTED, remoteIP, 'no-token', permission, 'default'); + return next( ); + } + console.log(LOG_DENIED, remoteIP, 'no-token', permission); + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + } + + } + + return check; + }; + + authorization.authorize = function authorize (accessToken) { + var subject = storage.findSubject(accessToken); + + var authorized = null; + + if (subject) { + var token = jwt.sign( { accessToken: subject.accessToken }, env.api_secret, { expiresIn: '1h' } ); + + //decode so we can tell the client the issued and expired times + var decoded = jwt.decode(token); + + var roles = _.uniq(subject.roles.concat(defaultRoles)); + + authorized = { + token: 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 + }; + } + + return authorized; + }; + + authorization.endpoints = require('./endpoints')(env, authorization); + + return authorization; +} + +module.exports = init; diff --git a/lib/authorization/storage.js b/lib/authorization/storage.js new file mode 100644 index 00000000000..5b89f2b4177 --- /dev/null +++ b/lib/authorization/storage.js @@ -0,0 +1,226 @@ +'use strict'; + +var _ = require('lodash'); +var crypto = require('crypto'); +var shiroTrie = require('shiro-trie'); +var ObjectID = require('mongodb').ObjectID; + +var find_options = require('../query'); + +function init (env, ctx) { + var storage = { }; + + var rolesCollection = ctx.store.collection(env.authentication_collections_prefix + 'roles'); + var subjectsCollection = ctx.store.collection(env.authentication_collections_prefix + 'subjects'); + + storage.queryOpts = { + dateField: 'created_at' + , noDateFilter: true + }; + + function query_for (opts) { + return find_options(opts, storage.queryOpts); + } + + function create (collection) { + function doCreate(obj, fn) { + if (!obj.hasOwnProperty('created_at')) { + obj.created_at = (new Date()).toISOString(); + } + collection.insert(obj, function (err, doc) { + storage.reload(function loaded() { + fn(null, doc.ops); + }); + }); + } + return doCreate; + } + + function list (collection) { + function doList(opts, fn) { + // these functions, find, sort, and limit, are used to + // dynamically configure the request, based on the options we've + // been given + + // determine sort options + function sort() { + return opts && opts.sort || {date: -1}; + } + + // configure the limit portion of the current query + function limit() { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + // handle all the results + function toArray(err, entries) { + fn(err, entries); + } + + // now just stitch them all together + limit.call(collection + .find(query_for(opts)) + .sort(sort()) + ).toArray(toArray); + } + + return doList; + } + + function remove (collection) { + function doRemove (_id, callback) { + collection.remove({ '_id': new ObjectID(_id) }, function (err) { + storage.reload(function loaded() { + callback(err, null); + }); + }); + } + return doRemove; + } + + function save (collection) { + function doSave (obj, callback) { + obj._id = new ObjectID(obj._id); + if (!obj.created_at) { + obj.created_at = (new Date()).toISOString(); + } + collection.save(obj, function (err) { + //id should be added for new docs + storage.reload(function loaded() { + callback(err, obj); + }); + }); + } + return doSave; + } + + storage.createSubject = create(subjectsCollection); + storage.saveSubject = save(subjectsCollection); + storage.removeSubject = remove(subjectsCollection); + storage.listSubjects = list(subjectsCollection); + + storage.createRole = create(rolesCollection); + storage.saveRole = save(rolesCollection); + storage.removeRole = remove(rolesCollection); + storage.listRoles = list(rolesCollection); + + storage.defaultRoles = [ + { name: 'admin', permissions: ['*'] } + , { name: 'denied', permissions: [ ] } + , { name: 'status-only', permissions: [ 'api:status:read' ] } + , { name: 'readable', permissions: [ '*:*:read' ] } + , { name: 'careportal', permissions: [ 'api:treatments:create' ] } + , { name: 'devicestatus-upload', permissions: [ 'api:devicestatus:create' ] } + ]; + + storage.reload = function reload (callback) { + + storage.listRoles({sort: {name: 1}}, function listResults (err, results) { + if (err) { + return callback && callback(err); + } + + storage.roles = results || [ ]; + + _.forEach(storage.defaultRoles, function eachRole (role) { + if (_.isEmpty(_.find(storage.roles, {name: role.name}))) { + storage.roles.push(role); + } + }); + + storage.roles = _.sortBy(storage.roles, 'name'); + + storage.listSubjects({sort: {name: 1}}, function listResults (err, results) { + if (err) { + return callback && callback(err); + } + + 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()); + var abbrev = subject.name.toLowerCase().replace(/[\W]/g, '').substring(0, 10); + subject.digest = shasum.digest('hex'); + subject.accessToken = abbrev + '-' + subject.digest.substring(0, 16); + } + + return subject; + }); + + if (callback) { + callback( ); + } + }); + }); + + }; + + storage.findRole = function findRole (roleName) { + return _.find(storage.roles, {name: roleName}); + }; + + storage.roleToShiro = function roleToShiro (roleName) { + var shiro = null; + + var role = storage.findRole(roleName); + if (role) { + shiro = shiroTrie.new(); + shiro.add(role.permissions); + } + + return shiro; + }; + + storage.rolesToShiros = function roleToShiro (roleNames) { + return _.chain(roleNames) + .map(storage.roleToShiro) + .reject(_.isEmpty) + .value(); + }; + + storage.roleToPermissions = function roleToPermissions (roleName) { + var permissions = [ ]; + + var role = storage.findRole(roleName); + if (role) { + permissions = role.permissions; + } + + return permissions; + }; + + storage.findSubject = function findSubject (accessToken) { + var prefix = _.last(accessToken.split('-')); + + if (prefix.length < 16) { + return null; + } + + return _.find(storage.subjects, function matches (subject) { + return subject.digest.indexOf(prefix) === 0; + }); + }; + + storage.resolveSubjectAndPermissions = function resolveSubjectAndPermissions (accessToken) { + var shiros = []; + + var subject = storage.findSubject(accessToken); + if (subject) { + shiros = storage.rolesToShiros(subject.roles); + } + + return { + subject: subject + , shiros: shiros + }; + }; + + return storage; + +} + +module.exports = init; diff --git a/lib/bootevent.js b/lib/bootevent.js index 3caf04fe6fc..2cd706ae55d 100644 --- a/lib/bootevent.js +++ b/lib/bootevent.js @@ -14,9 +14,13 @@ function boot (env) { next(); } + function hasBootErrors(ctx) { + return ctx.bootErrors && ctx.bootErrors.length > 0; + } + function setupMongo (ctx, next) { - if (ctx.bootErrors && ctx.bootErrors.length > 0) { + if (hasBootErrors(ctx)) { return next(); } @@ -36,8 +40,23 @@ function boot (env) { } } + function setupAuthorization (ctx, next) { + if (hasBootErrors(ctx)) { + return next(); + } + + ctx.authorization = require('./authorization')(env, ctx); + ctx.authorization.storage.reload(function loaded (err) { + if (err) { + ctx.bootErrors = ctx.bootErrors || [ ]; + ctx.bootErrors.push({'desc': 'Unable to setup authorization', err: err}); + } + next(); + }); + } + function setupInternals (ctx, next) { - if (ctx.bootErrors && ctx.bootErrors.length > 0) { + if (hasBootErrors(ctx)) { return next(); } @@ -66,7 +85,7 @@ function boot (env) { } function ensureIndexes (ctx, next) { - if (ctx.bootErrors && ctx.bootErrors.length > 0) { + if (hasBootErrors(ctx)) { return next(); } @@ -81,7 +100,7 @@ function boot (env) { } function setupListeners (ctx, next) { - if (ctx.bootErrors && ctx.bootErrors.length > 0) { + if (hasBootErrors(ctx)) { return next(); } @@ -116,7 +135,7 @@ function boot (env) { } function setupBridge (ctx, next) { - if (ctx.bootErrors && ctx.bootErrors.length > 0) { + if (hasBootErrors(ctx)) { return next(); } @@ -128,7 +147,7 @@ function boot (env) { } function setupMMConnect (ctx, next) { - if (ctx.bootErrors && ctx.bootErrors.length > 0) { + if (hasBootErrors(ctx)) { return next(); } @@ -140,7 +159,7 @@ function boot (env) { } function finishBoot (ctx, next) { - if (ctx.bootErrors && ctx.bootErrors.length > 0) { + if (hasBootErrors(ctx)) { return next(); } @@ -152,6 +171,7 @@ function boot (env) { return require('bootevent')( ) .acquire(checkEnv) .acquire(setupMongo) + .acquire(setupAuthorization) .acquire(setupInternals) .acquire(ensureIndexes) .acquire(setupListeners) diff --git a/lib/client/boluscalc.js b/lib/client/boluscalc.js index 4f99ef6bb6c..c577522e557 100644 --- a/lib/client/boluscalc.js +++ b/lib/client/boluscalc.js @@ -542,9 +542,7 @@ function init(client, $, plugins) { $.ajax({ method: 'POST', url: '/api/v1/treatments/' - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() , data: data }).done(function treatmentSaved (response) { console.info('treatment saved', response); @@ -623,7 +621,8 @@ function init(client, $, plugins) { categories = []; foodlist = []; $.ajax('/api/v1/food/regular.json', { - success: function (records) { + headers: client.headers() + , success: function (records) { records.forEach(function (r) { foodlist.push(r); if (r.category && !categories[r.category]) { @@ -644,7 +643,8 @@ function init(client, $, plugins) { boluscalc.loadFoodQuickpicks = function loadFoodQuickpicks( ) { // Load quickpicks $.ajax('/api/v1/food/quickpicks.json', { - success: function (records) { + headers: client.headers() + , success: function (records) { quickpicks = records; $('#bc_quickpick').empty().append(''); for (var i=0; i -1); + $('#treatmentDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('careportal') > -1); + $('#boluscalcDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('boluscalc') > -1); + // Edit mode - $('#editbutton').toggle(client.settings.editMode && isAuthenticated); - $('#editbutton').click(function editModeClick (event) { + editButton.toggle(client.settings.editMode && treatmentUpdateAllowed); + editButton.click(function editModeClick (event) { client.editMode = !client.editMode; if (client.editMode) { client.renderer.drawTreatments(client); - $('#editbutton i').addClass('selected'); + editButton.find('i').addClass('selected'); } else { chart.focus.selectAll('.draggable-treatment') .style('cursor', 'default') .on('mousedown.drag', null); - $('#editbutton i').removeClass('selected'); + editButton.find('i').removeClass('selected'); } if (event) { event.preventDefault(); } }); - }); + }; + + client.hashauth = require('../hashauth'); + client.hashauth.init(client, $).initAuthentication(client.afterAuth); client.foucusRangeMS = times.hours(client.settings.focusHours).msecs; $('.focus-range li[data-hours=' + client.settings.focusHours + ']').addClass('selected'); @@ -560,6 +606,28 @@ client.init = function init(serverSettings, plugins) { brushed(); } + function refreshAuthIfNeeded ( ) { + var token = client.browserUtils.queryParms().token; + if (token && client.authorized) { + var renewTime = (client.authorized.exp * 1000) - times.mins(15).msecs - Math.abs((client.authorized.iat * 1000) - client.authorized.lat); + var refreshIn = Math.round((renewTime - client.now) / 1000); + if (client.now > renewTime) { + console.info('Refreshing authorization'); + $.ajax('/api/v2/authorization/request/' + token, { + success: function (authorized) { + if (authorized) { + console.info('Got new authorization', authorized); + authorized.lat = client.now; + client.authorized = authorized; + } + } + }); + } else if (refreshIn < times.mins(5).secs) { + console.info('authorization refresh in ' + refreshIn + 's'); + } + } + } + function updateClock() { updateClockDisplay(); var interval = (60 - (new Date()).getSeconds()) * 1000 + 5; @@ -579,6 +647,7 @@ client.init = function init(serverSettings, plugins) { $('body').css({ 'opacity': opacity.DAY }); } } + refreshAuthIfNeeded(); } function updateClockDisplay() { @@ -750,7 +819,8 @@ client.init = function init(serverSettings, plugins) { 'authorize' , { client: 'web' - , secret: client.hashauth.hash() + , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() + , token: client.authorized && client.authorized.token , history: history } , function authCallback(data) { @@ -766,13 +836,13 @@ client.init = function init(serverSettings, plugins) { //with predicted alarms, latestSGV may still be in target so to see if the alarm // is for a HIGH we can only check if it's >= the bottom of the target function isAlarmForHigh() { - return client.latestSGV.mgdl >= client.settings.thresholds.bgTargetBottom; + return client.latestSGV && client.latestSGV.mgdl >= client.settings.thresholds.bgTargetBottom; } //with predicted alarms, latestSGV may still be in target so to see if the alarm // is for a LOW we can only check if it's <= the top of the target function isAlarmForLow() { - return client.latestSGV.mgdl <= client.settings.thresholds.bgTargetTop; + return client.latestSGV && client.latestSGV.mgdl <= client.settings.thresholds.bgTargetTop; } socket.on('announcement', function (notify) { @@ -835,8 +905,7 @@ client.init = function init(serverSettings, plugins) { if (serverSettings.apiEnabled) { $('.serverSettings').show(); } - $('#treatmentDrawerToggle').toggle(client.settings.showPlugins.indexOf('careportal') > -1); - $('#boluscalcDrawerToggle').toggle(client.settings.showPlugins.indexOf('boluscalc') > -1); + // hide food control if not enabled $('.foodcontrol').toggle(client.settings.enable.indexOf('food') > -1); // hide cob control if not enabled diff --git a/lib/hashauth.js b/lib/hashauth.js index 70441bc105e..4fbdc69e1fc 100644 --- a/lib/hashauth.js +++ b/lib/hashauth.js @@ -17,12 +17,11 @@ hashauth.init = function init(client, $) { } hashauth.verifyAuthentication = function verifyAuthentication(next) { + hashauth.authenticated = false; $.ajax({ method: 'GET' , url: '/api/v1/verifyauth?t=' + Date.now() //cache buster - , headers: { - 'api-secret': hashauth.apisecrethash - } + , headers: client.headers() }).done(function verifysuccess (response) { if (response.message === 'OK') { hashauth.authenticated = true; @@ -50,12 +49,18 @@ hashauth.init = function init(client, $) { }; hashauth.removeAuthentication = function removeAuthentication(event) { + $.localStorage.remove('apisecrethash'); + + if (hashauth.authenticated) { + client.browserUtils.reload(); + } + + // clear eveything just in case hashauth.apisecret = null; hashauth.apisecrethash = null; hashauth.authenticated = false; - $('#authentication_placeholder').html(hashauth.inlineCode()); - hashauth.updateSocketAuth(); + if (event) { event.preventDefault(); } @@ -73,19 +78,23 @@ hashauth.init = function init(client, $) { text: translate('Update') , click: function() { var dialog = this; - $( dialog ).dialog( 'close' ); - hashauth.processSecret($('#apisecret').val(),$('#storeapisecret').is(':checked')); - } - } - , { - text: translate('Remove') - , click: function () { - $( this ).dialog( 'close' ); - hashauth.removeAuthentication(); + hashauth.processSecret($('#apisecret').val(), $('#storeapisecret').is(':checked'), function done (close) { + if (close) { + client.afterAuth(true); + $( dialog ).dialog( 'close' ); + } else { + $('#apisecret').val('').focus(); + } + }); } } ] , open: function open ( ) { + $('#requestauthenticationdialog').keypress(function pressed (e) { + if (e.keyCode === $.ui.keyCode.ENTER) { + $(this).parent().find('button.ui-button-text-only').trigger('click'); + } + }); $('#apisecret').val('').focus(); } @@ -96,13 +105,16 @@ hashauth.init = function init(client, $) { return false; }; - hashauth.processSecret = function processSecret(apisecret, storeapisecret) { + hashauth.processSecret = function processSecret(apisecret, storeapisecret, callback) { var translate = client.translate; hashauth.apisecret = apisecret; hashauth.storeapisecret = storeapisecret; if (hashauth.apisecret.length < 12) { window.alert(translate('Too short API secret')); + if (callback) { + callback(false); + } } else { var shasum = crypto.createHash('sha1'); shasum.update(hashauth.apisecret); @@ -115,8 +127,14 @@ hashauth.init = function init(client, $) { } $('#authentication_placeholder').html(hashauth.inlineCode()); hashauth.updateSocketAuth(); + if (callback) { + callback(true); + } } else { alert(translate('Wrong API secret')); + if (callback) { + callback(false); + } } }); } @@ -125,6 +143,18 @@ hashauth.init = function init(client, $) { hashauth.inlineCode = function inlineCode() { var translate = client.translate; + var status = null; + + if (client.authorized) { + status = translate('Authorized by token') + ' (' + translate('view without token') + ')' + + '
' + client.authorized.sub + ': ' + client.authorized.permissionGroups.join(', ') + ''; + } else if (hashauth.isAuthenticated()) { + console.info('status isAuthenticated', hashauth); + status = translate('Admin authorized') + ' (' + translate('Remove') + ')'; + } else { + status = translate('Not authorized') + ' (' + translate('Authenticate') + ')'; + } + var html = ''+ ''+ - '
'+ - (hashauth.isAuthenticated() ? - translate('Device authenticated') + ' (' + translate('Remove') + ')' - : - translate('Device not authenticated') + ' (' + translate('Authenticate') + ')' - )+ - '
'; + '
' + status + '
'; return html; }; @@ -151,11 +175,12 @@ hashauth.init = function init(client, $) { 'authorize' , { client: 'web' - , secret: hashauth.hash() + , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() + , token: client.authorized && client.authorized.token } , function authCallback(data) { console.log('Client rights: ',data); - if (!data.read) { + if (!data.read && !client.authorized) { hashauth.requestAuthentication(); } } diff --git a/lib/language.js b/lib/language.js index f248682daac..d06e1b01f5e 100644 --- a/lib/language.js +++ b/lib/language.js @@ -18,6 +18,7 @@ function init() { , { code: 'he', language: 'עברית' } , { code: 'hr', language: 'Hrvatski' } , { code: 'it', language: 'Italiano' } + , { code: 'nl', language: 'Nederlands' } , { code: 'nb', language: 'Norsk (Bokmål)' } , { code: 'nl', language: 'Nederlands' } , { code: 'pl', language: 'Polski' } diff --git a/lib/middleware/index.js b/lib/middleware/index.js index 8cdcb1b18b2..d7c96c6db79 100644 --- a/lib/middleware/index.js +++ b/lib/middleware/index.js @@ -1,7 +1,6 @@ 'use strict'; var wares = { - verifyAuthorization : require('./verify-token'), sendJSONStatus : require('./send-json-status'), bodyParser : require('body-parser'), compression : require('compression') @@ -13,7 +12,6 @@ function extensions (list) { function configure (env) { return { - verifyAuthorization: wares.verifyAuthorization(env), sendJSONStatus: wares.sendJSONStatus( ), bodyParser: wares.bodyParser, compression: wares.compression, diff --git a/lib/middleware/verify-token.js b/lib/middleware/verify-token.js deleted file mode 100644 index 9e85e22cb03..00000000000 --- a/lib/middleware/verify-token.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var consts = require('../constants'); - -function configure (env) { - function verifyAuthorization(req, res, next) { - // Retrieve the secret values to be compared. - var api_secret = env.api_secret; - - var secret = req.params.secret ? req.params.secret : req.header('api-secret'); - - // try to get the scret from the body, but don't leave it there - if (!secret && req.body) { - if (_.isArray(req.body) && req.body.length > 0) { - secret = req.body[0].secret; - delete req.body[0].secret; - } else { - secret = req.body.secret; - delete req.body.secret; - } - } - - // Return an error message if the authorization fails. - var authorized = (api_secret && api_secret.length > 12) ? (secret === api_secret) : false; - if (!authorized) { - res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'api-secret Request Header is incorrect or missing.'); - } else { - next(); - } - - return authorized; - } - return verifyAuthorization; - -} -module.exports = configure; diff --git a/lib/pebble.js b/lib/pebble.js index ad76050c56c..f2eddb03756 100644 --- a/lib/pebble.js +++ b/lib/pebble.js @@ -171,6 +171,7 @@ function pebble (req, res) { } function configure (env, ctx) { + var wares = require('./middleware/')(env); function middle (req, res, next) { req.env = env; req.ctx = ctx; @@ -182,7 +183,7 @@ function configure (env, ctx) { next( ); } - return [middle, pebble]; + return [middle, wares.sendJSONStatus, ctx.authorization.isPermitted('api:pebble,entries:read'), pebble]; } configure.pebble = pebble; diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 734f7b33c02..92bde1b5717 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -30,6 +30,7 @@ function init() { , require('./careportal')() , require('./pump')() , require('./openaps')() + , require('./loop')() , require('./boluswizardpreview')() , require('./cannulaage')() , require('./sensorage')() @@ -50,6 +51,7 @@ function init() { , require('./cob')() , require('./pump')() , require('./openaps')() + , require('./loop')() , require('./boluswizardpreview')() , require('./cannulaage')() , require('./sensorage')() @@ -177,4 +179,4 @@ function init() { } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index e2ad9c30c1b..d47c8a73e05 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -75,7 +75,7 @@ function init() { iobObj = _.isArray(devicestatusEntry.openaps.iob) ? devicestatusEntry.openaps.iob[0] : devicestatusEntry.openaps.iob; // array could still be empty, handle as null - if (iobObj === undefined) { + if (_.isEmpty(iobObj)) { return {}; } diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js new file mode 100644 index 00000000000..bdf0de15f2f --- /dev/null +++ b/lib/plugins/loop.js @@ -0,0 +1,378 @@ +'use strict'; + +var _ = require('lodash'); +var moment = require('moment'); +var times = require('../times'); +var levels = require('../levels'); +var timeago = require('./timeago')(); + +// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'freq', 'rssi']; Unused variable + +function init() { + + var loop = { + name: 'loop' + , label: 'Loop' + , pluginType: 'pill-status' + }; + + var firstPrefs = true; + + loop.getPrefs = function getPrefs(sbx) { + + var prefs = { + warn: sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 30 + , urgent: sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 60 + , enableAlerts: sbx.extendedSettings.enableAlerts + }; + + if (firstPrefs) { + firstPrefs = false; + console.info(' Prefs:', prefs); + } + + return prefs; + }; + + loop.setProperties = function setProperties (sbx) { + sbx.offerProperty('loop', function setLoop ( ) { + return loop.analyzeData(sbx); + }); + }; + + loop.analyzeData = function analyzeData (sbx) { + var recentHours = 6; + var recentMills = sbx.time - times.hours(recentHours).msecs; + + var recentData = _.chain(sbx.data.devicestatus) + .filter(function (status) { + return ('loop' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills; + }).value( ); + + var prefs = loop.getPrefs(sbx); + var recent = moment(sbx.time).subtract(prefs.warn / 2, 'minutes'); + + function getDisplayForStatus (status) { + + var desc = { + symbol: '⚠' + , code: 'warning' + , label: 'Warning' + }; + + if (!status) { + return desc; + } + + if (status.failureReason || (status.enacted && !status.enacted.received)) { + desc.symbol = 'x'; + desc.code = 'error'; + desc.label = 'Error'; + } else if (status.enacted && moment(status.timestamp).isAfter(recent)) { + desc.symbol = '⌁'; + desc.code = 'enacted'; + desc.label = 'Enacted'; + } else if (status.recommendedTempBasal && moment(status.recommendedTempBasal.timestamp).isAfter(recent)) { + desc.symbol = '⏀'; + desc.code = 'recommendation'; + desc.label = 'Recomendation'; + } else if (status.moment && status.moment.isAfter(recent)) { + desc.symbol = '↻'; + desc.code = 'looping'; + desc.label = 'Looping'; + } + return desc; + } + + var result = { + lastLoop: null + , lastEnacted: null + , lastPredicted: null + , lastOkMoment: null + }; + + function assignLastEnacted (loopStatus) { + var enacted = loopStatus.enacted; + if (enacted && enacted.timestamp) { + enacted.moment = moment(enacted.timestamp); + if (!result.lastEnacted || enacted.moment.isAfter(result.lastEnacted.moment)) { + result.lastEnacted = enacted; + } + } + } + + function assignLastPredicted (loopStatus) { + if (loopStatus.predicted && loopStatus.predicted.startDate) { + result.lastPredicted = loopStatus.predicted; + } + } + + function assignLastLoop (loopStatus) { + if (!result.lastLoop || loopStatus.moment.isAfter(result.lastLoop.moment)) { + result.lastLoop = loopStatus; + } + } + + function assignLastOkMoment (loopStatus) { + if (!loopStatus.failureReason && (!result.lastOkMoment || loopStatus.moment.isAfter(result.lastOkMoment))) { + result.lastOkMoment = loopStatus.moment; + } + } + + _.forEach(recentData, function eachStatus (status) { + if (status && status.loop && status.loop.timestamp) { + var loopStatus = status.loop; + loopStatus.moment = moment(loopStatus.timestamp); + assignLastEnacted(loopStatus); + assignLastLoop(loopStatus); + assignLastPredicted(loopStatus); + assignLastOkMoment(loopStatus); + } + }); + + result.display = getDisplayForStatus(result.lastLoop); + + return result; + }; + + loop.checkNotifications = function checkNotifications(sbx) { + var prefs = loop.getPrefs(sbx); + + if (!prefs.enableAlerts) { return; } + + var prop = sbx.properties.loop; + + if (!prop.lastLoop) { + console.info('Loop hasn\'t reported a loop yet'); + return; + } + + var now = moment(); + var level = statusLevel(prop, prefs, sbx); + if (level >= levels.WARN) { + sbx.notifications.requestNotify({ + level: level + , title: 'Loop isn\'t looping' + , message: 'Last Loop: ' + formatAgo(prop.lastLoop.moment, now.valueOf()) + , pushoverSound: 'echo' + , group: 'Loop' + , plugin: loop + , debug: prop + }); + } + }; + + loop.updateVisualisation = function updateVisualisation (sbx) { + var prop = sbx.properties.loop; + + var prefs = loop.getPrefs(sbx); + + function valueString (prefix, value) { + return (value != null) ? prefix + value : ''; + } + + var events = [ ]; + + function addRecommendedTempBasal() { + if (prop.lastLoop && prop.lastLoop.recommendedTempBasal) { + + var recommendedTempBasal = prop.lastLoop.recommendedTempBasal; + + var valueParts = [ + 'Suggested Temp: ' + recommendedTempBasal.rate + 'U/hour for ' + + recommendedTempBasal.duration + 'm' + ]; + + valueParts = concatIOB(valueParts); + valueParts = concatCOB(valueParts); + + events.push({ + time: moment(recommendedTempBasal.timestamp) + , value: valueParts.join('') + }); + } + } + + function addLastEnacted() { + if (prop.lastEnacted) { + var canceled = prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0; + + var valueParts = [ + , 'Temp Basal' + (canceled ? ' Canceled' : ' Started') + '' + , canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + 'U/hour for ' + prop.lastEnacted.duration + 'm' + , valueString(', ', prop.lastEnacted.reason) + ]; + + valueParts = concatIOB(valueParts); + valueParts = concatCOB(valueParts); + + events.push({ + time: prop.lastEnacted.moment + , value: valueParts.join('') + }); + } + } + + function concatIOB (valueParts) { + if (prop.lastLoop && prop.lastLoop.iob) { + var iob = prop.lastLoop.iob; + valueParts = valueParts.concat([ + ', IOB: ' + , sbx.roundInsulinForDisplayFormat(iob.iob) + 'U' + , iob.basaliob ? ', Basal IOB ' + sbx.roundInsulinForDisplayFormat(iob.basaliob) + 'U' : '' + ]); + } + + return valueParts; + } + + function concatCOB (valueParts) { + if (prop.lastLoop && prop.lastLoop.cob) { + cob = prop.lastLoop.cob; + valueParts = valueParts.concat([ + ', COB: ' + , cob.cob + 'g' + ]); + } + + return valueParts; + } + + function getForecastPoints ( ) { + var points = [ ]; + + function toPoints (startTime, offset) { + return function toPoint (value, index) { + return { + mgdl: value + , color: '#ff00ff' + , mills: startTime.valueOf() + times.mins(5 * index).msecs + offset + , noFade: true + }; + }; + } + + if (prop.lastPredicted) { + var predicted = prop.lastPredicted; + var startTime = moment(predicted.startDate); + if (predicted.values) { + points = points.concat(_.map(predicted.values, toPoints(startTime, 0))); + } + // if (prop.lastPredBGs.IOB) { + // points = points.concat(_.map(prop.lastPredBGs.IOB, toPoints(moment, 3000))); + // } + // if (prop.lastPredBGs.COB) { + // points = points.concat(_.map(prop.lastPredBGs.COB, toPoints(moment, 7000))); + // } + } + + return points; + } + + if ('error' === prop.display.code) { + events.push({ + time: prop.lastLoop.moment + , value: valueString('Error: ', prop.lastLoop.failureReason) + }); + addRecommendedTempBasal(); + } else if ('enacted' === prop.display.code) { + addLastEnacted(); + } else if ('looping' === prop.display.code) { + addLastEnacted(); + } else { + addRecommendedTempBasal(); + } + + var sorted = _.sortBy(events, function toMill(event) { + return event.time.valueOf(); + }).reverse(); + + var info = _.map(sorted, function eventToInfo (event) { + return { + label: timeAt(false, sbx) + timeFormat(event.time, sbx) + , value: event.value + }; + }); + + var loopName = 'Loop'; + + if (prop.lastLoop && prop.lastLoop.name) { + loopName = prop.lastLoop.name; + } + + var label = loopName + ' ' + prop.display.symbol; + + var lastLoopMoment = prop.lastLoop ? prop.lastLoop.moment : null; + + sbx.pluginBase.updatePillText(loop, { + value: timeFormat(lastLoopMoment, sbx) + , label: label + , info: info + , pillClass: statusClass(prop, prefs, sbx) + }); + + var forecastPoints = getForecastPoints(); + if (forecastPoints && forecastPoints.length > 0) { + sbx.pluginBase.addForecastPoints(forecastPoints, {type: 'loop', label: 'Loop Forecasts'}); + } + }; + + function statusClass (prop, prefs, sbx) { + var level = statusLevel(prop, prefs, sbx); + var cls = 'current'; + + if (level === levels.WARN) { + cls = 'warn'; + } else if (level === levels.URGENT) { + cls = 'urgent'; + } + + return cls; + } + + function statusLevel (prop, prefs, sbx) { + var level = levels.NONE; + var now = moment(sbx.time); + + if (prop.lastOkMoment) { + var urgentTime = prop.lastOkMoment.clone().add(prefs.urgent, 'minutes'); + var warningTime = prop.lastOkMoment.clone().add(prefs.warn, 'minutes'); + + if (urgentTime.isBefore(now)) { + level = levels.URGENT; + } else if (warningTime.isBefore(now)) { + level = levels.WARN; + } + } + + return level; + } + + return loop; + +} + +function timeFormat (m, sbx) { + + var when; + if (m && sbx.data.inRetroMode) { + when = m.format('LT'); + } else if (m) { + when = formatAgo(m, sbx.time); + } else { + when = 'unknown'; + } + + return when; +} + +function formatAgo (m, nowMills) { + var ago = timeago.calcDisplay({mills: m.valueOf()}, nowMills); + return (ago.value ? ago.value : '') + ago.shortLabel + (ago.shortLabel.length === 1 ? ' ago' : ''); +} + +function timeAt (prefix, sbx) { + return sbx.data.inRetroMode ? (prefix ? ' ' : '') + '@ ' : (prefix ? ', ' : ''); +} + +module.exports = init; diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 2fd1897ab5c..7cab06e2e94 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -115,6 +115,13 @@ function init ( ) { } } }); + + if (result.extended) { + info.push({label: '------------', value: ''}); + _.forOwn(result.extended, function(value, key) { + info.push({ label: key, value: value }); + }); + } sbx.pluginBase.updatePillText(pump, { value: values.join(' ') @@ -232,6 +239,7 @@ function prepareData (prop, prefs, sbx) { level: levels.NONE , clock: pump.clock ? { value: moment(pump.clock) } : null , reservoir: pump.reservoir || pump.reservoir === 0 ? { value: pump.reservoir } : null + , extended: pump.extended || null }; updateClock(prefs, result, sbx); diff --git a/lib/query.js b/lib/query.js index f356fa9d613..0b5de5601b9 100644 --- a/lib/query.js +++ b/lib/query.js @@ -56,7 +56,7 @@ function default_options (opts) { function enforceDateFilter (query, opts) { var dateValue = query[opts.dateField]; - if (!dateValue && !query.dateString) { + if (!dateValue && !query.dateString && true !== opts.noDateFilter) { var minDate = Date.now( ) - opts.deltaAgo; query[opts.dateField] = { $gte: opts.useEpoch ? minDate : new Date(minDate).toISOString() diff --git a/lib/report_plugins/treatments.js b/lib/report_plugins/treatments.js index 6ec52807b41..ebc9e06b75e 100644 --- a/lib/report_plugins/treatments.js +++ b/lib/report_plugins/treatments.js @@ -139,9 +139,7 @@ treatments.report = function report_treatments(datastorage, sorteddaystoshow, op $.ajax({ method: 'DELETE' , url: '/api/v1/treatments/' + data._id - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function treatmentDeleted (response) { console.info('treatment deleted', response); }).fail(function treatmentDeleteFail (response) { @@ -249,9 +247,7 @@ treatments.report = function report_treatments(datastorage, sorteddaystoshow, op $.ajax({ method: 'PUT' , url: '/api/v1/treatments/' - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() , data: data }).done(function treatmentSaved (response) { console.info('treatment saved', response); diff --git a/lib/settings.js b/lib/settings.js index c3015f9f9be..554c3507a51 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -34,6 +34,7 @@ function init ( ) { , focusHours: 3 , heartbeat: 60 , baseURL: '' + , authDefaultRoles: 'readable' , thresholds: { bgHigh: 260 , bgTargetTop: 180 @@ -43,11 +44,18 @@ function init ( ) { }; var valueMappers = { - alarmUrgentHighMins: mapNumberArray + nightMode: mapTruthy + , alarmUrgentHigh: mapTruthy + , alarmUrgentHighMins: mapNumberArray + , alarmHigh: mapTruthy , alarmHighMins: mapNumberArray + , alarmLow: mapTruthy , alarmLowMins: mapNumberArray + , alarmUrgentLow: mapTruthy , alarmUrgentLowMins: mapNumberArray , alarmUrgentMins: mapNumberArray + , alarmTimeagoWarn: mapTruthy + , alarmTimeagoUrgent: mapTruthy , alarmWarnMins: mapNumberArray , timeFormat: mapNumber }; @@ -79,6 +87,12 @@ function init ( ) { } } + function mapTruthy (value) { + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + return value; + } + //TODO: getting sent in status.json, shouldn't be settings.DEFAULT_FEATURES = ['delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile']; @@ -296,4 +310,4 @@ function init ( ) { } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/websocket.js b/lib/websocket.js index f09e58a2683..9619c53bec7 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -74,20 +74,24 @@ function init (env, ctx, server) { 'browser client gzip': false }); } - - function verifyAuthorization (secret) { - var read, write, write_treatment; - - // read now true by default - read = true; - write = (secret === env.api_secret); - write_treatment = !env.treatments_auth || (secret === env.api_secret); - - return { - read: read - , write: write - , write_treatment: write_treatment - }; + + function verifyAuthorization (message, callback) { + ctx.authorization.resolve({ api_secret: message.secret, token: message.token }, function resolved (err, result) { + + if (err) { + return callback( err, { + read: false + , write: false + , write_treatment: false + }); + } + + return callback(null, { + read: ctx.authorization.checkMultiple('api:*:read', result.shiros) + , write: ctx.authorization.checkMultiple('api:*:create,update,delete', result.shiros) + , write_treatment: ctx.authorization.checkMultiple('api:treatments:create,update,delete', result.shiros) + }); + }); } function emitData (delta) { @@ -393,42 +397,44 @@ function init (env, ctx, server) { // [, status : true ] // } socket.on('authorize', function authorize (message, callback) { - socketAuthorization = verifyAuthorization(message.secret); - clientType = message.client; - history = message.history || 48; //default history is 48 hours - pingme = message.pingme; - - if (socketAuthorization.read) { - socket.join('DataReceivers'); - if (pingme) { - socket.join('PingReceivers'); - } - // send all data upon new connection - if (lastData && lastData.split) { - var split = lastData.split(Date.now(), times.hours(3).msecs, times.hours(history).msecs); - if (message.status) { - split.first.status = status(split.first.profiles); + verifyAuthorization(message, function verified (err, authorization) { + socketAuthorization = authorization; + clientType = message.client; + history = message.history || 48; //default history is 48 hours + pingme = message.pingme; + + if (socketAuthorization.read) { + socket.join('DataReceivers'); + if (pingme) { + socket.join('PingReceivers'); } - //send out first chunk - socket.emit('dataUpdate', split.first); + // send all data upon new connection + if (lastData && lastData.split) { + var split = lastData.split(Date.now(), times.hours(3).msecs, times.hours(history).msecs); + if (message.status) { + split.first.status = status(split.first.profiles); + } + //send out first chunk + socket.emit('dataUpdate', split.first); - //then send out the rest - setTimeout(function sendTheRest() { - split.rest.delta = true; - socket.emit('dataUpdate', split.rest); - }, 500); + //then send out the rest + setTimeout(function sendTheRest() { + split.rest.delta = true; + socket.emit('dataUpdate', split.rest); + }, 500); - setTimeout(function sendTheLast() { - split.last.delta = true; - socket.emit('dataUpdate', split.last); - }, 10000); + setTimeout(function sendTheLast() { + split.last.delta = true; + socket.emit('dataUpdate', split.last); + }, 10000); + } } - } - console.log(LOG_WS + 'Authetication ID: ',socket.client.id, ' client: ', clientType, ' history: ' + history); - if (callback) { - callback(socketAuthorization); - } + console.log(LOG_WS + 'Authetication ID: ', socket.client.id, ' client: ', clientType, ' history: ' + history); + if (callback) { + callback(socketAuthorization); + } + }); }); // Pind message diff --git a/package.json b/package.json index 8bbbc13ebea..449ae689f38 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "forever": "~0.13.0", "git-rev": "git://github.com/bewest/git-rev.git", "jquery": "^2.1.4", + "jsonwebtoken": "^7.1.7", "lodash": "^4.0.0", "long": "~2.2.3", "mfb": "^0.12.0", @@ -77,6 +78,7 @@ "request": "^2.58.0", "sgvdata": "git://github.com/ktind/sgvdata.git#wip/protobuf", "share2nightscout-bridge": "git://github.com/bewest/share2nightscout-bridge.git#wip/generalize", + "shiro-trie": "^0.3.7", "simple-statistics": "~0.7.0", "socket.io": "^1.3.5", "sugar": "^1.4.1", diff --git a/static/admin/index.html b/static/admin/index.html index 7b040f71fec..5257ab33a14 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -45,7 +45,7 @@

Admin Tools

Authentication status: - + diff --git a/static/css/drawer.css b/static/css/drawer.css index 96499e995ba..6414d56b8c4 100644 --- a/static/css/drawer.css +++ b/static/css/drawer.css @@ -46,6 +46,7 @@ input[type=number]:invalid { display: block; text-align: right; padding: 10px 0; + text-decoration: underline; } #treatmentDrawer { @@ -152,6 +153,7 @@ input[type=number]:invalid { } #about a { color: #fff; + text-decoration: underline; } form { @@ -367,4 +369,4 @@ ul.navigation { #boluscalcDrawer .foodinput { background: #505050; margin: 10px; -} \ No newline at end of file +} diff --git a/static/css/main.css b/static/css/main.css index cf2349d61af..39f6021459c 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -23,6 +23,11 @@ body { z-index: 2; } +a, a:visited, a:link { + color: #2196f3; + text-decoration: none; +} + #container.announcing .customTitle { color: orange; } @@ -340,6 +345,15 @@ body { display: none; } +#authorizationstatus a { + color: #2196f3; + text-decoration: underline; +} + +#authorizationstatus .small { + font-size: 12px; +} + @media (max-width: 800px) { .bgStatus { width: 300px; @@ -662,4 +676,4 @@ body { font-size: 12px; line-height: 13px; } -} +} \ No newline at end of file diff --git a/static/food/js/food.js b/static/food/js/food.js index b7d2b7208e4..45a62546e00 100644 --- a/static/food/js/food.js +++ b/static/food/js/food.js @@ -71,7 +71,8 @@ var client = Nightscout.client; // Fetch data from mongo $('#fe_status').hide().text('Loading food database ...').fadeIn('slow'); $.ajax('/api/v1/food.json', { - success: function ajaxSuccess(records) { + headers: client.headers() + , success: function ajaxSuccess(records) { records.forEach(function processRecords(r) { restoreBoolValue(r,'hidden'); restoreBoolValue(r,'hideafteruse'); @@ -92,8 +93,8 @@ var client = Nightscout.client; }); $('#fe_status').hide().text(translate('Database loaded')).fadeIn('slow'); foodquickpick.sort(function compare(a,b) { return cmp(parseInt(a.position),parseInt(b.position)) }); - }, - error: function ajaxError() { + } + , error: function ajaxError() { $('#fe_status').hide().text(translate('Error: Database failed to load')).fadeIn('slow'); } }).done(initeditor); @@ -523,9 +524,7 @@ var client = Nightscout.client; method: 'POST', url: '/api/v1/food/', data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success (response) { $('#fe_status').hide().text('OK').fadeIn('slow'); foodrec._id = response[0]._id; @@ -547,9 +546,7 @@ var client = Nightscout.client; method: 'PUT', url: '/api/v1/food/', data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success () { $('#fe_status').hide().text('OK').fadeIn('slow'); updateFoodArray(foodrec); @@ -574,9 +571,7 @@ var client = Nightscout.client; method: 'DELETE', url: '/api/v1/food/'+_id, data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success () { $('#fe_status').hide().text('OK').fadeIn('slow'); }).fail(function fail() { @@ -597,9 +592,7 @@ var client = Nightscout.client; method: 'PUT', url: '/api/v1/food/', data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success (response) { console.log('Updated record: ',response); }); @@ -621,9 +614,7 @@ var client = Nightscout.client; method: 'POST', url: '/api/v1/food/', data: newrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success (response) { $('#fe_status').hide().text('OK').fadeIn('slow'); newrec._id = response[0]._id; diff --git a/static/glyphs/LICENSE.txt b/static/glyphs/LICENSE.txt old mode 100644 new mode 100755 index 1970f449a11..b8a9414fbb6 --- a/static/glyphs/LICENSE.txt +++ b/static/glyphs/LICENSE.txt @@ -12,7 +12,7 @@ Font license info ## Font Awesome - Copyright (C) 2012 by Dave Gandy + Copyright (C) 2016 by Dave Gandy Author: Dave Gandy License: SIL () diff --git a/static/glyphs/README.txt b/static/glyphs/README.txt old mode 100644 new mode 100755 index a91438a9a8a..beaab3366f0 --- a/static/glyphs/README.txt +++ b/static/glyphs/README.txt @@ -2,14 +2,14 @@ This webfont is generated by http://fontello.com open source project. ================================================================================ -Please, note, that you should obey original font licences, used to make this +Please, note, that you should obey original font licenses, used to make this webfont pack. Details available in LICENSE.txt file. - Usually, it's enough to publish content of LICENSE.txt file somewhere on your site in "About" section. - If your project is open-source, usually, it will be ok to make LICENSE.txt - file publically available in your repository. + file publicly available in your repository. - Fonts, used in Fontello, don't require a clickable link on your site. But any kind of additional authors crediting is welcome. diff --git a/static/glyphs/config.json b/static/glyphs/config.json old mode 100644 new mode 100755 index 27c0e45a7e5..a76965262fc --- a/static/glyphs/config.json +++ b/static/glyphs/config.json @@ -113,6 +113,24 @@ "css": "cog", "code": 59398, "src": "websymbols" + }, + { + "uid": "c1f1975c885aa9f3dad7810c53b82074", + "css": "lock", + "code": 59410, + "src": "fontawesome" + }, + { + "uid": "657ab647f6248a6b57a5b893beaf35a9", + "css": "lock-open", + "code": 59411, + "src": "fontawesome" + }, + { + "uid": "f48ae54adfb27d8ada53d0fd9e34ee10", + "css": "trash-empty", + "code": 59412, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/static/glyphs/css/animation.css b/static/glyphs/css/animation.css old mode 100644 new mode 100755 diff --git a/static/glyphs/css/fontello-codes.css b/static/glyphs/css/fontello-codes.css old mode 100644 new mode 100755 index 3aaaa9ad5b5..994b55ca64d --- a/static/glyphs/css/fontello-codes.css +++ b/static/glyphs/css/fontello-codes.css @@ -16,4 +16,7 @@ .icon-tint:before { content: '\e80e'; } /* '' */ .icon-chart-line:before { content: '\e80f'; } /* '' */ .icon-sort-numeric:before { content: '\e810'; } /* '' */ -.icon-edit:before { content: '\e811'; } /* '' */ \ No newline at end of file +.icon-edit:before { content: '\e811'; } /* '' */ +.icon-lock:before { content: '\e812'; } /* '' */ +.icon-lock-open:before { content: '\e813'; } /* '' */ +.icon-trash-empty:before { content: '\e814'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/css/fontello-embedded.css b/static/glyphs/css/fontello-embedded.css old mode 100644 new mode 100755 index 7b7f5d6fda6..1bd83157f9a --- a/static/glyphs/css/fontello-embedded.css +++ b/static/glyphs/css/fontello-embedded.css @@ -1,15 +1,15 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?46577197'); - src: url('../font/fontello.eot?46577197#iefix') format('embedded-opentype'), - url('../font/fontello.svg?46577197#fontello') format('svg'); + src: url('../font/fontello.eot?37278242'); + src: url('../font/fontello.eot?37278242#iefix') format('embedded-opentype'), + url('../font/fontello.svg?37278242#fontello') format('svg'); font-weight: normal; font-style: normal; } @font-face { font-family: 'fontello'; - src: url('data:application/octet-stream;base64,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') format('woff'), - url('data:application/octet-stream;base64,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') format('truetype'); + src: url('data:application/octet-stream;base64,d09GRgABAAAAABmIAA8AAAAAKsgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIwleU9TLzIAAAGUAAAAQwAAAFY+IUkiY21hcAAAAdgAAACmAAACiOnFAFpjdnQgAAACgAAAABMAAAAgBtX/AmZwZ20AAAKUAAAFkAAAC3CKkZBZZ2FzcAAACCQAAAAIAAAACAAAABBnbHlmAAAILAAADcEAABWEVKl1aWhlYWQAABXwAAAAMgAAADYLJgpEaGhlYQAAFiQAAAAgAAAAJAfIA/ZobXR4AAAWRAAAADoAAABYTOP/+2xvY2EAABaAAAAALgAAAC47fjW8bWF4cAAAFrAAAAAgAAAAIAFdDDxuYW1lAAAW0AAAAXcAAALNzJ0cHnBvc3QAABhIAAAAxAAAAR260xEFcHJlcAAAGQwAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYMpJLMlj4HNx8wlhkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAKVkFSAB4nGNgZK5nnMDAysDAVMW0h4GBoQdCMz5gMGRkAooysDIzYAUBaa4pDA4vGF6IMAf9z2KIYg5imA4UZgTJAQDp2AvBAHic7ZLbDQIxDATnuPA+3lwXVEJBfFHwdgHrsGUQaSzFcqJoJ8ASGM3DNBjeDNR6uTv0/siu9xvPPtOqr/vn40pV71uvC88237hizYatz+2ZOHDkxJkLV27cmT264r+mKsOU3Vxp/uj5BmeKQnlSqMQVyqGCs0fBFlCwDxRsBoVyq2BbKNTrFGwQBbtEwVZRsF8UbBoFO0fB9lHwP/Af+sH8BeUZMdoAAHicY2BAAxIQyBz0PxOEARJmA9sAeJytVml300YUHXlJnIQsJQstamHExGmwRiZswYAJQbJjIF2crZWgixQ76b7xid/gX/Nk2nPoN35a7xsvJJC053Cak6N3583VzNtlElqS2AvrkZSbL8XU1iaN7DwJ6YZNy1F8KDt7IWWKyd8FURCtltq3HYdERCJQta6wRBD7HlmaZHzoUUbLtqRXTcotPekuW+NBvVXffho6yrE7oaRmM3RoPbIlVRhVokimPVLSpmWo+itJK7y/wsxXzVDCiE4iabwZxtBI3htntMpoNbbjKIpsstwoUiSa4UEUeZTVEufkigkMygfNkPLKpxHlw/yIrNijnFawS7bT/L4vead3OT+xX29RtuRAH8iO7ODsdCVfhFtbYdy0k+0oVBF213dCbNnsVP9mj/KaRgO3KzK90IxgqXyFECs/ocz+IVktnE/5kkejWrKRE0HrZU7sSz6B1uOIKXHNGFnQ3dEJEdT9kjMM9pg+Hvzx3imWCxMCeBzLekclnAgTKWFzNEnaMHJgJWWLKqn1rpg45XVaxFvCfu3a0ZfOaONQd2I8Ww8dWzlRyfFoUqeZTJ3aSc2jKQ2ilHQmeMyvAyg/oklebWM1iZVH0zhmxoREIgIt3EtTQSw7saQpBM2jGb25G6a5di1apMkD9dyj9/TmVri501PaDvSzRn9Wp2I62AvT6WnkL/Fp2uUiRen66Rl+TOJB1gIykS02w5SDB2/9DtLL15YchdcG2O7t8yuofdZE8KQB+xvQHk/VKQlMhZhViFZAYq1rWZbJ1awWqcjUd0OaVr6s0wSKchwXx76Mcf1fMzOWmBK+34nTsyMuPXPtSwjTHHybdT2a16nFcgFxZnlOp1mW7+s0x/IDneZZntfpCEtbp6MsP9RpgeVHOh1jeUELmnTfwZCLMOQCDpAwhKUDQ1hegiEsFQxhuQhDWBZhCMslGMLyYxjCchmGsLysZdXUU0nj2plYBmxCYGKOHrnMReVqKrlUQrtoVGpDnhJulVQUz6p/ZaBePPKGObAWSJfIml8xzpWPRuX41hUtbxo7V8Cx6m8fjvY58VLWi4U/Bf/V1lQlvWLNw5Or8BuGnmwnqjapeHRNl89VPbr+X1RUWAv0G0iFWCjKsmxwZyKEjzqdhmqglUPMbMw8tOt1y5qfw/03MUIWUP34NxQaC9yDTllJWe3grNXX27LcO4NyOBMsSTE38/pW+CIjs9J+kVnKno98HnAFjEpl2GoDrRW82ScxD5neJM8EcVtRNkja2M4EiQ0c84B5850EJmHqqg3kTuGGDfgFYW7BeSdconqjLIfuRezzKKT8W6fiRPaoaIzAs9kbYa/vQspvcQwkNPmlfgxUFaGpGDUV0DRSbqgGX8bZum1Cxg70Iyp2w7Ks4sPHFveVkm0ZhHykiNWjo5/WXqJOqtx+ZhSX752+BcEgNTF/e990cZDKu1rJMkdtA1O3GpVT15pD41WH6uZR9b3j7BM5a5puuiceel/TqtvBxVwssPZtDtJSJhfU9WGFDaLLxaVQ6mU0Se+4BxgWGNDvUIqN/6v62HyeK1WF0XEk307Ut9HnYAz8D9h/R/UD0Pdj6HINLs/3mhOfbvThbJmuohfrp+g3MGutuVm6BtzQdAPiIUetjrjKDXynBnF6pLkc6SHgY90V4gHAJoDF4BPdtYzmUwCj+Yw5PsDnzGHQZA6DLeYw2GbOGsAOcxjsMofBHnMYfMGcdYAvmcMgZA6DiDkMnjAnAHjKHAZfMYfB18xh8A1z7gN8yxwGMXMYJMxhsK/p1jDMLV7QXaC2QVWgA1NPWNzD4lBTZcj+jheG/b1BzP7BIKb+qOn2kPoTLwz1Z4OY+otBTP1V050h9TdeGOrvBjH1D4OY+ky/GMtlBr+MfJcKB5RdbD7n74n3D9vFQLkAAQAB//8AD3icrVh7bFvXeT/fueeee3lJXr7uQyKpK4qUSJmiKIVPx6JpSpathxknsWmFVhWFdiQ5taw4wOJHkyBovQyBByz2gqBLhbWxgyR/BJnXdH90K9A0aFqkGArEAYo+5mB/DUOKDenQbcay2fS+e0knbubNSFY9zj33u+ec+72/33cJu3HjxlnhdSFPAmSA7CR1cv67+WRCEBnM7Pm2cl+zViJcYAJnG4SJAhPXiSRTKq0SQgROhBYRiSyJ8iFCAWiDUAoHCVC4J1Ir32ajLNGNO+08WPOEQmNR3ewxB11i30igUKVmzqKqEM/SsmbRqlAuZCGuQtks5XSN+wD/VOiDfLFQKldhB9hjuZBMxAXNyJWylM8//cahhy49VQecvI3Xk1uXz5z/aqsEyy+/9crSL/pHDzHgIt1qeE5wATQp5OEhDRq1/YH0zjRN7h6G5Zvbf3Dp6fn5py89NPPM0jZaan117qGXl5dfvn+1z2QyoNr6POGeobDAKcjMLfHAcOKu6fafxOLxWhwGCUXpCW3Sh4lORshIbbgPqCiBQOkMEal4nFCBHmcgEOE4ACFrptZjahoXwyNDGsoZHwPJHpKFMpRyJhjF21Jpk0dFlX9wRfSzKOfA8AYYvy3xpIRk8coVUYxymyCK7Wv8PYf4wQc3iZwj0WG9y/+Jm/wPgQA+IAKdEYEBO47GFOA4sQXAlWua2aOZDv+oVeQ0hUyifXL9ULYH5P92VOS/fQ3ZxJc6PF/5oMPz7YjwmGQzZzOpfsr0ezZRFDtEzq9cwT22AAIhXX/3kB4ySKZqO2RgqHomtiSg+JwKLQ4Eld/AC4EmwZt6uFf1Aon19Q6GE1rA26Oaskg84HaJ2shQzkAnTAzEk4WSCTlD4ylnXnbmSM+iPwpT6Wolcz2ZqVTT9O8ylc68krHn1fT15EgFKpnFymi6Wl2oAhyrNqpQWagAbFQPIKkCHd5x+Hv6Z8h7nOTIdG0Stc0J8rkhyJRInEjrGFUcRN4iEmNSg0gSaxImsTqQu7JbkgNWr6GFVC8u9YDH5r4cT26HQiln9IHGkWMTZUl1REgFCqX8QM4QAoUk3nMzoBl4X3pfj8XSlkXPZSpQHVmqXX9nchGWanT75OIRy7j+jh4Dy6Db9dgPLeN93bJ03AB3V0YeyVRhcgkWp2Dr5NJS+1nYalgQ09s/wTW2aOwW+XrJGNlB7q3VmYCBRGQUUXRThRGXTFyYSpAiyS3i4tzVIC4XbxLu4nXbSNWJUj47kkwMxCJhLeTtVXs7wnpsU8WT5aItrCPl55ccNmzZMlGw3vwiOvgc6sA4u/Fl4UPhfpIi42S+NjM0mIhHwr09MgVdA4Jq8ftUiYtMIoIwE8Cg242JVAAqbKCjYAJZRx/2zKJGvWRuLDuS7rfCTNRHQJe4ZEopTJmJVDIlpcrJMSiWS+UUZtMdkDcNs2xKhikVkphhMXsKH/6N4j699pLWb52L6MbF1a+45XfflTynVy/4YtFzUSP8rbVTXunlhdMN2nx8EV59Lhwxv7lyUnH/8MeS+9TKBb03+lxU73lp5ZTseecdyXNi5c8j+sOVRuN0o4HeeqvNk6SCFWix9gAXBQEUoqDZJZV5qYcTt0Lc68SFNJfSIm5ZdjeI2y03ieyW65Gwbfo9sztrE1vz49nR4dRALJyMJG91AN//6QCfPPl/eAMkHAJOFr6gW3yugOGf0V2ezJGDZLV2GMuqwMCDQpMNV4D7mY+qMvF6iHeduO205WkRr6J4G8TrVZpE8Sr14VRHhc0D990zu6u2fWJbsZAdTeWH8/9TkcE7KTL1iSJTvw+tvm1LjZPv/B61+3kUbcfi94RZgRKZaGS4NqSFggGMPq/HrbixOsxQHMhxjLc1ICLD1CQLqCIU189jSX8JyhhmGHMYWGV45dLVq+1Xrl4Ftrmw+WJzc7P54uaCQDu0pavtpc2FF1/EJ/bYrVcbmAfuxUhRiUHStZQL7HeGgorAyC5DZQJME/v9WAtgDbeIRAwIGOnFQB4CMFQqpMqGqUlmyY8Tv/TPcLj9LbgwNzfzZbo+9uST9c3N5+ACDFyzYCAx/9P6/NEX/nR17BS0npzfbNc3iX02uXEUediHzpOtjdjSCljxNxAwoZsJrJtpGPOyOVzuJu6A/SOJvcjDwCe/cATCcKT9jfY/vn9z0v4GHOnIeJSO4/kRMkwOk121qWKOUqEEjDbvoQgjERsxzkS+gT7POKY2ipiSshamPrtAf5Ln9u8LRwZTfWHJwRsqaBbYjue4ZxUQNHbcVBclbhr5XLmUQowYQnyJ4KNYRhxiav2CkTQSmkp9lCd5kfdBlRYLCDMtMIuFMYrZMgt0vHX4cOuNX75hXy797FLbCjJ56alnmqrVOnr6vBz19PYt0ZV/W2GLo6FAwGN5qMJlj89H44vy49OFqSf40kjQK3okRh/NPLJcWVrLnozv3Rs/mV1bqiw/kjkZ27s3xlWZF3KKVmrPB61BtSe6ZOVKpVxscSSk63RcCQZVxef30zgsxqZYcjxuWfHxJJuylraEVFnzGI5SbNtdRN3KiPDDJEEypEiqZIbsJ0vkGDlB/oh8rfb0g1hUNho7RplbKQ0GEf2jynUjqAqKW1daZsgnuP0eGVG8m7W8LipySgmIpEU0rbdJens9swEJbQE9TdLT4+2Ze+ZrT33l8T947Pj6Iw+3vrT4wMK+++t7du+amtxeuXtrIT8+NpLWQp3fYBghvonFhlvggHa76lQh5VBoSbyVoIIFpd990iU4E8CUBVyyTVUSbhIx/SAWiKMdO+lF6m4PBTqYrdhNQz9Pb8vQ4WryV+nKMM1szf6qew9/2SX88k4P4PX0tjRktmZ+0SX8TPNfL6mGodK/9RnQ3TafiV77qC+d7hOCfWkJJza5v3v9dyvt7P3eZ+5/e5P+mfv2Yz/vS9NsxBnh3S71vXP2S+3h1S4Fzlx7zXnnIo5OPvsPOiGMYT4ZI4O1gS5ad3IYtmENjCjaRDRB61Et3MvEnpGhQipuO34qGZcwNIDrdmM1hCGWTBXKiBvQdFVUt2kJpgGWG2QVyw7VgKHbuNwoDERf++g1GGX9W3urPjFqRZm/Ol4TRt/TQh7dFfO4XF5Lc3tCGnK4euo1+vqJ+2fFbCP5RMGvUX+w8ER9SZzp5OI32SnBjWmun+ypzfa4EbqbQb8gsACCH/RbELH/EBn2Ukxg2IlQ4ThHZE/oPrxQsmAfMk9IX0TXVK+TLTFVGSOiPhDIBzAO+Bh0WqqU05GgzKUJzOXs1H+dnRE+1JTp9kvYVJz5cB1msAs5A8JOl+z9V0mRZoXFax/Rv/6tR3HZa/iZXx+D3dxeMS0bCua5G9cQ5M7QoxiJY2SRrNUeZqBQmCGC4lIE1waRXKJLEjc8wNyAYJe2iEJckuJqYWeJib2BF5E0uQpebFlIXQsCeeDA3nvmZqendlS33V0sZNLJoVh/JBwc08ZIAAK+DsIvBySOXQhmOQPLLN8C8STO7UjQ7exYRBNir+KDfM5QAbCdQbuW7Uwp2WYuJCWnn8lV0dpof/SDcilvP/wmls8+LR1UtSPRcYjB2+1r2GG8rw1vS1vpWCH3/Gr+9LN6jEbh2fLKH69BbDgOyVIyGcHKrvsNGlZ7FSU96HenFyYacDCmQ92nqMNpOa/Gw+2WHqvhYRPDqX7DsozZfG71fP40HgxD+bndh+natH1MJJksDduAgRpxjWLOMoZDexe2Ze1CSG58LPwD+gonPnI3Zr1ZkGp6rTrhx6ZCIqyAlZTOTGLfurvzrWOcSAITJLaBdsJnqxgYjAgM1Q+Ei8AfwkaRiohaRTs+RFqP7Pm2G/dt6a7HRv7OG8wv9KLaXbduYQQ27rjn4MGDNawB0zu3V+zez4oYIdQE11wIDoYQk+h5HZIYywb6Qkj/NB2WB3KmhU09TzmZU0JgZmLmxNppO5BgQqKIwdHp1DX4l9qBWhF0l+tHriD+Dy7vbI/vXF7eCZcTlkuQIrLi9bTHhwpQGoTLQwVxUO4pX2yfvUiP5y/m/Rn/Af/3Jw9M9pfg+ZtHtN862jlgahlUFuJRmQmFoe4ZuyU8QYZzF9pnL0C2cLHg8x3wZ5y88DH9Q7S1hHkBUZKIcA0QqtANQumjtpZZAyGKDRcY1BOhoVLIb3+WCA1ggVDBFDEupC4sNfMOZkUF5Yy/gr7GyQbA5Zhx/deolpgReOGnX6dBnL56bKJB79t+sf2WgxZhyojBsbUXXlg7ZjmY6WOsux4yRMZro8xmBZMQGs+GTusIXB4lIoBjLpsnEeqhoWJCH3Q+laAxpISDWpA1hw3zVvaQ5SoYwkVDHVSNfaf3QdFhrMsfnHr+8nkaOOfTAKuAw+Ixy/wdJo88T7/u9A833hSCqDMf6myMTJCd5AhZqR16YBfl8paB3oALOHI9g7rsfmZg9OZnBhU7cdXV8nmp7FEoB5kvE8ntdj45uJvELbnrqyuHlr90cP++e+tzM5M7tEEtaf8k/FjwAcUZgY672cnVvMN9CPEjlnUs1FWAPPatCS6Jur2mW8ZTgY6+cLX97Q811m9nM7AUeVBWnOHcp9PnFKkzlZTZtkfGnvoyleX22f+MMPFNzuCfFLnU9biive6NlCtjfMdMu1J/ISvw3fYPbCJM2uP/Mm+v0MD133g0RdHo6qRt6f34xuu/yU5PZWnIYeJBHXsc7UHlvwFt9oFbAAAAeJxjYGRgYABin/13C+L5bb4ycDO/AIowXD75OAhG///zP5OlnBnE52BgAokCAJl3Ds8AAHicY2BkYGAO+p/FwMBS9v/P//8s5QxAERQgBgCjFAbFeJxjfsHAwLzy/3+mJgYGEGZeBcT3oPgFEtsDygdiJgMobc3AwFIGxO7//4D5IP0LgFiQgQEAfdISGwAAAAAAAAC4ARgBeAHWAkYCzANAA94EkgTQBRIFQgXwBvoHWAe2CG4JZAmsCf4KwgAAAAEAAAAWAIYADQAAAAAAAgA0AEQAcwAAAJILcAAAAAB4nHWQy07CQBSG/5GLCokaTdw6KwMxlksiCxISEgxsdEMMW1NKaUtKh0wHEl7Dd/BhfAmfxZ92MAZim+l855szZ04HwDW+IZA/Txw5C5wxyvkEp+hZLtA/Wy6SXyyXUMWb5TL9u+UKHhBYruIGH6wgiueMFvi0LHAlLi2f4ELcWS7QP1ouknuWS7gVr5bL9J7lCiYitVzFvfgaqNVWR0FoZG1Ql+1mqyOnW6moosSNpbs2odKp7Mu5Sowfx8rx1HLPYz9Yx67eh/t54us0UolsOc29GvmJr13jz3bV003QNmYu51ot5dBmyJVWC98zTmjMqtto/D0PAyissIVGxKsKYSBRo61zbqOJFjqkKTMkM/OsCAlcxDQu1twRZisp4z7HnFFC6zMjJjvw+F0e+TEp4P6YVfTR6mE8Ie3OiDIv2ZfD7g6zRqQky3QzO/vtPcWGp7VpDXftutRZVxLDgxqS97FbW9B49E52K4a2iwbff/7vB+NphE8AeJxtjetKAzEUhDNtrO7FXrzUpwiswuLzpGcPu8FsEnJR+vauSAuC82PmGxgYsRK/qsX/OmKFNSRusMEt7lChRoMW99hihz0OeMAjnvCMI15EO7ENikwky8NOu9GyGnw5LVHC4U8f/JeTM7tSn3TOHM/qrb9i363Jj9f63jcXfO26LWlHbC83m09vy8wy2JKqyZc4Wp2SJG1JZuNyTZOOWVnjuE1+IbesoyHJg8nSevqofkz5wK7JUadJ8RzyWYhvVYpJbnicY/DewXAiKGIjI2Nf5AbGnRwMHAzJBRsZWJ02MTAyaIEYm7mYGDkgLD4GMIvNaRfTAaA0J5DN7rSLwQHCZmZw2ajC2BEYscGhI2Ijc4rLRjUQbxdHAwMji0NHckgESEkkEGzmYWLk0drB+L91A0vvRiYGFwAMdiP0AAA=') format('woff'), + url('data:application/octet-stream;base64,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LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=') format('truetype'); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -17,7 +17,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?46577197#fontello') format('svg'); + src: url('../font/fontello.svg?37278242#fontello') format('svg'); } } */ @@ -69,4 +69,7 @@ .icon-tint:before { content: '\e80e'; } /* '' */ .icon-chart-line:before { content: '\e80f'; } /* '' */ .icon-sort-numeric:before { content: '\e810'; } /* '' */ -.icon-edit:before { content: '\e811'; } /* '' */ \ No newline at end of file +.icon-edit:before { content: '\e811'; } /* '' */ +.icon-lock:before { content: '\e812'; } /* '' */ +.icon-lock-open:before { content: '\e813'; } /* '' */ +.icon-trash-empty:before { content: '\e814'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/css/fontello-ie7-codes.css b/static/glyphs/css/fontello-ie7-codes.css old mode 100644 new mode 100755 index c7b31ffb299..78423d3d1d6 --- a/static/glyphs/css/fontello-ie7-codes.css +++ b/static/glyphs/css/fontello-ie7-codes.css @@ -16,4 +16,7 @@ .icon-tint { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-sort-numeric { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file +.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file diff --git a/static/glyphs/css/fontello-ie7.css b/static/glyphs/css/fontello-ie7.css old mode 100644 new mode 100755 index dcf347dde8b..00e81dcf09c --- a/static/glyphs/css/fontello-ie7.css +++ b/static/glyphs/css/fontello-ie7.css @@ -27,4 +27,7 @@ .icon-tint { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-sort-numeric { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file +.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file diff --git a/static/glyphs/css/fontello.css b/static/glyphs/css/fontello.css old mode 100644 new mode 100755 index c3be61027e7..777b4dc367b --- a/static/glyphs/css/fontello.css +++ b/static/glyphs/css/fontello.css @@ -1,10 +1,11 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?28973419'); - src: url('../font/fontello.eot?28973419#iefix') format('embedded-opentype'), - url('../font/fontello.woff?28973419') format('woff'), - url('../font/fontello.ttf?28973419') format('truetype'), - url('../font/fontello.svg?28973419#fontello') format('svg'); + src: url('../font/fontello.eot?25902905'); + src: url('../font/fontello.eot?25902905#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?25902905') format('woff2'), + url('../font/fontello.woff?25902905') format('woff'), + url('../font/fontello.ttf?25902905') format('truetype'), + url('../font/fontello.svg?25902905#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -14,7 +15,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?28973419#fontello') format('svg'); + src: url('../font/fontello.svg?25902905#fontello') format('svg'); } } */ @@ -71,4 +72,7 @@ .icon-tint:before { content: '\e80e'; } /* '' */ .icon-chart-line:before { content: '\e80f'; } /* '' */ .icon-sort-numeric:before { content: '\e810'; } /* '' */ -.icon-edit:before { content: '\e811'; } /* '' */ \ No newline at end of file +.icon-edit:before { content: '\e811'; } /* '' */ +.icon-lock:before { content: '\e812'; } /* '' */ +.icon-lock-open:before { content: '\e813'; } /* '' */ +.icon-trash-empty:before { content: '\e814'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/demo.html b/static/glyphs/demo.html old mode 100644 new mode 100755 index 9685f949753..c4efb3c641a --- a/static/glyphs/demo.html +++ b/static/glyphs/demo.html @@ -229,11 +229,11 @@ } @font-face { font-family: 'fontello'; - src: url('./font/fontello.eot?12833974'); - src: url('./font/fontello.eot?12833974#iefix') format('embedded-opentype'), - url('./font/fontello.woff?12833974') format('woff'), - url('./font/fontello.ttf?12833974') format('truetype'), - url('./font/fontello.svg?12833974#fontello') format('svg'); + src: url('./font/fontello.eot?94753240'); + src: url('./font/fontello.eot?94753240#iefix') format('embedded-opentype'), + url('./font/fontello.woff?94753240') format('woff'), + url('./font/fontello.ttf?94753240') format('truetype'), + url('./font/fontello.svg?94753240#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -279,7 +279,7 @@ + diff --git a/static/js/client.js b/static/js/client.js index c29ebf8b204..064acda222e 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -1,4 +1,4 @@ -'use strict'; +// don't 'use strict' since serverSettings might not get set if status.js fails if (serverSettings === undefined) { console.error('server settings were not loaded, will not call init'); diff --git a/static/js/init.js b/static/js/init.js new file mode 100644 index 00000000000..0b7a1ff9701 --- /dev/null +++ b/static/js/init.js @@ -0,0 +1,29 @@ +// don't 'use strict' since we're setting serverSettings globally +serverSettings = { + 'status':'unauthorized', + 'name':'Nightscout', + 'version':'unknown', + 'apiEnabled':false, + 'careportalEnabled':false, + 'boluscalcEnabled':false, + 'settings':{ }, + 'extendedSettings':{ }, + 'authorized':null +}; + +var params = {}; +if (window.location.search) { + window.location.search.substr(1).split('&').forEach(function(item) { + params[item.split('=')[0]] = item.split('=')[1].replace(/[_\+]/g, ' '); + }); +} + +var token = params.token; + +var script = window.document.createElement('script'); +var src = '/api/v1/status.js?t=' + new Date().getTime(); +if (token) { + src += '&token=' + token; +} +script.setAttribute('src', src); +window.document.body.appendChild(script); diff --git a/static/profile/js/profileeditor.js b/static/profile/js/profileeditor.js index 49cb48155bf..4f969d70adb 100644 --- a/static/profile/js/profileeditor.js +++ b/static/profile/js/profileeditor.js @@ -86,7 +86,8 @@ // Fetch data from mongo peStatus.hide().text(translate('Loading profile records ...')).fadeIn('slow'); $.ajax('/api/v1/profile.json', { - success: function (records) { + headers: client.headers() + , success: function (records) { if (!records.length) { records.push(defaultprofile); } @@ -122,8 +123,8 @@ }); peStatus.hide().text(translate('Default values used.')).fadeIn('slow'); } - }, - error: function () { + } + , error: function () { mongorecords.push({ defaultProfile: 'Default' , store : { @@ -258,9 +259,7 @@ $.ajax({ method: 'DELETE' , url: '/api/v1/profile/'+mongorecords[currentrecord]._id - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function postSuccess () { console.info('profile deleted'); peStatus.hide().text(status).fadeIn('slow'); @@ -657,9 +656,7 @@ method: 'PUT' , url: '/api/v1/profile/' , data: adjustedRecord - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function postSuccess (data, status) { console.info('profile saved', data); peStatus.hide().text(status).fadeIn('slow'); diff --git a/static/report/js/report.js b/static/report/js/report.js index 5735feaa9ec..6a2b5cdfdfc 100644 --- a/static/report/js/report.js +++ b/static/report/js/report.js @@ -110,7 +110,8 @@ $('#info').html(''+translate('Loading food database')+' ...'); $.ajax('/api/v1/food/regular.json', { - success: function foodLoadSuccess(records) { + headers: client.headers() + , success: function foodLoadSuccess(records) { records.forEach(function (r) { food_list.push(r); if (r.category && !food_categories[r.category]) { food_categories[r.category] = {}; } @@ -300,7 +301,8 @@ var treatmentData; var tquery = '?find[notes]=/' + notes + '/i'; $.ajax('/api/v1/treatments.json' + tquery + timerange, { - success: function (xhr) { + headers: client.headers() + , success: function (xhr) { treatmentData = xhr.map(function (treatment) { return moment.tz(treatment.created_at,zone).format('YYYY-MM-DD'); }); diff --git a/tests/api.entries.test.js b/tests/api.entries.test.js index 9696269d686..4dbf626eada 100644 --- a/tests/api.entries.test.js +++ b/tests/api.entries.test.js @@ -11,6 +11,7 @@ describe('Entries REST api', function ( ) { this.timeout(10000); before(function (done) { var env = require('../env')( ); + env.settings.authDefaultRoles = 'readable'; this.wares = require('../lib/middleware/')(env); this.archive = null; this.app = require('express')( ); @@ -196,13 +197,13 @@ describe('Entries REST api', function ( ) { }); }); - it('/entries/preview', function (done) { + it('disallow POST by readable /entries/preview', function (done) { request(this.app) .post('/entries/preview.json') .send(load('json')) - .expect(201) + .expect(401) .end(function (err, res) { - res.body.should.be.instanceof(Array).and.have.lengthOf(30); + // res.body.should.be.instanceof(Array).and.have.lengthOf(30); done(); }); }); diff --git a/tests/api.status.test.js b/tests/api.status.test.js index 5044c2afdd2..8edb2a0cf36 100644 --- a/tests/api.status.test.js +++ b/tests/api.status.test.js @@ -8,6 +8,7 @@ describe('Status REST api', function ( ) { before(function (done) { var env = require('../env')( ); env.settings.enable = ['careportal', 'rawbg']; + env.settings.authDefaultRoles = 'readable'; env.api_secret = 'this is my long pass phrase'; this.wares = require('../lib/middleware/')(env); this.app = require('express')( ); @@ -43,6 +44,27 @@ describe('Status REST api', function ( ) { }); }); + it('/status.svg', function (done) { + request(this.app) + .get('/api/status.svg') + .end(function(err, res) { + res.statusCode.should.equal(302); + done(); + }); + }); + + it('/status.txt', function (done) { + request(this.app) + .get('/api/status.txt') + .expect(200, 'STATUS OK') + .end(function(err, res) { + res.type.should.equal('text/plain'); + res.statusCode.should.equal(200); + done(); + }); + }); + + it('/status.js', function (done) { request(this.app) .get('/api/status.js') diff --git a/tests/api.treatments.test.js b/tests/api.treatments.test.js index d7b2d3c6c84..0a84d6f8b5c 100644 --- a/tests/api.treatments.test.js +++ b/tests/api.treatments.test.js @@ -11,7 +11,8 @@ describe('Treatment API', function ( ) { beforeEach(function (done) { process.env.API_SECRET = 'this is my long pass phrase'; self.env = require('../env')(); - self.env.settings.enable = ['careportal']; + self.env.settings.authDefaultRoles = 'readable'; + self.env.settings.enable = ['careportal', 'api']; this.wares = require('../lib/middleware/')(self.env); self.app = require('express')(); self.app.enable('api'); @@ -24,7 +25,7 @@ describe('Treatment API', function ( ) { }); after(function () { - delete process.env.API_SECRET; + // delete process.env.API_SECRET; }); it('post single treatments', function (done) { @@ -140,4 +141,4 @@ describe('Treatment API', function ( ) { }); }); }); -}); \ No newline at end of file +}); diff --git a/tests/api.unauthorized.test.js b/tests/api.unauthorized.test.js index 64e7d254bd0..ef5976ba1fb 100644 --- a/tests/api.unauthorized.test.js +++ b/tests/api.unauthorized.test.js @@ -12,6 +12,7 @@ describe('authed REST api', function ( ) { delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; var env = require('../env')( ); + env.settings.authDefaultRoles = 'readable'; this.wares = require('../lib/middleware/')(env); this.archive = null; this.app = require('express')( ); @@ -59,6 +60,19 @@ describe('authed REST api', function ( ) { }); }); + it('/entries/preview', function (done) { + var known_key = this.known_key; + request(this.app) + .post('/entries/preview.json') + .set('api-secret', known_key) + .send(load('json')) + .expect(201) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(30); + done(); + }); + }); + it('allow authorized POST', function (done) { var app = this.app; var known_key = this.known_key; diff --git a/tests/careportal.test.js b/tests/careportal.test.js index bf45f6cc498..671304f3b22 100644 --- a/tests/careportal.test.js +++ b/tests/careportal.test.js @@ -15,40 +15,25 @@ var nowData = { describe('client', function ( ) { var self = this; + var headless = require('./fixtures/headless')(benv, this); + before(function (done) { - benv.setup(function() { - self.$ = require('jquery'); - self.$.localStorage = require('./fixtures/localstorage'); - - self.$.fn.tipsy = function mockTipsy ( ) { }; - - var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); - self.$('body').html(indexHtml); - - var d3 = require('d3'); - //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; - - benv.expose({ - $: self.$ - , jQuery: self.$ - , d3: d3 - , io: { - connect: function mockConnect ( ) { - return { - on: function mockOn ( ) { } - }; - } - } - }); - done(); - }); + done( ); }); after(function (done) { - benv.teardown(); - done(); + done( ); + }); + + beforeEach(function (done) { + headless.setup({ }, done); }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + it ('open careportal, and enter a treatment', function (done) { var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); diff --git a/tests/ddata.test.js b/tests/ddata.test.js new file mode 100644 index 00000000000..6b8a60269ee --- /dev/null +++ b/tests/ddata.test.js @@ -0,0 +1,74 @@ + +'use strict'; + +var should = require('should'); + + +describe('ddata', function ( ) { + // var sandbox = require('../lib/sandbox')(); + // var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + + it('should be a module', function (done) { + var libddata = require('../lib/data/ddata'); + var ddata = libddata( ); + should.exist(ddata); + should.exist(libddata); + should.exist(libddata.call); + ddata = ctx.ddata.clone( ); + should.exist(ddata); + done( ); + }); + + it('has #clone( )', function (done) { + should.exist(ctx.ddata.treatments); + should.exist(ctx.ddata.sgvs); + should.exist(ctx.ddata.mbgs); + should.exist(ctx.ddata.cals); + should.exist(ctx.ddata.profiles); + should.exist(ctx.ddata.devicestatus); + should.exist(ctx.ddata.lastUpdated); + var ddata = ctx.ddata.clone( ); + should.exist(ddata); + should.exist(ddata.treatments); + should.exist(ddata.sgvs); + should.exist(ddata.mbgs); + should.exist(ddata.cals); + should.exist(ddata.profiles); + should.exist(ddata.devicestatus); + should.exist(ddata.lastUpdated); + done( ); + }); + + it('has #split( )', function (done) { + var date = new Date( ); + var time = date.getTime( ); + var cutoff = 1000 * 60 * 5; + var max = 1000 * 60 * 60 * 24 * 2; + var pieces = ctx.ddata.split(time, cutoff, max); + should.exist(pieces); + should.exist(pieces.first); + should.exist(pieces.last); + should.exist(pieces.rest); + + done( ); + }); + + // TODO: ensure partition function gets called via: + // Properties + // * ddata.devicestatus + // * ddata.mbgs + // * ddata.sgvs + // * ddata.treatments + // * ddata.profiles + // * ddata.lastUpdated + // Methods + // * ddata.processTreatments + // * ddata.processDurations + // * ddata.clone + // * ddata.split + + +}); + diff --git a/tests/fixtures/headless.js b/tests/fixtures/headless.js new file mode 100644 index 00000000000..6928766fe44 --- /dev/null +++ b/tests/fixtures/headless.js @@ -0,0 +1,127 @@ + +var read = require('fs').readFileSync; +var _ = require('lodash'); + +function headless (benv, binding) { + var self = binding; + function root ( ) { + return benv; + } + + function init (opts, callback) { + var localStorage = opts.localStorage || './localstorage'; + var htmlFile = opts.htmlFile || __dirname + '/../../static/index.html'; + var serverSettings = opts.serverSettings || require('./default-server-settings'); + var someData = opts.mockAjax || { }; + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require(localStorage); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(htmlFile, 'utf8'); + self.$('body').html(indexHtml); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + if (opts.mockProfileEditor) { + self.$.plot = function mockPlot () { + }; + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + self.$.fn.dialog = function mockDialog (opts) { + function maybeCall (name, obj) { + if (obj[name] && obj[name].call) { + obj[name](); + } + + } + maybeCall('open', opts); + + _.forEach(opts.buttons, function (button) { + maybeCall('click', button); + }); + }; + } + if (opts.mockSimpleAjax) { + someData = opts.mockSimpleAjax; + self.$.ajax = function mockAjax (url, opts) { + var returnVal = someData[url] || []; + if (opts && typeof opts.success === 'function') { + opts.success(returnVal); + } + return self.$.Deferred().resolveWith(returnVal); + }; + } + if (opts.mockAjax) { + self.$.ajax = function mockAjax (url, opts) { + //logfile.write(url+'\n'); + //console.log(url,opts); + if (opts && opts.success && opts.success.call) { + return { + done: function mockDone (fn) { + if (someData[url]) { + console.log('+++++Data for ' + url + ' sent'); + opts.success(someData[url]); + } else { + console.log('-----Data for ' + url + ' missing'); + opts.success([]); + } + fn(); + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + } + return { + done: function mockDone (fn) { + fn({message: 'OK'}); + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + }; + } + + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , serverSettings: serverSettings + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + + var extraRequires = opts.benvRequires || [ ]; + extraRequires.forEach(function (req) { + benv.require(req); + }); + callback( ); + }); + + } + + function teardown ( ) { + benv.teardown(); + } + root.setup = init; + root.teardown = teardown; + + return root; +} + +module.exports = headless; + diff --git a/tests/hashauth.test.js b/tests/hashauth.test.js index 9d2802e8f0e..99b3aa3ede4 100644 --- a/tests/hashauth.test.js +++ b/tests/hashauth.test.js @@ -7,6 +7,25 @@ var serverSettings = require('./fixtures/default-server-settings'); describe('hashauth', function ( ) { var self = this; + var headless = require('./fixtures/headless')(benv, this); + + before(function (done) { + done( ); + }); + + after(function (done) { + done( ); + }); + + beforeEach(function (done) { + headless.setup({ }, done); + }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + /* before(function (done) { benv.setup(function() { self.$ = require('jquery'); @@ -41,6 +60,7 @@ describe('hashauth', function ( ) { benv.teardown(); done(); }); + */ it ('should make module unauthorized', function () { var plugins = require('../lib/plugins/')().registerClientDefaults(); @@ -55,7 +75,7 @@ describe('hashauth', function ( ) { client.init(serverSettings, plugins); - hashauth.inlineCode().indexOf('Device not authenticated').should.be.greaterThan(0); + hashauth.inlineCode().indexOf('Not authorized').should.be.greaterThan(0); hashauth.isAuthenticated().should.equal(false); var testnull = (hashauth.hash()===null); testnull.should.equal(true); @@ -74,7 +94,7 @@ describe('hashauth', function ( ) { client.init(serverSettings, plugins); - hashauth.inlineCode().indexOf('Device authenticated').should.be.greaterThan(0); + hashauth.inlineCode().indexOf('Admin authorized').should.be.greaterThan(0); hashauth.isAuthenticated().should.equal(true); }); @@ -137,7 +157,7 @@ describe('hashauth', function ( ) { localStorage.remove('apisecrethash'); - hashauth.init(client,$); + hashauth.init(client, self.$); client.init(serverSettings, plugins); diff --git a/tests/loop.test.js b/tests/loop.test.js new file mode 100644 index 00000000000..8180315da33 --- /dev/null +++ b/tests/loop.test.js @@ -0,0 +1,237 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); +var moment = require('moment'); + +var env = require('../env')(); +var loop = require('../lib/plugins/loop')(); +var sandbox = require('../lib/sandbox')(); +var levels = require('../lib/levels'); + +var statuses = [ + { + 'created_at':'2016-08-13T20:09:15Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'enacted':{ + 'timestamp':'2016-08-13T20:09:15Z', + 'rate':0.875, + 'duration':30, + 'received':true + }, + 'version':'0.9.1', + 'recommendedBolus':0, + 'timestamp':'2016-08-13T20:09:15Z', + 'predicted':{ + 'startDate':'2016-08-13T20:03:47Z', + 'values':[ + 149, + 149, + 148, + 148, + 147, + 147 + ] + }, + 'iob':{ + 'timestamp':'2016-08-13T20:05:00Z', + 'iob':0.1733152537837709 + }, + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T20:04:15Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'version':'0.9.1', + 'recommendedBolus':0, + 'timestamp':'2016-08-13T20:04:15Z', + 'failureReason':'SomeError', + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:20Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'timestamp':'2016-08-13T01:18:20Z', + 'version':'0.9.1', + 'iob':{ + 'timestamp':'2016-08-13T01:15:00Z', + 'iob':-0.1205140849137931 + }, + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:20Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'timestamp':'2016-08-13T01:13:20Z', + 'version':'0.9.1', + 'iob':{ + 'timestamp':'2016-08-13T01:10:00Z', + 'iob':-0.1205140849137931 + }, + 'failureReason':'StaleDataError(\"Glucose Date: 2016-08-12 23:23:49 +0000 or Pump status date: 2016-08-13 01:13:10 +0000 older than 15.0 min\")', + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:15Z', + 'pump':{ + 'reservoir':90.5, + 'clock':'2016-08-13T01:13:10Z', + 'battery':{ + 'status':'normal', + 'voltage':1.5 + }, + 'pumpID':'543204' + }, + 'device':'loop://ExamplePhone', + 'uploader':{ + 'timestamp':'2016-08-13T01:13:15Z', + 'battery':43, + 'name':'ExamplePhone' + } + } +]; + +var now = moment(statuses[0].created_at); + +_.forEach(statuses, function updateMills (status) { + status.mills = moment(status.created_at).valueOf(); +}); + +describe('loop', function ( ) { + + it('should set the property and update the pill and add forecast points', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('Loop ⌁'); + options.value.should.equal('1m ago'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('Temp Basal Started 0.88U/hour for 30m, IOB: 0.17U'); + } + , addForecastPoints: function mockAddForecastPoints (points) { + points.length.should.equal(6); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + + result.display.symbol.should.equal('⌁'); + result.display.code.should.equal('enacted'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + }; + + loop.setProperties(sbx); + loop.updateVisualisation(sbx); + }); + + it('should show errors', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('Loop x'); + options.value.should.equal('1m ago'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('Error: SomeError'); + done(); + } + } + }; + + var errorTime = moment(statuses[1].created_at); + + var sbx = sandbox.clientInit(ctx, errorTime.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + + result.display.symbol.should.equal('x'); + result.display.code.should.equal('error'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + }; + + loop.setProperties(sbx); + + loop.updateVisualisation(sbx); + + }); + + + it('should check the recieved flag to see if it was received', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var notStatuses = _.cloneDeep(statuses); + notStatuses[0].loop.enacted.received = false; + var sbx = require('../lib/sandbox')().clientInit(ctx, now, {devicestatus: notStatuses}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + result.display.symbol.should.equal('x'); + result.display.code.should.equal('error'); + done(); + }; + + loop.setProperties(sbx); + }); + + it('should generate an alert for a stuck loop', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.add(2, 'hours').valueOf(), {devicestatus: statuses}); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + loop.setProperties(sbx); + loop.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Loop'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('Loop isn\'t looping'); + done(); + }); + +}); diff --git a/tests/notifications-api.test.js b/tests/notifications-api.test.js index 3f3b4ddc22a..4597aa69bf5 100644 --- a/tests/notifications-api.test.js +++ b/tests/notifications-api.test.js @@ -25,8 +25,15 @@ describe('Notifications API', function ( ) { , ddata: { lastUpdated: Date.now() } + , store: { + collection: function ( ) { + return { }; + } + } }; + ctx.authorization = require('../lib/authorization')(env, ctx); + var notifications = require('../lib/notifications')(env, ctx); ctx.notifications = notifications; diff --git a/tests/pebble.test.js b/tests/pebble.test.js index dc4f30ae0a0..1571ad5a1ae 100644 --- a/tests/pebble.test.js +++ b/tests/pebble.test.js @@ -83,14 +83,20 @@ ctx.ddata.treatments = updateMills([ ctx.ddata.devicestatus = [{uploader: {battery: 100}}]; +var bootevent = require('../lib/bootevent'); describe('Pebble Endpoint', function ( ) { var pebble = require('../lib/pebble'); before(function (done) { var env = require('../env')( ); + env.settings.authDefaultRoles = 'readable'; this.app = require('express')( ); this.app.enable('api'); - this.app.use('/pebble', pebble(env, ctx)); - done(); + var self = this; + bootevent(env).boot(function booted (context) { + context.ddata = ctx.ddata.clone( ); + self.app.use('/pebble', pebble(env, context)); + done(); + }); }); it('/pebble default(1) count', function (done) { @@ -216,12 +222,17 @@ describe('Pebble Endpoint', function ( ) { describe('Pebble Endpoint with Raw and IOB and COB', function ( ) { var pebbleRaw = require('../lib/pebble'); before(function (done) { - var envRaw = require('../env')( ); - envRaw.settings.enable = ['rawbg', 'iob', 'cob']; + var env = require('../env')( ); + env.settings.enable = ['rawbg', 'iob', 'cob']; + env.settings.authDefaultRoles = 'readable'; this.appRaw = require('express')( ); this.appRaw.enable('api'); - this.appRaw.use('/pebble', pebbleRaw(envRaw, ctx)); - done(); + var self = this; + bootevent(env).boot(function booted (context) { + context.ddata = ctx.ddata.clone( ); + self.appRaw.use('/pebble', pebbleRaw(env, context)); + done(); + }); }); it('/pebble', function (done) { diff --git a/tests/pluginbase.test.js b/tests/pluginbase.test.js index 2397720b09e..c04f0c802ce 100644 --- a/tests/pluginbase.test.js +++ b/tests/pluginbase.test.js @@ -4,22 +4,26 @@ require('should'); var benv = require('benv'); describe('pluginbase', function ( ) { + var headless = require('./fixtures/headless')(benv, this); before(function (done) { - benv.setup(function() { - benv.expose({ - $: require('jquery') - , jQuery: require('jquery') - }); - done(); - }); + done( ); }); after(function (done) { - benv.teardown(); - done(); + done( ); + }); + + beforeEach(function (done) { + headless.setup({ }, done); }); + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + + it('does stuff', function() { function div (clazz) { @@ -51,4 +55,4 @@ describe('pluginbase', function ( ) { majorPills.length.should.equal(1); }); -}); \ No newline at end of file +}); diff --git a/tests/profileeditor.test.js b/tests/profileeditor.test.js index 42794814802..287b55deb0f 100644 --- a/tests/profileeditor.test.js +++ b/tests/profileeditor.test.js @@ -72,94 +72,32 @@ var someData = { describe('Profile editor', function ( ) { var self = this; + var headless = require('./fixtures/headless')(benv, this); before(function (done) { - benv.setup(function() { - self.$ = require('jquery'); - self.$.localStorage = require('./fixtures/localstorage'); - - self.$.fn.tipsy = function mockTipsy ( ) { }; - - self.$.fn.dialog = function mockDialog (opts) { - function maybeCall (name, obj) { - if (obj[name] && obj[name].call) { - obj[name](); - } - - } - maybeCall('open', opts); - - _.forEach(opts.buttons, function (button) { - maybeCall('click', button); - }); - }; - - var indexHtml = read(__dirname + '/../static/profile/index.html', 'utf8'); - self.$('body').html(indexHtml); - - //var filesys = require('fs'); - //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) - - self.$.ajax = function mockAjax (url, opts) { - //logfile.write(url+'\n'); - //console.log(url,opts); - if (opts && opts.success && opts.success.call) { - return { - done: function mockDone (fn) { - if (someData[url]) { - console.log('+++++Data for ' + url + ' sent'); - opts.success(someData[url]); - } else { - console.log('-----Data for ' + url + ' missing'); - opts.success([]); - } - fn(); - return self.$.ajax(); - }, - fail: function mockFail () { - return self.$.ajax(); - } - }; - } - return { - done: function mockDone (fn) { - fn({message: 'OK'}); - return self.$.ajax(); - }, - fail: function mockFail () { - return self.$.ajax(); - } - }; - }; - - var d3 = require('d3'); - //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; - - benv.expose({ - $: self.$ - , jQuery: self.$ - , d3: d3 - , serverSettings: serverSettings - , io: { - connect: function mockConnect ( ) { - return { - on: function mockOn ( ) { } - }; - } - } - }); + done( ); + }); - benv.require(__dirname + '/../bundle/bundle.source.js'); - benv.require(__dirname + '/../static/profile/js/profileeditor.js'); + after(function (done) { + done( ); + }); - done(); - }); + beforeEach(function (done) { + var opts = { + htmlFile: __dirname + '/../static/profile/index.html' + , mockProfileEditor: true + , mockAjax: someData + , benvRequires: [ + __dirname + '/../bundle/bundle.source.js' + , __dirname + '/../static/profile/js/profileeditor.js' + ] + }; + headless.setup(opts, done); }); - after(function (done) { - benv.teardown(true); - done(); + afterEach(function (done) { + headless.teardown( ); + done( ); }); it ('should produce some html', function (done) { diff --git a/tests/reports.test.js b/tests/reports.test.js index b1d6badef2b..69481907512 100644 --- a/tests/reports.test.js +++ b/tests/reports.test.js @@ -173,75 +173,36 @@ exampleProfile[0].startDate.setMilliseconds(0); describe('reports', function ( ) { var self = this; + var headless = require('./fixtures/headless')(benv, this); before(function (done) { - benv.setup(function() { - self.$ = require('jquery'); - self.$.localStorage = require('./fixtures/localstorage'); - - self.$.fn.tipsy = function mockTipsy ( ) { }; - - self.$.fn.dialog = function mockDialog (opts) { - function maybeCall (name, obj) { - if (obj[name] && obj[name].call) { - obj[name](); - } - - } - maybeCall('open', opts); - - _.forEach(opts.buttons, function (button) { - maybeCall('click', button); - }); - }; - - var indexHtml = read(__dirname + '/../static/report/index.html', 'utf8'); - self.$('body').html(indexHtml); - - //var filesys = require('fs'); - //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) - - self.$.ajax = function mockAjax (url, opts) { - var returnVal = someData[url] || []; - if (opts && typeof opts.success === 'function') { - opts.success(returnVal); - } - return self.$.Deferred().resolveWith(returnVal); - }; - - self.$.plot = function mockPlot () { - }; - - var d3 = require('d3'); - //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; - - benv.expose({ - $: self.$ - , jQuery: self.$ - , d3: d3 - , serverSettings: serverSettings - , io: { - connect: function mockConnect ( ) { - return { - on: function mockOn ( ) { } - }; - } - } - }); + done( ); + }); - benv.require(__dirname + '/../bundle/bundle.source.js'); - benv.require(__dirname + '/../static/report/js/report.js'); + after(function (done) { + done( ); + }); - done(); - }); + beforeEach(function (done) { + var opts = { + htmlFile: __dirname + '/../static/report/index.html' + , mockProfileEditor: true + , serverSettings: serverSettings + , mockSimpleAjax: someData + , benvRequires: [ + __dirname + '/../bundle/bundle.source.js' + , __dirname + '/../static/report/js/report.js' + ] + }; + headless.setup(opts, done); }); - after(function (done) { - benv.teardown(true); - done(); + afterEach(function (done) { + headless.teardown( ); + done( ); }); + it ('should produce some html', function (done) { var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); @@ -300,7 +261,7 @@ describe('reports', function ( ) { //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) //logfile.write($('body').html()); - //console.log(result); + // console.log(result); result.indexOf('Milk now').should.be.greaterThan(-1); // daytoday result.indexOf('50 g (1.67U)').should.be.greaterThan(-1); // daytoday diff --git a/tests/security.test.js b/tests/security.test.js index 0c5a5227f53..198cda6b4e3 100644 --- a/tests/security.test.js +++ b/tests/security.test.js @@ -10,8 +10,7 @@ describe('API_SECRET', function ( ) { var scope = this; function setup_app (env, fn) { require('../lib/bootevent')(env).boot(function booted (ctx) { - var wares = require('../lib/middleware/')(env); - ctx.app = api(env, wares, ctx); + ctx.app = api(env, ctx); scope.app = ctx.app; scope.entries = ctx.entries; fn(ctx); diff --git a/tests/verifyauth.test.js b/tests/verifyauth.test.js index 0e635ec304c..339f716264e 100644 --- a/tests/verifyauth.test.js +++ b/tests/verifyauth.test.js @@ -8,8 +8,7 @@ describe('verifyauth', function ( ) { var scope = this; function setup_app (env, fn) { require('../lib/bootevent')(env).boot(function booted (ctx) { - var wares = require('../lib/middleware/')(env); - ctx.app = api(env, wares, ctx); + ctx.app = api(env, ctx); scope.app = ctx.app; fn(ctx); });