From f144c986b784393d347be0e9fc307916742fc6d8 Mon Sep 17 00:00:00 2001 From: vjrj Date: Sun, 20 May 2018 17:45:25 +0200 Subject: [PATCH 01/84] Added fire stats in zone to API --- imports/api/ActiveFires/server/countFires.js | 44 ++++++++++--------- .../ActiveFires/server/tweetFiresInZone.js | 8 ++-- imports/startup/server/rest.js | 41 ++++++++++++++++- settings-development-sample.json | 1 + 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/imports/api/ActiveFires/server/countFires.js b/imports/api/ActiveFires/server/countFires.js index dc65ab4..39008da 100644 --- a/imports/api/ActiveFires/server/countFires.js +++ b/imports/api/ActiveFires/server/countFires.js @@ -29,7 +29,25 @@ const findFiresInRegion = (zone) => { return result; }; -const countFires = (regions, stringsToRemove) => { +export const countRealFires = (firesCursor) => { + const realFires = []; + firesCursor.forEach((fire) => { + const union = firesUnion([fire]); + const falsePos = whichAreFalsePositives(FalsePositives, union); + const industries = whichAreFalsePositives(Industries, union); + if (falsePos.count() === 0 && industries.count() === 0) { + realFires.push(fire); + } + }); + // group fires + const realUnion = firesUnion(realFires); + const unionCount = realUnion[0] && + realUnion[0].geometry && + realUnion[0].geometry.coordinates ? realUnion[0].geometry.coordinates.length : 0; + return unionCount; +}; + +const countFiresInRegions = (regions, stringsToRemove) => { let total = 0; const fireStats = {}; @@ -41,25 +59,9 @@ const countFires = (regions, stringsToRemove) => { const fires = findFiresInRegion(region); // TODO Also check neighbour fires (when better implementation of that part) const initialCount = fires.count(); - let count = initialCount; - if (count > 0) { - const realFires = []; - fires.forEach((fire) => { - const union = firesUnion([fire]); - const falsePos = whichAreFalsePositives(FalsePositives, union); - const industries = whichAreFalsePositives(Industries, union); - if (falsePos.count() === 0 && industries.count() === 0) { - realFires.push(fire); - } else { - count -= 1; - } - }); - // group fires - const realUnion = firesUnion(realFires); - const unionCount = realUnion[0] && - realUnion[0].geometry && - realUnion[0].geometry.coordinates ? realUnion[0].geometry.coordinates.length : 0; - if (debug) console.log(`${regionName} initial: ${initialCount}, first calc: ${count} union calc: ${unionCount}`); + if (initialCount > 0) { + const unionCount = countRealFires(fires); + if (debug) console.log(`${regionName} initial: ${initialCount}, union calc: ${unionCount}`); if (unionCount > 0) { total += unionCount; fireStats[regionName] = { count: unionCount, code: regionCode }; @@ -72,4 +74,4 @@ const countFires = (regions, stringsToRemove) => { return { total, fires: fireStats }; }; -export default countFires; +export default countFiresInRegions; diff --git a/imports/api/ActiveFires/server/tweetFiresInZone.js b/imports/api/ActiveFires/server/tweetFiresInZone.js index d88ef85..74d40fb 100644 --- a/imports/api/ActiveFires/server/tweetFiresInZone.js +++ b/imports/api/ActiveFires/server/tweetFiresInZone.js @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import tbuffer from '@turf/buffer'; import emojiFlags from 'emoji-flags'; import tweet from '/imports/modules/server/twitter'; -import countFires from './countFires'; +import countFiresInRegions from './countFires'; const debug = false; @@ -103,7 +103,7 @@ const correctTweetSize = (tweetText) => { }; const tweetEuropeFires = () => { - const resultEu = countFires(europe, [[' ', '']]); + const resultEu = countFiresInRegions(europe, [[' ', '']]); if (resultEu.total > 0) { let tweetText = `${tweetHeaders[0].trim()} ${composeEuropeTweet(resultEu.total, resultEu.fires, false)}. More info: https://fires.comunes.org/fires`; @@ -126,8 +126,8 @@ const tweetEuropeFires = () => { }; const tweetIberiaFires = () => { - const resultEs = countFires(autonomies, stringsToRemoveIberia); - const resultPt = countFires(portugal, stringsToRemoveIberia); + const resultEs = countFiresInRegions(autonomies, stringsToRemoveIberia); + const resultPt = countFiresInRegions(portugal, stringsToRemoveIberia); const fires = Object.assign(resultEs.fires, resultPt.fires); const total = resultEs.total + resultPt.total; diff --git a/imports/startup/server/rest.js b/imports/startup/server/rest.js index b6f63a9..c19edee 100644 --- a/imports/startup/server/rest.js +++ b/imports/startup/server/rest.js @@ -2,9 +2,12 @@ /* eslint-disable import/no-absolute-path */ import { Meteor } from 'meteor/meteor'; +import { NumberBetween } from '/imports/modules/server/other-checks'; import Fires from '/imports/api/Fires/Fires'; +import { check } from 'meteor/check'; import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; +import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; Meteor.startup(() => { const uptime = new Date(); @@ -19,7 +22,7 @@ Meteor.startup(() => { // Generates: POST on /api/users and GET, DELETE /api/users/:id for // Meteor.users collection /* apiV1.addCollection(Meteor.users, { - * excludedEndpoints: ['getAll', 'put'], + * excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'], * routeOptions: { * authRequired: true * }, @@ -75,4 +78,40 @@ Meteor.startup(() => { return { ms: new Date() - uptime }; } }); + + // Maps to: /api/v1/fires-in/:lat/:lng/:km + // Ex: http://127.0.0.1:3000/api/v1/fires-in/38.736946/-9.142685/100 + apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { + get: function get() { + const lat = Number(this.urlParams.lat); + const lng = Number(this.urlParams.lng); + const km = Number(this.urlParams.km); + const { token } = this.urlParams; + check(lng, NumberBetween(-180, 180)); + check(lat, NumberBetween(-90, 90)); + check(km, NumberBetween(0, 100)); + check(token, String); + + console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); + + if (token !== Meteor.settings.private.internalApiToken) { + return {}; + } + + const fires = ActiveFiresCollection.find({ + ourid: { + $near: { + $geometry: { + type: 'Point', + coordinates: [lng, lat] + }, + $maxDistance: km * 1000, + $minDistance: 0 + } + } + }); + + return { total: fires.count(), real: countRealFires(fires) }; + } + }); }); diff --git a/settings-development-sample.json b/settings-development-sample.json index 1ba68f8..9aa3ae3 100644 --- a/settings-development-sample.json +++ b/settings-development-sample.json @@ -33,6 +33,7 @@ }, "proxies_count": 0, "ironPassword": "SomePasswordAlLeast32LongYouKnowHowToCount", + "internalApiToken": "someSimilarPasswordForUseOurAPIinternally", "cron": { "debug": false }, From e36dda74db2896d122afa8ee0a1470176bd322ba Mon Sep 17 00:00:00 2001 From: vjrj Date: Mon, 21 May 2018 15:43:07 +0200 Subject: [PATCH 02/84] Better api logs --- imports/startup/server/rest.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imports/startup/server/rest.js b/imports/startup/server/rest.js index c19edee..423922f 100644 --- a/imports/startup/server/rest.js +++ b/imports/startup/server/rest.js @@ -92,12 +92,13 @@ Meteor.startup(() => { check(km, NumberBetween(0, 100)); check(token, String); - console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); - if (token !== Meteor.settings.private.internalApiToken) { + console.warn(`WARNING: Query for fires in ${lat}, ${lng} in ${km} km radius with wrong token`); return {}; } + console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); + const fires = ActiveFiresCollection.find({ ourid: { $near: { From bea2703f2f1f8becc0c820766ea1bab2f3ebca08 Mon Sep 17 00:00:00 2001 From: vjrj Date: Mon, 21 May 2018 15:43:43 +0200 Subject: [PATCH 03/84] Better stats in FiresMap --- imports/ui/pages/FiresMap/FireStats.js | 44 ++++++++++++++++++++++---- imports/ui/pages/FiresMap/FiresMap.js | 4 +-- public/locales/en/common.json | 4 ++- public/locales/es/common.json | 5 ++- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/imports/ui/pages/FiresMap/FireStats.js b/imports/ui/pages/FiresMap/FireStats.js index 3a9887e..f88245c 100644 --- a/imports/ui/pages/FiresMap/FireStats.js +++ b/imports/ui/pages/FiresMap/FireStats.js @@ -8,19 +8,49 @@ import { translate, Trans } from 'react-i18next'; import FromNow from '/imports/ui/components/FromNow/FromNow'; class FireStats extends Component { + constructor(props) { + super(props); + this.state = { + lastFireDetected: props.lastFireDetected, + lastCheck: props.lastCheck, + loadingAll: props.loadingAll + }; + } + + componentWillReceiveProps(nextProps) { + if (this.props.lastCheck !== nextProps.lastCheck || + this.props.lastFireDetected !== nextProps.lastFireDetected || + this.props.loadingAll !== nextProps.loadingAll + ) { + this.setState({ + lastFireDetected: nextProps.lastFireDetected, + lastCheck: nextProps.lastCheck, + loadingAll: nextProps.loadingAll + }); + } + } + + shouldComponentUpdate(nextProps, nextState) { + return !(nextState.lastCheck === this.state.lastCheck && + nextState.lastFireDetected === this.state.lastFireDetected && + nextState.loadingAll === this.state.loadingAll); + } + render() { - return ( - - { this.props.lastCheck && this.props.lastFireDetected ? - Actualizado , último fuego detectado . - : '' } - - ); + if (this.state.loadingAll) return (
Actualizando...
); + if (this.state.lastCheck && !this.state.lastFireDetected) { + return (
Actualizado .
); + } + if (this.state.lastCheck && this.state.lastFireDetected) { + return (
Actualizado , último fuego detectado .
); + } + return (); } } FireStats.propTypes = { lastFireDetected: PropTypes.object, + loadingAll: PropTypes.bool.isRequired, lastCheck: PropTypes.instanceOf(Date) }; diff --git a/imports/ui/pages/FiresMap/FiresMap.js b/imports/ui/pages/FiresMap/FiresMap.js index 9013572..265b0cc 100644 --- a/imports/ui/pages/FiresMap/FiresMap.js +++ b/imports/ui/pages/FiresMap/FiresMap.js @@ -245,8 +245,8 @@ class FiresMap extends React.Component {

{ (this.props.activefires.length + this.props.firealerts.length) === 0 ? - No hay fuegos activos en esta zona del mapa. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. : - En rojo, {{ count: this.props.activefires.length + this.props.firealerts.length }} fuegos activos. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. + No hay fuegos activos en esta zona del mapa. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. : + En rojo, {{ count: this.props.activefires.length + this.props.firealerts.length }} fuegos activos. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. }

{isNotHomeAndMobile() && this.props.firealerts.length > 0 && diff --git a/public/locales/en/common.json b/public/locales/en/common.json index e5189fc..bc6ae5a 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -167,6 +167,7 @@ "Verifica tu dirección de correo": "Verify Your Email Address", "Zonas vigiladas": "Monitored areas", "En verde, las zonas vigiladas por nuestros usuari@s actualmente": "In green, the areas monitored by our users currently", + "Actualizado <1>.": "Updated <1>.", "Actualizado <1>, último fuego detectado <3>.": "Updated <1>, last fire detected <3>.", "Información adicional sobre fuego": "Additional information about fire", "CO2emisions": "Did you know that wildfires <1>produce as much CO² as cars and <3>about ⅕ of all our carbon emissions?", @@ -248,5 +249,6 @@ "Note: Cloud cover might obscure active fire detections.", "Hay más información sobre un fuego": "There is more information about a fire", - "esDecirTalDia": "That is on {{date}}" + "esDecirTalDia": "That is on {{date}}", + "Actualizando...": "Updating..." } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 35a1da4..a460481 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -237,6 +237,7 @@ "Verifica tu dirección de correo", "Zonas vigiladas": "Zonas vigiladas", "En verde, las zonas vigiladas por nuestros usuari@s actualmente": "En verde, las zonas vigiladas por nuestros usuari@s actualmente", + "Actualizado <1>.": "Actualizado <1>.", "Actualizado <1>, último fuego detectado <3>.": "Actualizado <1>, último fuego detectado <3>.", "Tomamos nota, ¡gracias por colaborar!": "Tomamos nota, ¡gracias por colaborar!", @@ -333,5 +334,7 @@ "Nota: Las nubes pueden entorpecer la detección de fuegos activos.", "Hay más información sobre un fuego": "Hay más información sobre un fuego", - "esDecirTalDia": "Es decir el {{date}}" + "esDecirTalDia": "Es decir el {{date}}", + "Actualizando...": + "Actualizando..." } From d30385b49e181051d03c4130e360bc0f37606068 Mon Sep 17 00:00:00 2001 From: vjrj Date: Mon, 21 May 2018 17:42:37 +0200 Subject: [PATCH 04/84] Disable raven when not configured --- imports/startup/client/ravenLogger.js | 6 +++--- imports/startup/server/ravenLogger.js | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/imports/startup/client/ravenLogger.js b/imports/startup/client/ravenLogger.js index adbe66f..d7fc16e 100644 --- a/imports/startup/client/ravenLogger.js +++ b/imports/startup/client/ravenLogger.js @@ -2,11 +2,11 @@ import RavenLogger from 'meteor/flowkey:raven'; import { Meteor } from 'meteor/meteor'; const ravenOptions = {}; - -const enabled = true && !Meteor.isDevelopment; +const publicDSN = Meteor.settings.public.sentryPublicDSN; +const enabled = !Meteor.isDevelopment && publicDSN; const ravenLogger = enabled ? new RavenLogger({ - publicDSN: Meteor.settings.public.sentryPublicDSN, // will be used on the client + publicDSN, // will be used on the client shouldCatchConsoleError: true, // default trackUser: true // default }, ravenOptions) : { log: (error) => { console.log(error); } }; diff --git a/imports/startup/server/ravenLogger.js b/imports/startup/server/ravenLogger.js index 868132a..e6bd2e9 100644 --- a/imports/startup/server/ravenLogger.js +++ b/imports/startup/server/ravenLogger.js @@ -2,12 +2,14 @@ import RavenLogger from 'meteor/flowkey:raven'; import { Meteor } from 'meteor/meteor'; const ravenOptions = {}; +const publicDSN = Meteor.settings.public.sentryPublicDSN; +const privateDSN = Meteor.settings.sentryPrivateDSN; -const ravenLogger = new RavenLogger({ - publicDSN: Meteor.settings.public.sentryPublicDSN, - privateDSN: Meteor.settings.sentryPrivateDSN, +const ravenLogger = publicDSN && privateDSN ? new RavenLogger({ + publicDSN, + privateDSN, shouldCatchConsoleError: true, // default true trackUser: true // default false -}, ravenOptions); +}, ravenOptions) : { log: (error) => { console.log(error); } }; export default ravenLogger; From 391b8935bec71314990d695186bde53fb74cbefc Mon Sep 17 00:00:00 2001 From: vjrj Date: Mon, 21 May 2018 17:43:05 +0200 Subject: [PATCH 05/84] Formatted messages --- public/locales/es/common.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/locales/es/common.json b/public/locales/es/common.json index a460481..d6ec163 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -335,6 +335,5 @@ "Hay más información sobre un fuego": "Hay más información sobre un fuego", "esDecirTalDia": "Es decir el {{date}}", - "Actualizando...": - "Actualizando..." + "Actualizando...": "Actualizando..." } From 245619f4e43d16672cb2d01aa349c505fc4a9db4 Mon Sep 17 00:00:00 2001 From: vjrj Date: Mon, 21 May 2018 17:43:17 +0200 Subject: [PATCH 06/84] Tests more robusts --- cucumber/features/feedback.feature | 1 - cucumber/features/login.feature | 1 + cucumber/features/steps_definitions/hooks.js | 4 +- cucumber/features/steps_definitions/login.js | 40 ++++++++++++-------- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/cucumber/features/feedback.feature b/cucumber/features/feedback.feature index 8c759e3..3bbb113 100644 --- a/cucumber/features/feedback.feature +++ b/cucumber/features/feedback.feature @@ -1,4 +1,3 @@ -@watch Feature: Allow users to give feedback As a user (authenticated or not) diff --git a/cucumber/features/login.feature b/cucumber/features/login.feature index 38fe003..a723e56 100644 --- a/cucumber/features/login.feature +++ b/cucumber/features/login.feature @@ -1,3 +1,4 @@ +@watch Feature: Allow users to login and logout As a user diff --git a/cucumber/features/steps_definitions/hooks.js b/cucumber/features/steps_definitions/hooks.js index 9e3e611..712582c 100644 --- a/cucumber/features/steps_definitions/hooks.js +++ b/cucumber/features/steps_definitions/hooks.js @@ -5,14 +5,14 @@ this.Before(() => { // global.expect = require('@xolvio/jasmine-expect').expect; - /* Not used + // https://github.com/webdriverio/webdriverio/issues/1145 if (!this.initMyCmds) { client.addCommand('waitForClickable', function elementClickable(selector, timeout) { this.waitForVisible(selector, timeout); this.waitForEnabled(selector, timeout); }); this.initMyCmds = true; - } */ + } client.windowHandleSize({ width: 1500, height: 1000 }); diff --git a/cucumber/features/steps_definitions/login.js b/cucumber/features/steps_definitions/login.js index f580e1d..c509ba0 100644 --- a/cucumber/features/steps_definitions/login.js +++ b/cucumber/features/steps_definitions/login.js @@ -10,7 +10,7 @@ let pass; let email; const setUserValues = (isEdit) => { - client.waitForVisible('input[name="firstName"]', 5000); + client.waitForVisible('input[name="firstName"]', 10000); firstName = randomUsername(); lastName = randomUsername(); email = randomEmail(); @@ -32,11 +32,22 @@ module.exports = function doSteps(notos) { goHome(client); }); + function waitNoBert() { + client.pause(2000); + if (client.isVisible('.bert-container')) { + console.log('Removing bert alert'); + client.click('.bert-container'); + client.waitForVisible('.bert-container', 10000, true); + } else { + console.log('No bert alert'); + } + } + function register() { if (client.isVisible('#logout')) { client.click('#logout'); } - client.waitForVisible('#signup', 5000); + client.waitForVisible('#signup', 10000); client.click('#signup'); setUserValues(); if (!notos) { @@ -57,6 +68,7 @@ module.exports = function doSteps(notos) { this.Then(/^I should be logged in$/, () => { client.waitUntil(() => client.isVisible('#logout') === true, 10000); + client.waitUntil(() => client.isVisible('#profile') === true, 10000); }); /* this.Given(/^I am signed out$/, () => { @@ -64,30 +76,24 @@ module.exports = function doSteps(notos) { * }); */ - function closeAlert() { - // client.waitForVisible('.bert-alert', 5000); - // client.click('.bert-content'); - client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000); - } this.When(/^I sign out$/, () => { - if (client.isVisible('.bert-alert')) { - client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000); - } - client.waitForVisible('#logout', 10000); + waitNoBert(); + client.waitForClickable('#logout', 10000); + // client.waitForVisible('#logout', 10000); client.click('#logout'); client.waitForVisible('#login', 10000); }); function login(badpass) { + waitNoBert(); client.waitForVisible('#login', 10000); - client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000); client.click('#login'); client.waitForVisible('#loginSubmit', 5000); client.setValue('input[name="emailAddress"]', email); client.setValue('input[name="password"]', badpass ? randomPassword() : pass); client.click('#loginSubmit'); - closeAlert(); + waitNoBert(); } this.When(/^I enter my email and password$/, () => { @@ -95,11 +101,13 @@ module.exports = function doSteps(notos) { }); this.Then(/^I can edit my profile$/, () => { - client.waitForVisible('#profile', 5000); + waitNoBert(); + client.waitForClickable('#profile', 10000); client.click('#profile'); + client.pause(2000); setUserValues(true); client.click('#profileSubmit'); - closeAlert(); + waitNoBert(); checkName(); }); @@ -117,7 +125,7 @@ module.exports = function doSteps(notos) { this.Then(/^I should be registered$/, () => { client.waitForVisible('.bert-alert', 10000, true); - closeAlert(); + waitNoBert(); }); this.Given(/^I register with some name, password and email but without accept conditions$/, () => { From b53e5d0e36066046f18b43ec9c87c46c8acd6400 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 22 May 2018 18:19:37 +0200 Subject: [PATCH 07/84] Log ravenLogger enabled/disabled --- imports/startup/client/ravenLogger.js | 2 ++ imports/startup/server/ravenLogger.js | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/imports/startup/client/ravenLogger.js b/imports/startup/client/ravenLogger.js index d7fc16e..4440499 100644 --- a/imports/startup/client/ravenLogger.js +++ b/imports/startup/client/ravenLogger.js @@ -11,4 +11,6 @@ const ravenLogger = enabled ? new RavenLogger({ trackUser: true // default }, ravenOptions) : { log: (error) => { console.log(error); } }; +console.log(`ravenLogger ${enabled ? 'enabled' : 'disabled'}`); + export default ravenLogger; diff --git a/imports/startup/server/ravenLogger.js b/imports/startup/server/ravenLogger.js index e6bd2e9..b3b83a3 100644 --- a/imports/startup/server/ravenLogger.js +++ b/imports/startup/server/ravenLogger.js @@ -4,12 +4,15 @@ import { Meteor } from 'meteor/meteor'; const ravenOptions = {}; const publicDSN = Meteor.settings.public.sentryPublicDSN; const privateDSN = Meteor.settings.sentryPrivateDSN; +const enabled = publicDSN && privateDSN; -const ravenLogger = publicDSN && privateDSN ? new RavenLogger({ +const ravenLogger = enabled ? new RavenLogger({ publicDSN, privateDSN, shouldCatchConsoleError: true, // default true trackUser: true // default false }, ravenOptions) : { log: (error) => { console.log(error); } }; +console.log(`ravenLogger ${enabled ? 'enabled' : 'disabled'}`); + export default ravenLogger; From b54707f2dc8740ecfd9fa91f496b4a95b5454332 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 23 May 2018 06:29:31 +0200 Subject: [PATCH 08/84] Improved cucumber tests --- cucumber/features/login.feature | 1 - cucumber/features/pages.feature | 25 +++++++++++++++---- .../features/steps_definitions/feedback.js | 1 + cucumber/features/steps_definitions/hooks.js | 8 ++++++ cucumber/features/steps_definitions/login.js | 21 ++++++++++++---- cucumber/features/steps_definitions/pages.js | 11 +++----- 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/cucumber/features/login.feature b/cucumber/features/login.feature index a723e56..38fe003 100644 --- a/cucumber/features/login.feature +++ b/cucumber/features/login.feature @@ -1,4 +1,3 @@ -@watch Feature: Allow users to login and logout As a user diff --git a/cucumber/features/pages.feature b/cucumber/features/pages.feature index ac66526..74afc36 100644 --- a/cucumber/features/pages.feature +++ b/cucumber/features/pages.feature @@ -10,8 +10,9 @@ Feature: Test all secundary pages | about | About | Then I check that all pages works properly - Scenario: Check that all secondary pages work well - Given a list of page urls and contents + Scenario: Check that all secondary pages work well when not logged + Given I logout if logged + And a list of page urls and contents | login | Login | true | | signup | Sign Up | true | | fires | Fires | true | @@ -21,12 +22,26 @@ Feature: Test all secundary pages | fire/inexistent | This page doesn't exist | false | | fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false | Then I check that all page urls works properly + # And they are spiderable + @watch + Scenario: Check that all secondary pages work well when logged + Given I have an account and I logged in + And a list of page urls and contents + | status | Status | false | + | fires | Fires | true | + | zones | Monitored | true | + | verify-email/something | Verify | true | + | fire/inexistent | This page doesn't exist | false | + | fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false | + Then I check that all page urls works properly + + @watch Scenario: Check that other non visible pages work well - Given a list of non visible pages ids and contents - | status | Status | - | error | Upps | + Given I logout if logged + And a list of non visible pages ids and contents + | error | Upppps | false | # FIXME: find a way to monitor json pages # | api/v1/status/last-fire-detected | updateAt | # | api/v1/status/last-fire-check | description | diff --git a/cucumber/features/steps_definitions/feedback.js b/cucumber/features/steps_definitions/feedback.js index f25a3b3..ffd78be 100644 --- a/cucumber/features/steps_definitions/feedback.js +++ b/cucumber/features/steps_definitions/feedback.js @@ -24,6 +24,7 @@ module.exports = function doSteps() { client.waitForVisible('#feedback-tab', 10000); client.click('#feedback-tab'); client.waitForVisible('#feedback-form', 10000); + client.waitForEnabled('input[name="email"]'); client.setValue('input[name="email"]', randomEmail()); client.setValue('#feedbackTextarea', randomText(500)); client.click('#sendFeedbackBtn'); diff --git a/cucumber/features/steps_definitions/hooks.js b/cucumber/features/steps_definitions/hooks.js index 712582c..574b9c4 100644 --- a/cucumber/features/steps_definitions/hooks.js +++ b/cucumber/features/steps_definitions/hooks.js @@ -11,6 +11,14 @@ this.waitForVisible(selector, timeout); this.waitForEnabled(selector, timeout); }); + + client.addCommand('waitUntilText', function waitUntilText(selector, text, ms) { + ms = ms || 5000; + this.waitForVisible(selector); + const self = this; + this.waitUntil(() => self.getText(selector).includes(text), ms, `expected text in ${selector} be different after ${ms / 1000}s. Obtained: '${self.getText(selector)}' expected: '${text}'`); + }); + this.initMyCmds = true; } diff --git a/cucumber/features/steps_definitions/login.js b/cucumber/features/steps_definitions/login.js index c509ba0..b255b96 100644 --- a/cucumber/features/steps_definitions/login.js +++ b/cucumber/features/steps_definitions/login.js @@ -35,35 +35,46 @@ module.exports = function doSteps(notos) { function waitNoBert() { client.pause(2000); if (client.isVisible('.bert-container')) { - console.log('Removing bert alert'); + // console.log('Removing bert alert'); client.click('.bert-container'); client.waitForVisible('.bert-container', 10000, true); } else { - console.log('No bert alert'); + // console.log('No bert alert'); } } - function register() { + function logoutIfLogged() { if (client.isVisible('#logout')) { client.click('#logout'); } client.waitForVisible('#signup', 10000); + } + + function register() { + logoutIfLogged(); + client.waitForVisible('#signup', 10000); client.click('#signup'); setUserValues(); if (!notos) { client.click('input[name="tos"]'); } client.click('#signUpSubmit'); + client.waitForVisible('#logout', 10000); } + this.Given(/^I logout if logged$/, () => { + waitNoBert(); + logoutIfLogged(); + }); + this.Given(/^I have an account and I logged in$/, () => { register(); }); function checkName() { client.waitForVisible('#profile', 5000); - client.waitForText('#profile', firstName); - client.waitForText('#profile', lastName); + client.waitUntilText('#profile', firstName); + client.waitUntilText('#profile', lastName); } this.Then(/^I should be logged in$/, () => { diff --git a/cucumber/features/steps_definitions/pages.js b/cucumber/features/steps_definitions/pages.js index 38e61b0..29f6aab 100644 --- a/cucumber/features/steps_definitions/pages.js +++ b/cucumber/features/steps_definitions/pages.js @@ -16,7 +16,7 @@ module.exports = function () { const content = pages[i][1]; client.waitForVisible(id, 10000); client.click(id); - client.waitForText('#react-root', content); + client.waitUntilText('#react-root', content); // https://jasmine.github.io/2.3/introduction.html#section-Expectations expect(client.getTitle()).toContain(pages[i][1]); } @@ -26,7 +26,7 @@ module.exports = function () { const content = pages[i][1]; client.url(`${process.env.ROOT_URL}/${url}`); client.waitForVisible('#react-root', 10000); - client.waitForText('#react-root', content); + client.waitUntilText('#react-root', content); const checkTitle = pages[i][2] === 'true'; // https://jasmine.github.io/2.3/introduction.html#section-Expectations if (checkTitle) { @@ -41,10 +41,7 @@ module.exports = function () { this.Then(/^I check that all non visible pages works properly$/, (callback) => { for (let i = 0; i < pages.length; i += 1) { - client.url(`${process.env.ROOT_URL}/${pages[i][0]}`); - const content = pages[i][1]; - client.waitForVisible('#react-root', 10000); - client.waitForText('#react-root', content); + processPageUrl(i); } callback(); }); @@ -77,7 +74,7 @@ module.exports = function () { // Write code here that turns the phrase above into concrete actions for (let i = 0; i < pages.length; i += 1) { client.url(`${process.env.ROOT_URL}/${pages[i][0]}?_escaped_fragment_=`); - client.waitForText('#react-root', pages[i][1]); + client.waitUntilText('#react-root', pages[i][1]); } callback(); }); From 783967c0e112a010a4b3c2af8f8d9a208e8c5f56 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 23 May 2018 10:53:35 +0200 Subject: [PATCH 09/84] Log console errors in tests. .meteorignore --- .meteorignore | 3 +++ cucumber/features/pages.feature | 3 +++ cucumber/features/steps_definitions/hooks.js | 25 ++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 .meteorignore diff --git a/.meteorignore b/.meteorignore new file mode 100644 index 0000000..b25101c --- /dev/null +++ b/.meteorignore @@ -0,0 +1,3 @@ +# Don't reload meteor server when we modify or do test +cucumber +output.json diff --git a/cucumber/features/pages.feature b/cucumber/features/pages.feature index 74afc36..fd2193b 100644 --- a/cucumber/features/pages.feature +++ b/cucumber/features/pages.feature @@ -1,5 +1,8 @@ Feature: Test all secundary pages + Background: + Given I am on the home page + Scenario: Check that all secondary pages work well Given a list of page ids and contents | license | License | diff --git a/cucumber/features/steps_definitions/hooks.js b/cucumber/features/steps_definitions/hooks.js index 574b9c4..64b405a 100644 --- a/cucumber/features/steps_definitions/hooks.js +++ b/cucumber/features/steps_definitions/hooks.js @@ -2,6 +2,31 @@ (function () { module.exports = function () { + const printLog = (type) => { + client.log(type).value.forEach((logItem) => { + const { source, message } = logItem; + if (type === 'browser' && source === 'console-api') { + // Clean messages of type: + // http://localhost:3000/app/app.js?hash=34da4258bd2caa4e14c7c0c20ba6396f309e7236 13325:18 "GMaps script just loaded" + // to get the last part + const cleanMessage = message.replace(/[^ ]{1,} [^ ]{1,} "(.*)"$/g, '$1'); + console.log(`${logItem.level}: ${cleanMessage}`); + } else { + // Right now we don't need logs like: + // %cDownload the React DevTools for a better development experience: https://fb.me/react-devtools + // console.log(`${logItem.level}: ${message}`); + } + }); + }; + + this.StepResult((stepResult) => { + if (stepResult.getStatus() === 'failed') { + // http://webdriver.io/api/protocol/log.html + console.log(`Step result: ${stepResult.getStatus()}`); + printLog('browser'); + } + }); + this.Before(() => { // global.expect = require('@xolvio/jasmine-expect').expect; From 12bf1ef09d273053103d4e5ea62df8562da84c51 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 23 May 2018 11:05:30 +0200 Subject: [PATCH 10/84] RavenLogger in dev if enabled --- imports/startup/client/ravenLogger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/startup/client/ravenLogger.js b/imports/startup/client/ravenLogger.js index 4440499..d678591 100644 --- a/imports/startup/client/ravenLogger.js +++ b/imports/startup/client/ravenLogger.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; const ravenOptions = {}; const publicDSN = Meteor.settings.public.sentryPublicDSN; -const enabled = !Meteor.isDevelopment && publicDSN; +const enabled = !!publicDSN; const ravenLogger = enabled ? new RavenLogger({ publicDSN, // will be used on the client From 37e42b70f1522b529c69c9d1c15c5d1fcf27e2a6 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 23 May 2018 11:33:54 +0200 Subject: [PATCH 11/84] FireStats null when empty --- imports/ui/pages/FiresMap/FireStats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/ui/pages/FiresMap/FireStats.js b/imports/ui/pages/FiresMap/FireStats.js index f88245c..a4742ef 100644 --- a/imports/ui/pages/FiresMap/FireStats.js +++ b/imports/ui/pages/FiresMap/FireStats.js @@ -44,7 +44,7 @@ class FireStats extends Component { if (this.state.lastCheck && this.state.lastFireDetected) { return (
Actualizado , último fuego detectado .
); } - return (); + return null; } } From 3e5700f648a304bafdedd0ae41a684681965f65b Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 12 Jun 2018 06:08:11 +0200 Subject: [PATCH 12/84] Greater icon --- designs/icons-tcef.svg | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/designs/icons-tcef.svg b/designs/icons-tcef.svg index a35fdfa..0835ab8 100644 --- a/designs/icons-tcef.svg +++ b/designs/icons-tcef.svg @@ -247,7 +247,7 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.23559314" - inkscape:cx="-1825.0924" + inkscape:cx="-2890.4884" inkscape:cy="-486.23766" inkscape:document-units="mm" inkscape:current-layer="layer1" @@ -621,5 +621,39 @@ y="0" x="0" /> + + + + + + + From eefa5860073a91c29cf4baa28a8f5cff62b01fc6 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 12 Jun 2018 06:08:25 +0200 Subject: [PATCH 13/84] Return unauth --- imports/startup/server/rest.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/imports/startup/server/rest.js b/imports/startup/server/rest.js index 423922f..3e919d5 100644 --- a/imports/startup/server/rest.js +++ b/imports/startup/server/rest.js @@ -65,7 +65,7 @@ Meteor.startup(() => { } }); - // Maps to: /api/v1/status/last-fire-detected + // Maps to: /api/v1/status/last-fires-count apiV1.addRoute('status/active-fires-count', { authRequired: false }, { get: function get() { return { total: ActiveFiresCollection.find({}).count() }; @@ -80,7 +80,8 @@ Meteor.startup(() => { }); // Maps to: /api/v1/fires-in/:lat/:lng/:km - // Ex: http://127.0.0.1:3000/api/v1/fires-in/38.736946/-9.142685/100 + // 100 km max + // Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { get: function get() { const lat = Number(this.urlParams.lat); @@ -94,7 +95,7 @@ Meteor.startup(() => { if (token !== Meteor.settings.private.internalApiToken) { console.warn(`WARNING: Query for fires in ${lat}, ${lng} in ${km} km radius with wrong token`); - return {}; + return { error: 'Unauthorized' }; } console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); From e28886e03108b4bfc49c3144208259656ac4d6b7 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 12 Jun 2018 06:09:55 +0200 Subject: [PATCH 14/84] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0f4ef0a..eeb3622 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ npm-debug.log .deploy/ public/locales/undefined/ .deploy-staging +.screenshots +.vscode +output.json From 5e5ecb69b8e8a3724874df90e0075921c0f2e7ae Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 12 Jun 2018 06:10:19 +0200 Subject: [PATCH 15/84] Wait for settings --- imports/ui/pages/FiresMap/FiresMap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/ui/pages/FiresMap/FiresMap.js b/imports/ui/pages/FiresMap/FiresMap.js index 265b0cc..a5573b5 100644 --- a/imports/ui/pages/FiresMap/FiresMap.js +++ b/imports/ui/pages/FiresMap/FiresMap.js @@ -453,7 +453,7 @@ export default translate([], { wait: true })(withTracker(() => { const lastFireDetected = ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); return { - loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready()), + loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready()), userSubs: userSubs ? userSubs.value : null, userSubsBounds: userSubs ? userSubsBounds.value : null, subsready: settingsSubs.ready(), From ec0edf4475cee371a86b3701122d2152ac9eb839 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 12 Jun 2018 08:05:44 +0200 Subject: [PATCH 16/84] More rest methods --- imports/startup/server/rest.js | 91 +++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/imports/startup/server/rest.js b/imports/startup/server/rest.js index 3e919d5..4c8fd86 100644 --- a/imports/startup/server/rest.js +++ b/imports/startup/server/rest.js @@ -8,6 +8,9 @@ import { check } from 'meteor/check'; import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; +import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; +import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; +import Industries from '/imports/api/Industries/Industries'; Meteor.startup(() => { const uptime = new Date(); @@ -79,41 +82,71 @@ Meteor.startup(() => { } }); + function getFires(route, full) { + const lat = Number(route.urlParams.lat); + const lng = Number(route.urlParams.lng); + const km = Number(route.urlParams.km); + const { token } = route.urlParams; + check(lng, NumberBetween(-180, 180)); + check(lat, NumberBetween(-90, 90)); + check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100)); + check(token, String); + + if (token !== Meteor.settings.private.internalApiToken) { + console.warn(`WARNING: Query for fires in ${lat}, ${lng} in ${km} km radius with wrong token`); + return { error: 'Unauthorized' }; + } + + console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); + + const fires = ActiveFiresCollection.find({ + ourid: { + $near: { + $geometry: { + type: 'Point', + coordinates: [lng, lat] + }, + $maxDistance: km * 1000, + $minDistance: 0 + } + } + }, { + fields: { + lat: 1, + lon: 1, + when: 1, + scan: 1 + } + }); + + if (!full) { + return { total: fires.count(), real: countRealFires(fires) }; + } + + // TODO only get real + const firesA = fires.fetch(); + if (firesA.length > 0) { + const union = firesUnion(fires); + const falsePos = whichAreFalsePositives(FalsePositives, union); + const industries = whichAreFalsePositives(Industries, union); + return { fires: firesA, falsePos: falsePos.fetch(), industries: industries.fetch() }; + } + return { fires: [] }; + } + + // Maps to: /api/v1/fires-in/:lat/:lng/:km // 100 km max // Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { get: function get() { - const lat = Number(this.urlParams.lat); - const lng = Number(this.urlParams.lng); - const km = Number(this.urlParams.km); - const { token } = this.urlParams; - check(lng, NumberBetween(-180, 180)); - check(lat, NumberBetween(-90, 90)); - check(km, NumberBetween(0, 100)); - check(token, String); + return getFires(this, false); + } + }); - if (token !== Meteor.settings.private.internalApiToken) { - console.warn(`WARNING: Query for fires in ${lat}, ${lng} in ${km} km radius with wrong token`); - return { error: 'Unauthorized' }; - } - - console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); - - const fires = ActiveFiresCollection.find({ - ourid: { - $near: { - $geometry: { - type: 'Point', - coordinates: [lng, lat] - }, - $maxDistance: km * 1000, - $minDistance: 0 - } - } - }); - - return { total: fires.count(), real: countRealFires(fires) }; + apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, { + get: function get() { + return getFires(this, true); } }); }); From 9dbd97009b9d604d55536bb974c69e9047ca8d1a Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 12 Jun 2018 10:21:55 +0200 Subject: [PATCH 17/84] Improved rest methods --- imports/startup/server/rest.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/imports/startup/server/rest.js b/imports/startup/server/rest.js index 4c8fd86..2529a5e 100644 --- a/imports/startup/server/rest.js +++ b/imports/startup/server/rest.js @@ -119,8 +119,10 @@ Meteor.startup(() => { } }); + const result = { total: fires.count(), real: countRealFires(fires) }; + if (!full) { - return { total: fires.count(), real: countRealFires(fires) }; + return result; } // TODO only get real @@ -129,9 +131,16 @@ Meteor.startup(() => { const union = firesUnion(fires); const falsePos = whichAreFalsePositives(FalsePositives, union); const industries = whichAreFalsePositives(Industries, union); - return { fires: firesA, falsePos: falsePos.fetch(), industries: industries.fetch() }; + result.fires = firesA; + result.industries = industries.fetch(); + result.falsePos = falsePos.fetch(); + return result; } - return { fires: [] }; + + // otherwise + return { + total: 0, real: 0, fires: [], falsePos: [], industries: [] + }; } From d551dea425b3e226ff74e48e27566d5c83c696d2 Mon Sep 17 00:00:00 2001 From: vjrj Date: Thu, 14 Jun 2018 17:54:51 +0200 Subject: [PATCH 18/84] Added done() to test --- test/encode.test.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/encode.test.js b/test/encode.test.js index 4e4a5dd..e7ff306 100644 --- a/test/encode.test.js +++ b/test/encode.test.js @@ -57,21 +57,23 @@ async function encAndDec(obj) { } describe('url encoding', () => { - it('should encrypt and dcrypt basic objects', async () => { + it('should encrypt and dcrypt basic objects', async (done) => { const obj = { lat: 2 }; const sealed = await urlEnc.encrypt(obj); const unsealed = await urlEnc.decrypt(sealed); chai.expect(unsealed).to.deep.equal(obj); + done(); }); - it('should encrypt and dcrypt objects with date', async () => { + it('should encrypt and dcrypt objects with date', async (done) => { const obj = { lat: 40.234503, lon: -3.350386, when: (new Date()).toString() }; const sealed = await urlEnc.encrypt(obj); const unsealed = await urlEnc.decrypt(sealed); chai.expect(unsealed).to.deep.equal(obj); + done(); }); - it('should encrypt complete objects', async () => { + it('should encrypt complete objects', async (done) => { const obj = { ourid: { type: 'Point', @@ -105,13 +107,15 @@ describe('url encoding', () => { chai.expect(sealed).to.not.equal(sealed2); chai.expect(sealed).to.not.include('/'); chai.expect(sealed).to.not.include('%'); + done(); }); // This fails because Date is return as String (not as Date) and because _id - it('should encrypt and dcrypt collection objects', async () => { + it('should encrypt and dcrypt collection objects', async (done) => { seeder(ActiveFiresCollection, firesSeed()); const obj = ActiveFiresCollection.findOne(); encAndDec(obj); + done(); }); it('should decrypt node-red enc objects', async () => { @@ -120,7 +124,7 @@ describe('url encoding', () => { // console.log(unsealed); }); - it('should encrypt and dcrypt Accounts hashes', async () => { + it('should encrypt and dcrypt Accounts hashes', async (done) => { const token = Accounts._generateStampedLoginToken(); const obj = Accounts._hashStampedToken(token); console.log(obj); @@ -129,13 +133,15 @@ describe('url encoding', () => { const w = unsealed.when; unsealed.when = new Date(w); chai.expect(unsealed).to.deep.equal(obj); + done(); }); // limit 0 for no limit and test everything (and increase timeout) - it('should encrypt and dcrypt all collection objects', async () => { + it('should encrypt and dcrypt all collection objects', async (done) => { seeder(ActiveFiresCollection, firesSeed()); ActiveFiresCollection.find({}, { limit: 1000 }).fetch().forEach((obj) => { encAndDec(obj); }); + done(); }).timeout(5000); }); From eebbdda064c045230785dfe163e5821e4957ff15 Mon Sep 17 00:00:00 2001 From: vjrj Date: Thu, 14 Jun 2018 17:55:38 +0200 Subject: [PATCH 19/84] Rest api updated --- imports/api/Rest/Rest.js | 322 +++++++++++++++++++++++++++ imports/api/Subscriptions/methods.js | 66 +++--- imports/api/Users/Users.js | 1 + imports/startup/server/rest.js | 162 +------------- package-lock.json | 25 +-- package.json | 1 + test/rest.test.js | 219 ++++++++++++++++++ 7 files changed, 591 insertions(+), 205 deletions(-) create mode 100644 imports/api/Rest/Rest.js create mode 100644 test/rest.test.js diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js new file mode 100644 index 0000000..b3c63b9 --- /dev/null +++ b/imports/api/Rest/Rest.js @@ -0,0 +1,322 @@ +/* global Restivus */ +/* eslint-disable import/no-absolute-path */ + +import { Meteor } from 'meteor/meteor'; +import { NumberBetween } from '/imports/modules/server/other-checks'; +import Fires from '/imports/api/Fires/Fires'; +import { check } from 'meteor/check'; +import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; +import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; +import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; +import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; +import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; +import Industries from '/imports/api/Industries/Industries'; +import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; +import jsend from 'jsend'; +import { Random } from 'meteor/random'; +import { subscriptionsInsert } from '/imports/api/Subscriptions/methods.js'; + +const debug = false; + +const uptime = new Date(); + + +function fail(e) { + return jsend.error(`Unexpected error in REST call: ${e}`); +} + +function defaultFailParams(e) { + return jsend.error(`Wrong REST params: ${e}`); +} + +function checkAuthToken(token) { + if (token !== Meteor.settings.private.internalApiToken) { + const message = `Unauthorized auth token '${token}' in REST API`; + console.warn(message); + return jsend.error(message); + } + return undefined; +} + +function checkLatLonDist(km, lat, lng) { + check(lng, NumberBetween(-180, 180)); + check(lat, NumberBetween(-90, 90)); + check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100)); +} + + +// export +const apiV1 = new Restivus({ + useDefaultAuth: true, + apiPath: 'api', + version: 'v1', + prettyJson: true +}); + +// Generates: POST on /api/users and GET, DELETE /api/users/:id for +// Meteor.users collection +/* apiV1.addCollection(Meteor.users, { + * excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'], + * routeOptions: { + * authRequired: true + * }, + * endpoints: { + * post: { + * authRequired: false + * }, + * delete: { + * roleRequired: 'admin' + * } + * } + * }); */ + +apiV1.addCollection(Fires, { + excludedEndpoints: ['put', 'post', 'patch', 'delete'], + // excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'], + routeOptions: { + authRequired: false + }, + endpoints: { + get: { + action: function getFire() { + return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); + } + } + } +}); + +// Maps to: /api/v1/status/last-fire-check +apiV1.addRoute('status/last-fire-check', { authRequired: false }, { + get: function get() { + return SiteSettings.findOne({ name: 'last-fire-check' }); + } +}); + +// Maps to: /api/v1/status/last-fire-detected +apiV1.addRoute('status/last-fire-detected', { authRequired: false }, { + get: function get() { + return ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); + } +}); + +// Maps to: /api/v1/status/active-fires-count +apiV1.addRoute('status/active-fires-count', { authRequired: false }, { + get: function get() { + return { total: ActiveFiresCollection.find({}).count() }; + } +}); + +// Maps to: /api/v1/status/uptime +apiV1.addRoute('status/uptime', { authRequired: false }, { + get: function get() { + return { ms: new Date() - uptime }; + } +}); + +function getFires(route, full) { + const lat = Number(route.urlParams.lat); + const lng = Number(route.urlParams.lng); + const km = Number(route.urlParams.km); + const { token } = route.urlParams; + try { + checkLatLonDist(km, lat, lng); + check(token, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); + + const fires = ActiveFiresCollection.find({ + ourid: { + $near: { + $geometry: { + type: 'Point', + coordinates: [lng, lat] + }, + $maxDistance: km * 1000, + $minDistance: 0 + } + } + }, { + fields: { + lat: 1, + lon: 1, + when: 1, + scan: 1 + } + }); + + const result = { total: fires.count(), real: countRealFires(fires) }; + + if (!full) { + return result; + } + + // TODO only get real + const firesA = fires.fetch(); + if (firesA.length > 0) { + const union = firesUnion(fires); + const falsePos = whichAreFalsePositives(FalsePositives, union); + const industries = whichAreFalsePositives(Industries, union); + result.fires = firesA; + result.industries = industries.fetch(); + result.falsePos = falsePos.fetch(); + return result; + } + + // otherwise + return { + total: 0, real: 0, fires: [], falsePos: [], industries: [] + }; +} + +// Maps to: /api/v1/fires-in/:lat/:lng/:km +// 100 km max +// Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 +apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { + get: function get() { + return getFires(this, false); + } +}); + +apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, { + get: function get() { + return getFires(this, true); + } +}); + +// Add mobile user: +// curl -X POST http://localhost:3000/api/v1/users/mobile -d "token: thisAppAutToken" -d "mobileToken: user-mobile-firebase-token" +// Response: +// +// https://docs.meteor.com/api/passwords.html#Accounts-createUser +apiV1.addRoute('mobile/users', { authRequired: false }, { + post: function post() { + const { token, mobileToken, lang } = this.bodyParams; + try { + check(token, String); + check(lang, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + let username; + + do { + username = Random.id(15); + } while (Meteor.users.find({ username }).count() !== 0); + + // FIXME check valid lang + + const now = new Date(); + // Accounts.createUser({ username: mobileToken, lang: 'FIXME' profile: {} }); + const result = Meteor.users.upsert({ + fireBaseToken: mobileToken + }, { + $set: { + username, + fireBaseToken: mobileToken, + lang, + profile: { }, + createdAt: now, + updatedAt: now + } + }); + + if (debug) { + console.log(this.bodyParams); + console.log(this.urlParams); + console.log(this.queryParams); + } + + const newUser = Meteor.users.findOne({ username }); + + return jsend.success({ + upsertResult: result, + username, + userId: newUser._id, + lang: newUser.lang, + mobileToken: newUser.fireBaseToken + }); + } + +}); + +// max sitance: 100km +apiV1.addRoute('mobile/subscriptions', { authRequired: false }, { + post: function post() { + const { + token, + mobileToken, + lat, + lon, + distance + } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + check(lat, Number); + check(lon, Number); + check(distance, Number); + checkLatLonDist(distance, lat, lon); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + + const newSubs = {}; + newSubs.location = {}; + newSubs.location.lat = lat; + newSubs.location.lon = lon; + newSubs.distance = distance; + + let result; + try { + result = subscriptionsInsert(newSubs, user._id, 'mobile'); + } catch (e) { + return fail(e); + } + return jsend.success({ subsId: result._str }); + }, + delete: function del() { + const { + token, + mobileToken, + subsId + } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + check(subsId, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + + try { + Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) }); + } catch (e) { + return fail(e); + } + + return jsend.success({}); + } +}); diff --git a/imports/api/Subscriptions/methods.js b/imports/api/Subscriptions/methods.js index 618ed21..69456ef 100644 --- a/imports/api/Subscriptions/methods.js +++ b/imports/api/Subscriptions/methods.js @@ -10,30 +10,48 @@ function geo(doc) { }; } +export function subscriptionsInsert(doc, userId, type) { + check(doc, { + location: Match.ObjectIncluding({ lat: Number, lon: Number }), + distance: Number + }); + const newDoc = { + owner: userId, + type, + geo: geo(doc), + ...doc + }; + // console.log(newDoc); + const already = Subscriptions.findOne(newDoc); + if (already) { + throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area'); + } + try { + return Subscriptions.insert(newDoc); + } catch (exception) { + // console.error(exception); + throw new Meteor.Error('500', exception); + } +} + +export function subscriptionsRemove(subscriptionId) { + check(subscriptionId, Meteor.Collection.ObjectID); + + try { + return Subscriptions.remove(subscriptionId); + } catch (exception) { + console.error(exception); + throw new Meteor.Error('500', exception); + } +} + Meteor.methods({ - 'subscriptions.insert': function subscriptionsInsert(doc) { + 'subscriptions.insert': function subscriptionInsertMethod(doc) { check(doc, { location: Match.ObjectIncluding({ lat: Number, lon: Number }), distance: Number }); - const type = 'web'; - const newDoc = { - owner: this.userId, - type, - geo: geo(doc), - ...doc - }; - // console.log(newDoc); - const already = Subscriptions.findOne(newDoc); - if (already) { - throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area'); - } - try { - return Subscriptions.insert(newDoc); - } catch (exception) { - // console.error(exception); - throw new Meteor.Error('500', exception); - } + return subscriptionsInsert(doc, this.userId, 'web'); }, 'subscriptions.update': function subscriptionsUpdate(doc) { check(doc, { @@ -52,15 +70,9 @@ Meteor.methods({ throw new Meteor.Error('500', exception); } }, - 'subscriptions.remove': function subscriptionsRemove(subscriptionId) { + 'subscriptions.remove': function subscriptionsRemoveMethod(subscriptionId) { check(subscriptionId, Meteor.Collection.ObjectID); - - try { - return Subscriptions.remove(subscriptionId); - } catch (exception) { - console.error(exception); - throw new Meteor.Error('500', exception); - } + return subscriptionsRemove(subscriptionId); } }); diff --git a/imports/api/Users/Users.js b/imports/api/Users/Users.js index c6858ae..8911a47 100644 --- a/imports/api/Users/Users.js +++ b/imports/api/Users/Users.js @@ -107,6 +107,7 @@ const schemaUser = new SimpleSchema({ telegramUsername: { type: String, optional: true }, telegramFirstName: { type: String, optional: true }, telegramLanguageCode: { type: String, optional: true }, + fireBaseToken: { type: String, optional: true }, createdAt: defaultCreatedAt, updatedAt: defaultUpdateAt }); diff --git a/imports/startup/server/rest.js b/imports/startup/server/rest.js index 2529a5e..64e8172 100644 --- a/imports/startup/server/rest.js +++ b/imports/startup/server/rest.js @@ -1,161 +1 @@ -/* global Restivus */ -/* eslint-disable import/no-absolute-path */ - -import { Meteor } from 'meteor/meteor'; -import { NumberBetween } from '/imports/modules/server/other-checks'; -import Fires from '/imports/api/Fires/Fires'; -import { check } from 'meteor/check'; -import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; -import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; -import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; -import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; -import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; -import Industries from '/imports/api/Industries/Industries'; - -Meteor.startup(() => { - const uptime = new Date(); - - const apiV1 = new Restivus({ - useDefaultAuth: true, - apiPath: 'api', - version: 'v1', - prettyJson: true - }); - - // Generates: POST on /api/users and GET, DELETE /api/users/:id for - // Meteor.users collection - /* apiV1.addCollection(Meteor.users, { - * excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'], - * routeOptions: { - * authRequired: true - * }, - * endpoints: { - * post: { - * authRequired: false - * }, - * delete: { - * roleRequired: 'admin' - * } - * } - * }); */ - - apiV1.addCollection(Fires, { - excludedEndpoints: ['put', 'post', 'patch', 'delete'], - // excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'], - routeOptions: { - authRequired: false - }, - endpoints: { - get: { - action: function getFire() { - return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); - } - } - } - }); - - // Maps to: /api/v1/status/last-fire-check - apiV1.addRoute('status/last-fire-check', { authRequired: false }, { - get: function get() { - return SiteSettings.findOne({ name: 'last-fire-check' }); - } - }); - - // Maps to: /api/v1/status/last-fire-detected - apiV1.addRoute('status/last-fire-detected', { authRequired: false }, { - get: function get() { - return ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); - } - }); - - // Maps to: /api/v1/status/last-fires-count - apiV1.addRoute('status/active-fires-count', { authRequired: false }, { - get: function get() { - return { total: ActiveFiresCollection.find({}).count() }; - } - }); - - // Maps to: /api/v1/status/uptime - apiV1.addRoute('status/uptime', { authRequired: false }, { - get: function get() { - return { ms: new Date() - uptime }; - } - }); - - function getFires(route, full) { - const lat = Number(route.urlParams.lat); - const lng = Number(route.urlParams.lng); - const km = Number(route.urlParams.km); - const { token } = route.urlParams; - check(lng, NumberBetween(-180, 180)); - check(lat, NumberBetween(-90, 90)); - check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100)); - check(token, String); - - if (token !== Meteor.settings.private.internalApiToken) { - console.warn(`WARNING: Query for fires in ${lat}, ${lng} in ${km} km radius with wrong token`); - return { error: 'Unauthorized' }; - } - - console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); - - const fires = ActiveFiresCollection.find({ - ourid: { - $near: { - $geometry: { - type: 'Point', - coordinates: [lng, lat] - }, - $maxDistance: km * 1000, - $minDistance: 0 - } - } - }, { - fields: { - lat: 1, - lon: 1, - when: 1, - scan: 1 - } - }); - - const result = { total: fires.count(), real: countRealFires(fires) }; - - if (!full) { - return result; - } - - // TODO only get real - const firesA = fires.fetch(); - if (firesA.length > 0) { - const union = firesUnion(fires); - const falsePos = whichAreFalsePositives(FalsePositives, union); - const industries = whichAreFalsePositives(Industries, union); - result.fires = firesA; - result.industries = industries.fetch(); - result.falsePos = falsePos.fetch(); - return result; - } - - // otherwise - return { - total: 0, real: 0, fires: [], falsePos: [], industries: [] - }; - } - - - // Maps to: /api/v1/fires-in/:lat/:lng/:km - // 100 km max - // Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 - apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { - return getFires(this, false); - } - }); - - apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { - return getFires(this, true); - } - }); -}); +import '../../api/Rest/Rest.js'; diff --git a/package-lock.json b/package-lock.json index 36134d3..1a2b23f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6206,7 +6206,6 @@ "hawk": { "version": "3.1.3", "bundled": true, - "dev": true, "requires": { "boom": "2.10.1", "cryptiles": "2.0.5", @@ -6383,7 +6382,6 @@ "nopt": { "version": "4.0.1", "bundled": true, - "dev": true, "optional": true, "requires": { "abbrev": "1.1.0", @@ -6440,7 +6438,6 @@ "osenv": { "version": "0.1.4", "bundled": true, - "dev": true, "optional": true, "requires": { "os-homedir": "1.0.2", @@ -6449,30 +6446,25 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, - "dev": true + "bundled": true }, "performance-now": { "version": "0.2.0", "bundled": true, - "dev": true, "optional": true }, "process-nextick-args": { "version": "1.0.7", - "bundled": true, - "dev": true + "bundled": true }, "punycode": { "version": "1.4.1", "bundled": true, - "dev": true, "optional": true }, "qs": { "version": "6.4.0", "bundled": true, - "dev": true, "optional": true }, "rc": { @@ -6489,7 +6481,6 @@ "minimist": { "version": "1.2.0", "bundled": true, - "dev": true, "optional": true } } @@ -6545,31 +6536,26 @@ }, "safe-buffer": { "version": "5.0.1", - "bundled": true, - "dev": true + "bundled": true }, "semver": { "version": "5.3.0", "bundled": true, - "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", "bundled": true, - "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", "bundled": true, - "dev": true, "optional": true }, "sntp": { "version": "1.0.9", "bundled": true, - "dev": true, "requires": { "hoek": "2.16.3" } @@ -10021,6 +10007,11 @@ } } }, + "jsend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jsend/-/jsend-1.0.2.tgz", + "integrity": "sha1-ld99RvvM8fLpVnQ0j5R36TWmyaU=" + }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", diff --git a/package.json b/package.json index ce8e067..65b675f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "ismobilejs": "^0.4.1", "jquery": "^2.2.4", "jquery-validation": "^1.17.0", + "jsend": "^1.0.2", "juice": "^4.2.2", "leaflet": "^1.3.1", "leaflet-geodesy": "^0.2.1", diff --git a/test/rest.test.js b/test/rest.test.js new file mode 100644 index 0000000..93cf57c --- /dev/null +++ b/test/rest.test.js @@ -0,0 +1,219 @@ +/* eslint-disable import/no-absolute-path */ +/* eslint-env mocha */ +/* eslint-disable func-names, prefer-arrow-callback */ + +import { chai } from 'meteor/practicalmeteor:chai'; +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; +import { Accounts } from 'meteor/accounts-base'; +import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; + +function url(path) { + return `http://127.0.0.1:3000/${path}`; +} + +describe('basic api v1 returns', () => { + it('should return uptime', async done => + HTTP.get(url('api/v1/status/uptime'), { + data: { + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).to.equal(200); + chai.expect(typeof result.data.ms).to.equal('number'); + done(); + })); + + it('should return last-fire', async done => + HTTP.get(url('api/v1/status/active-fires-count'), { + data: { + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + chai.expect(typeof result.data.total).to.equal('number'); + done(); + })); + + const token = Meteor.settings.private.internalApiToken; + + it('should return fires in some location', async done => + HTTP.get(url(`api/v1/fires-in/${token}/38.736946/-9.142685/100`), { + data: { + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + chai.expect(typeof result.data.total).to.equal('number'); + chai.expect(typeof result.data.real).to.equal('number'); + done(); + })); + + it('should return full fires in some location', async done => + HTTP.get(url(`api/v1/fires-in-full/${token}/38.736946/-9.142685/100`), { + data: { + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + chai.expect(typeof result.data.total).to.equal('number'); + chai.expect(typeof result.data.real).to.equal('number'); + chai.expect(typeof result.data.falsePos).to.equal('object'); + chai.expect(typeof result.data.industries).to.equal('object'); + chai.expect(typeof result.data.fires).to.equal('object'); + done(); + })); + + it('should not return full fires with some wrong token', async done => + HTTP.get(url('api/v1/fires-in-full/token/38.736946/-9.142685/100'), { + data: { + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.data.status).equal('error'); + chai.expect(result.statusCode).equal(200); + done(); + })); + + it('should not return fires with some wrong token', async done => + HTTP.get(url('api/v1/fires-in/token/38.736946/-9.142685/100'), { + data: { + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.data.status).equal('error'); + chai.expect(result.statusCode).equal(200); + done(); + })); + + it('should not return fires with some wrong distance', async done => + HTTP.get(url(`api/v1/fires-in-full/${token}/38.736946/-9.142685/1100`), (error, result) => { + chai.expect(error, null); + chai.expect(result.data.status).equal('error'); + chai.expect(result.statusCode).equal(200); + done(); + })); + + const mobileToken = 'user-mobile-firebase-token'; + + let testUserId; + + it('should create mobile users', async (done) => { + // this are removed in the test database but we are testing in dev/ci database + + // Delete the test account if it's still present + // Meteor.users.remove({ fireBaseToken: mobileToken }); + + HTTP.post(url('api/v1/mobile/users'), { + data: { + token, + mobileToken, + lang: 'en' + } + }, (error, result) => { + chai.expect(error, null); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + chai.expect(result.statusCode).equal(200); + chai.expect(jsendResult.data.upsertResult.numberAffected).to.equal(1); + chai.expect(jsendResult.data.mobileToken).to.equal(mobileToken); + chai.expect(jsendResult.data.lang).to.equal('en'); + chai.expect(typeof jsendResult.data.username).to.equal('string'); + testUserId = jsendResult.data.userId; + chai.expect(typeof testUserId).to.equal('string'); + done(); + }); + }); + + it('should not create mobile users with wrong token', async (done) => { + // this are removed in the test database but we are testing in dev/ci database + // Delete the test account if it's still present + // Meteor.users.remove({ fireBaseToken: mobileToken }); + + HTTP.post(url('api/v1/mobile/users'), { + data: { + token: 'wrong token', + mobileToken, + lang: 'en' + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.data.status).equal('error'); + done(); + }); + }); + + let subsId; + + it('should create mobile user subscriptions', async (done) => { + // this are removed in the test database but we are testing in dev/ci database + + // Remove previous subs of this user + // Subscriptions.remove({ owner: testUserId }); + // chai.expect(Subscriptions.find({ owner: testUserId }).count()).to.equal(0); + HTTP.post(url('api/v1/mobile/subscriptions'), { + data: { + token, + mobileToken, + lat: 3.106111, + lon: 53.5775, + distance: 100 + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + subsId = jsendResult.data.subsId; + done(); + }); + }); + + it('should remove mobile user subscriptions', async (done) => { + // this are removed in the test database but we are testing in dev/ci database + + HTTP.del(url('api/v1/mobile/subscriptions'), { + data: { + token, + mobileToken, + subsId + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + done(); + }); + }); + + it('should not create mobile user subscriptions with wrong token', async (done) => { + HTTP.post(url('api/v1/mobile/subscriptions'), { + data: { + token: 'wrongOne', + mobileToken, + lat: 3.106111, + lon: 53.5775, + distance: 100 + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.data.status).equal('error'); + done(); + }); + }); + + it('should not remove mobile user subscriptions with wrong token', async (done) => { + HTTP.del(url('api/v1/mobile/subscriptions'), { + data: { + token: 'wrongOne', + mobileToken, + subsId + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.data.status).equal('error'); + done(); + }); + }); +}); From 1de7e4a7b374853a572dfeeea8dc7f719c0b9810 Mon Sep 17 00:00:00 2001 From: vjrj Date: Thu, 14 Jun 2018 19:48:48 +0200 Subject: [PATCH 20/84] Proper error codes --- imports/api/Rest/Rest.js | 7 ++++--- test/rest.test.js | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index b3c63b9..eaf3764 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -20,20 +20,21 @@ const debug = false; const uptime = new Date(); +const restivusError = (code, message) => ({ status: 'error', statusCode: code, body: message }); function fail(e) { - return jsend.error(`Unexpected error in REST call: ${e}`); + return restivusError(500, `Unexpected error in REST call: ${e}`); } function defaultFailParams(e) { - return jsend.error(`Wrong REST params: ${e}`); + return restivusError(400, `Wrong REST params: ${e}`); } function checkAuthToken(token) { if (token !== Meteor.settings.private.internalApiToken) { const message = `Unauthorized auth token '${token}' in REST API`; console.warn(message); - return jsend.error(message); + return restivusError(401, message); } return undefined; } diff --git a/test/rest.test.js b/test/rest.test.js index 93cf57c..316d324 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -70,8 +70,7 @@ describe('basic api v1 returns', () => { } }, (error, result) => { chai.expect(error, null); - chai.expect(result.data.status).equal('error'); - chai.expect(result.statusCode).equal(200); + chai.expect(result.statusCode).equal(401); done(); })); @@ -81,16 +80,14 @@ describe('basic api v1 returns', () => { } }, (error, result) => { chai.expect(error, null); - chai.expect(result.data.status).equal('error'); - chai.expect(result.statusCode).equal(200); + chai.expect(result.statusCode).equal(401); done(); })); it('should not return fires with some wrong distance', async done => HTTP.get(url(`api/v1/fires-in-full/${token}/38.736946/-9.142685/1100`), (error, result) => { chai.expect(error, null); - chai.expect(result.data.status).equal('error'); - chai.expect(result.statusCode).equal(200); + chai.expect(result.statusCode).equal(400); done(); })); @@ -138,7 +135,7 @@ describe('basic api v1 returns', () => { } }, (error, result) => { chai.expect(error, null); - chai.expect(result.data.status).equal('error'); + chai.expect(result.statusCode).equal(401); done(); }); }); @@ -198,7 +195,7 @@ describe('basic api v1 returns', () => { } }, (error, result) => { chai.expect(error, null); - chai.expect(result.data.status).equal('error'); + chai.expect(result.statusCode).equal(401); done(); }); }); @@ -212,8 +209,12 @@ describe('basic api v1 returns', () => { } }, (error, result) => { chai.expect(error, null); - chai.expect(result.data.status).equal('error'); + chai.expect(result.statusCode).equal(401); done(); }); }); + + // TODO list all subs + + // TODO remove all subs }); From a48da85ebe2adb9d86f2f8acf82aa5734e478a0b Mon Sep 17 00:00:00 2001 From: vjrj Date: Thu, 14 Jun 2018 20:35:41 +0200 Subject: [PATCH 21/84] More REST calls --- imports/api/Rest/Rest.js | 43 ++++++++++++++++ test/rest.test.js | 108 +++++++++++++++++++++++++++++++++------ 2 files changed, 134 insertions(+), 17 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index eaf3764..9ee1256 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -321,3 +321,46 @@ apiV1.addRoute('mobile/subscriptions', { authRequired: false }, { return jsend.success({}); } }); + +apiV1.addRoute('mobile/subscriptions/all', { authRequired: false }, { + get: function get() { + const { token, mobileToken } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + + const result = Subscriptions.find({ owner: user._id }); + + return jsend.success({ subscriptions: result.fetch(), count: result.count() }); + }, + delete: function delAll() { + const { token, mobileToken } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + const toRemove = Subscriptions.find({ owner: user._id }).count(); + Subscriptions.remove({ owner: user._id }); + + return jsend.success({ count: toRemove }); + } +}); diff --git a/test/rest.test.js b/test/rest.test.js index 316d324..6b48274 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -142,7 +142,7 @@ describe('basic api v1 returns', () => { let subsId; - it('should create mobile user subscriptions', async (done) => { + function addSubs(done) { // this are removed in the test database but we are testing in dev/ci database // Remove previous subs of this user @@ -164,8 +164,48 @@ describe('basic api v1 returns', () => { subsId = jsendResult.data.subsId; done(); }); + } + + it('should create mobile user subscriptions', async (done) => { + addSubs(done); }); + it('should not create mobile user subscriptions with wrong token', async (done) => { + HTTP.post(url('api/v1/mobile/subscriptions'), { + data: { + token: 'wrongOne', + mobileToken, + lat: 3.106111, + lon: 53.5775, + distance: 100 + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(401); + done(); + }); + }); + + it('should get all mobile user subscriptions', async (done) => { + HTTP.get(url('api/v1/mobile/subscriptions/all'), { + data: { + token, + mobileToken + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + chai.expect(typeof jsendResult.data.subscriptions).to.equal('object'); + chai.expect(typeof jsendResult.data.count).to.equal('number'); + chai.expect(jsendResult.data.count).to.equal(1); + // console.log(JSON.stringify(jsendResult.data.subscriptions)); + done(); + }); + }); + + it('should remove mobile user subscriptions', async (done) => { // this are removed in the test database but we are testing in dev/ci database @@ -184,21 +224,6 @@ describe('basic api v1 returns', () => { }); }); - it('should not create mobile user subscriptions with wrong token', async (done) => { - HTTP.post(url('api/v1/mobile/subscriptions'), { - data: { - token: 'wrongOne', - mobileToken, - lat: 3.106111, - lon: 53.5775, - distance: 100 - } - }, (error, result) => { - chai.expect(error, null); - chai.expect(result.statusCode).equal(401); - done(); - }); - }); it('should not remove mobile user subscriptions with wrong token', async (done) => { HTTP.del(url('api/v1/mobile/subscriptions'), { @@ -214,7 +239,56 @@ describe('basic api v1 returns', () => { }); }); - // TODO list all subs + it('should not get mobile user subscriptions with wrong token', async (done) => { + HTTP.get(url('api/v1/mobile/subscriptions/all'), { + data: { + token: 'wrongOne', + mobileToken + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(401); + done(); + }); + }); + + it('should get all mobile user subscriptions', async (done) => { + HTTP.get(url('api/v1/mobile/subscriptions/all'), { + data: { + token, + mobileToken + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + chai.expect(typeof jsendResult.data.subscriptions).to.equal('object'); + chai.expect(typeof jsendResult.data.count).to.equal('number'); + chai.expect(jsendResult.data.count).to.equal(0); + done(); + }); + }); + + it('should del all mobile user subscriptions', async (done) => { + // Add two + addSubs(() => {}); + addSubs(() => {}); + HTTP.del(url('api/v1/mobile/subscriptions/all'), { + data: { + token, + mobileToken + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + chai.expect(typeof jsendResult.data.count).to.equal('number'); + chai.expect(jsendResult.data.count).to.equal(2); + done(); + }); + }); // TODO remove all subs }); From 714cc8610af618d770b2c94d2b0515bdd50055ac Mon Sep 17 00:00:00 2001 From: vjrj Date: Thu, 14 Jun 2018 21:24:29 +0200 Subject: [PATCH 22/84] Improved test lauch for ci --- test/rest.test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/rest.test.js b/test/rest.test.js index 6b48274..f7c4543 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -5,11 +5,10 @@ import { chai } from 'meteor/practicalmeteor:chai'; import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; -import { Accounts } from 'meteor/accounts-base'; -import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; +const testPort = process.env.TEST_PORT; function url(path) { - return `http://127.0.0.1:3000/${path}`; + return `http://127.0.0.1:${testPort}/${path}`; } describe('basic api v1 returns', () => { From d980a31c05785c133c1f12a1407cc6802081b630 Mon Sep 17 00:00:00 2001 From: vjrj Date: Thu, 14 Jun 2018 21:24:41 +0200 Subject: [PATCH 23/84] Improved test lauch for ci. Doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d7f41e..607eacc 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ More platforms and services in the future... We do tests via: ``` -TEST_WATCH=1 MONGO_URL=mongodb://localhost:27017/fuegostest meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010 +TEST_PORT=3000 TEST_WATCH=1 TEST_CLIENT=0 MONGO_URL=mongodb://localhost:27017/fuegostest meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010 # and From b8ab47e867a012af460048865526fd841678f427 Mon Sep 17 00:00:00 2001 From: vjrj Date: Fri, 15 Jun 2018 06:39:09 +0200 Subject: [PATCH 24/84] Improvements in REST API --- imports/api/Rest/Rest.js | 554 ++++++++++++++++++++------------------- test/rest.test.js | 34 ++- 2 files changed, 300 insertions(+), 288 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index 9ee1256..e7d8b9b 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -31,8 +31,8 @@ function defaultFailParams(e) { } function checkAuthToken(token) { - if (token !== Meteor.settings.private.internalApiToken) { - const message = `Unauthorized auth token '${token}' in REST API`; + if (!Meteor.settings.private.internalApiToken || token !== Meteor.settings.private.internalApiToken) { + const message = 'Unauthorized auth token in REST API'; console.warn(message); return restivusError(401, message); } @@ -45,18 +45,20 @@ function checkLatLonDist(km, lat, lng) { check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100)); } - +if (!Meteor.settings.private.internalApiToken) { + console.warn('Meteor.settings.private.internalApiToken is not configured so we don\'t enable our REST API'); +} else { // export -const apiV1 = new Restivus({ - useDefaultAuth: true, - apiPath: 'api', - version: 'v1', - prettyJson: true -}); + const apiV1 = new Restivus({ + useDefaultAuth: true, + apiPath: 'api', + version: 'v1', + prettyJson: true + }); -// Generates: POST on /api/users and GET, DELETE /api/users/:id for -// Meteor.users collection -/* apiV1.addCollection(Meteor.users, { + // Generates: POST on /api/users and GET, DELETE /api/users/:id for + // Meteor.users collection + /* apiV1.addCollection(Meteor.users, { * excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'], * routeOptions: { * authRequired: true @@ -71,137 +73,57 @@ const apiV1 = new Restivus({ * } * }); */ -apiV1.addCollection(Fires, { - excludedEndpoints: ['put', 'post', 'patch', 'delete'], - // excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'], - routeOptions: { - authRequired: false - }, - endpoints: { - get: { - action: function getFire() { - return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); + apiV1.addCollection(Fires, { + excludedEndpoints: ['put', 'post', 'patch', 'delete'], + // excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'], + routeOptions: { + authRequired: false + }, + endpoints: { + get: { + action: function getFire() { + return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); + } } } - } -}); - -// Maps to: /api/v1/status/last-fire-check -apiV1.addRoute('status/last-fire-check', { authRequired: false }, { - get: function get() { - return SiteSettings.findOne({ name: 'last-fire-check' }); - } -}); - -// Maps to: /api/v1/status/last-fire-detected -apiV1.addRoute('status/last-fire-detected', { authRequired: false }, { - get: function get() { - return ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); - } -}); - -// Maps to: /api/v1/status/active-fires-count -apiV1.addRoute('status/active-fires-count', { authRequired: false }, { - get: function get() { - return { total: ActiveFiresCollection.find({}).count() }; - } -}); - -// Maps to: /api/v1/status/uptime -apiV1.addRoute('status/uptime', { authRequired: false }, { - get: function get() { - return { ms: new Date() - uptime }; - } -}); - -function getFires(route, full) { - const lat = Number(route.urlParams.lat); - const lng = Number(route.urlParams.lng); - const km = Number(route.urlParams.km); - const { token } = route.urlParams; - try { - checkLatLonDist(km, lat, lng); - check(token, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); - - const fires = ActiveFiresCollection.find({ - ourid: { - $near: { - $geometry: { - type: 'Point', - coordinates: [lng, lat] - }, - $maxDistance: km * 1000, - $minDistance: 0 - } - } - }, { - fields: { - lat: 1, - lon: 1, - when: 1, - scan: 1 - } }); - const result = { total: fires.count(), real: countRealFires(fires) }; + // Maps to: /api/v1/status/last-fire-check + apiV1.addRoute('status/last-fire-check', { authRequired: false }, { + get: function get() { + return SiteSettings.findOne({ name: 'last-fire-check' }); + } + }); - if (!full) { - return result; - } + // Maps to: /api/v1/status/last-fire-detected + apiV1.addRoute('status/last-fire-detected', { authRequired: false }, { + get: function get() { + return ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); + } + }); - // TODO only get real - const firesA = fires.fetch(); - if (firesA.length > 0) { - const union = firesUnion(fires); - const falsePos = whichAreFalsePositives(FalsePositives, union); - const industries = whichAreFalsePositives(Industries, union); - result.fires = firesA; - result.industries = industries.fetch(); - result.falsePos = falsePos.fetch(); - return result; - } + // Maps to: /api/v1/status/active-fires-count + apiV1.addRoute('status/active-fires-count', { authRequired: false }, { + get: function get() { + return { total: ActiveFiresCollection.find({}).count() }; + } + }); - // otherwise - return { - total: 0, real: 0, fires: [], falsePos: [], industries: [] - }; -} + // Maps to: /api/v1/status/uptime + apiV1.addRoute('status/uptime', { authRequired: false }, { + get: function get() { + return { ms: new Date() - uptime }; + } + }); -// Maps to: /api/v1/fires-in/:lat/:lng/:km -// 100 km max -// Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 -apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { - return getFires(this, false); - } -}); - -apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { - return getFires(this, true); - } -}); - -// Add mobile user: -// curl -X POST http://localhost:3000/api/v1/users/mobile -d "token: thisAppAutToken" -d "mobileToken: user-mobile-firebase-token" -// Response: -// -// https://docs.meteor.com/api/passwords.html#Accounts-createUser -apiV1.addRoute('mobile/users', { authRequired: false }, { - post: function post() { - const { token, mobileToken, lang } = this.bodyParams; + function getFires(route, full) { + const lat = Number(route.urlParams.lat); + const lng = Number(route.urlParams.lng); + const km = Number(route.urlParams.km); + const { token } = route.urlParams; try { + checkLatLonDist(km, lat, lng); check(token, String); - check(lang, String); - check(mobileToken, String); } catch (e) { return defaultFailParams(e); } @@ -209,158 +131,250 @@ apiV1.addRoute('mobile/users', { authRequired: false }, { const failed = checkAuthToken(token); if (failed) return failed; - let username; + if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); - do { - username = Random.id(15); - } while (Meteor.users.find({ username }).count() !== 0); - - // FIXME check valid lang - - const now = new Date(); - // Accounts.createUser({ username: mobileToken, lang: 'FIXME' profile: {} }); - const result = Meteor.users.upsert({ - fireBaseToken: mobileToken + const fires = ActiveFiresCollection.find({ + ourid: { + $near: { + $geometry: { + type: 'Point', + coordinates: [lng, lat] + }, + $maxDistance: km * 1000, + $minDistance: 0 + } + } }, { - $set: { - username, - fireBaseToken: mobileToken, - lang, - profile: { }, - createdAt: now, - updatedAt: now + fields: { + lat: 1, + lon: 1, + when: 1, + scan: 1 } }); - if (debug) { - console.log(this.bodyParams); - console.log(this.urlParams); - console.log(this.queryParams); + const result = { total: fires.count(), real: countRealFires(fires) }; + + if (!full) { + return result; } - const newUser = Meteor.users.findOne({ username }); + // TODO only get real + const firesA = fires.fetch(); + if (firesA.length > 0) { + const union = firesUnion(fires); + const falsePos = whichAreFalsePositives(FalsePositives, union); + const industries = whichAreFalsePositives(Industries, union); + result.fires = firesA; + result.industries = industries.fetch(); + result.falsePos = falsePos.fetch(); + return result; + } - return jsend.success({ - upsertResult: result, - username, - userId: newUser._id, - lang: newUser.lang, - mobileToken: newUser.fireBaseToken - }); + // otherwise + return { + total: 0, real: 0, fires: [], falsePos: [], industries: [] + }; } -}); + // Maps to: /api/v1/fires-in/:lat/:lng/:km + // 100 km max + // Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 + apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { + get: function get() { + return getFires(this, false); + } + }); -// max sitance: 100km -apiV1.addRoute('mobile/subscriptions', { authRequired: false }, { - post: function post() { - const { - token, - mobileToken, - lat, - lon, - distance - } = this.bodyParams; - try { - check(token, String); - check(mobileToken, String); - check(lat, Number); - check(lon, Number); - check(distance, Number); - checkLatLonDist(distance, lat, lon); - } catch (e) { - return defaultFailParams(e); + apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, { + get: function get() { + return getFires(this, true); + } + }); + + // Add mobile user: + // curl -X POST http://localhost:3000/api/v1/users/mobile -d "token: thisAppAutToken" -d "mobileToken: user-mobile-firebase-token" + // Response: + // + // https://docs.meteor.com/api/passwords.html#Accounts-createUser + apiV1.addRoute('mobile/users', { authRequired: false }, { + post: function post() { + const { token, mobileToken, lang } = this.bodyParams; + try { + check(token, String); + check(lang, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + let username; + + const already = Meteor.users.find({ fireBaseToken: mobileToken }); + + if (already.count() > 1) { + return restivusError(500, 'Unexpected error in REST call: several users with that mobile token?'); + } else if (already.count() === 1) { + username = already.fetch()[0].username; + } else { + do { + username = Random.id(15); + } while (Meteor.users.find({ username }).count() !== 0); + } + + // FIXME check valid lang + + const now = new Date(); + + const result = Meteor.users.upsert({ + fireBaseToken: mobileToken + }, { + $set: { + username, + fireBaseToken: mobileToken, + lang, + profile: { }, + createdAt: now, + updatedAt: now + } + }); + + + if (debug) { + console.log(this.bodyParams); + console.log(this.urlParams); + console.log(this.queryParams); + } + + const upsertUser = Meteor.users.findOne({ username }); + + if (debug) console.log(upsertUser); + + return jsend.success({ + upsertResult: result, + username, + userId: upsertUser._id, + lang: upsertUser.lang, + mobileToken: upsertUser.fireBaseToken + }); } - const failed = checkAuthToken(token); - if (failed) return failed; + }); - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; + // max sitance: 100km + apiV1.addRoute('mobile/subscriptions', { authRequired: false }, { + post: function post() { + const { + token, + mobileToken, + lat, + lon, + distance + } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + check(lat, Number); + check(lon, Number); + check(distance, Number); + checkLatLonDist(distance, lat, lon); + } catch (e) { + return defaultFailParams(e); + } - const newSubs = {}; - newSubs.location = {}; - newSubs.location.lat = lat; - newSubs.location.lon = lon; - newSubs.distance = distance; + const failed = checkAuthToken(token); + if (failed) return failed; - let result; - try { - result = subscriptionsInsert(newSubs, user._id, 'mobile'); - } catch (e) { - return fail(e); + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + + const newSubs = {}; + newSubs.location = {}; + newSubs.location.lat = lat; + newSubs.location.lon = lon; + newSubs.distance = distance; + + let result; + try { + result = subscriptionsInsert(newSubs, user._id, 'mobile'); + } catch (e) { + return fail(e); + } + return jsend.success({ subsId: result._str }); + }, + delete: function del() { + const { + token, + mobileToken, + subsId + } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + check(subsId, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + + try { + Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) }); + } catch (e) { + return fail(e); + } + + return jsend.success({}); } - return jsend.success({ subsId: result._str }); - }, - delete: function del() { - const { - token, - mobileToken, - subsId - } = this.bodyParams; - try { - check(token, String); - check(mobileToken, String); - check(subsId, String); - } catch (e) { - return defaultFailParams(e); + }); + + apiV1.addRoute('mobile/subscriptions/all', { authRequired: false }, { + get: function get() { + const { token, mobileToken } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + + const result = Subscriptions.find({ owner: user._id }); + + return jsend.success({ subscriptions: result.fetch(), count: result.count() }); + }, + delete: function delAll() { + const { token, mobileToken } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failed; + const toRemove = Subscriptions.find({ owner: user._id }).count(); + Subscriptions.remove({ owner: user._id }); + + return jsend.success({ count: toRemove }); } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; - - try { - Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) }); - } catch (e) { - return fail(e); - } - - return jsend.success({}); - } -}); - -apiV1.addRoute('mobile/subscriptions/all', { authRequired: false }, { - get: function get() { - const { token, mobileToken } = this.bodyParams; - try { - check(token, String); - check(mobileToken, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; - - const result = Subscriptions.find({ owner: user._id }); - - return jsend.success({ subscriptions: result.fetch(), count: result.count() }); - }, - delete: function delAll() { - const { token, mobileToken } = this.bodyParams; - try { - check(token, String); - check(mobileToken, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; - const toRemove = Subscriptions.find({ owner: user._id }).count(); - Subscriptions.remove({ owner: user._id }); - - return jsend.success({ count: toRemove }); - } -}); + }); +} diff --git a/test/rest.test.js b/test/rest.test.js index f7c4543..01d3194 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -270,24 +270,22 @@ describe('basic api v1 returns', () => { }); it('should del all mobile user subscriptions', async (done) => { - // Add two - addSubs(() => {}); - addSubs(() => {}); - HTTP.del(url('api/v1/mobile/subscriptions/all'), { - data: { - token, - mobileToken - } - }, (error, result) => { - chai.expect(error, null); - chai.expect(result.statusCode).equal(200); - const jsendResult = result.data; - chai.expect(jsendResult.status).equal('success'); - chai.expect(typeof jsendResult.data.count).to.equal('number'); - chai.expect(jsendResult.data.count).to.equal(2); - done(); + // Add subs + addSubs(() => { + HTTP.del(url('api/v1/mobile/subscriptions/all'), { + data: { + token, + mobileToken + } + }, (error, result) => { + chai.expect(error, null); + chai.expect(result.statusCode).equal(200); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + chai.expect(typeof jsendResult.data.count).to.equal('number'); + chai.expect(jsendResult.data.count).to.equal(1); + done(); + }); }); }); - - // TODO remove all subs }); From 5b3d86e6a5feaeb8c13fc4d44d22f28e5b39a3d6 Mon Sep 17 00:00:00 2001 From: vjrj Date: Mon, 18 Jun 2018 11:58:04 +0200 Subject: [PATCH 25/84] Changed rest params in subs --- imports/api/Rest/Rest.js | 24 +++++++++++++++--------- test/rest.test.js | 25 +++++-------------------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index e7d8b9b..cd3df27 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -22,6 +22,11 @@ const uptime = new Date(); const restivusError = (code, message) => ({ status: 'error', statusCode: code, body: message }); + +function failMsg(msg) { + return restivusError(500, `Unexpected error in REST call: ${msg}`); +} + function fail(e) { return restivusError(500, `Unexpected error in REST call: ${e}`); } @@ -289,7 +294,7 @@ if (!Meteor.settings.private.internalApiToken) { if (failed) return failed; const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; + if (!user) return failMsg('User not found'); const newSubs = {}; newSubs.location = {}; @@ -323,7 +328,7 @@ if (!Meteor.settings.private.internalApiToken) { if (failed) return failed; const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; + if (!user) return failMsg('User not found'); try { Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) }); @@ -335,9 +340,9 @@ if (!Meteor.settings.private.internalApiToken) { } }); - apiV1.addRoute('mobile/subscriptions/all', { authRequired: false }, { + apiV1.addRoute('mobile/subscriptions/all/:token/:mobileToken', { authRequired: false }, { get: function get() { - const { token, mobileToken } = this.bodyParams; + const { token, mobileToken } = this.urlParams; try { check(token, String); check(mobileToken, String); @@ -349,14 +354,14 @@ if (!Meteor.settings.private.internalApiToken) { if (failed) return failed; const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; + if (!user) return failMsg('User not found'); const result = Subscriptions.find({ owner: user._id }); return jsend.success({ subscriptions: result.fetch(), count: result.count() }); }, delete: function delAll() { - const { token, mobileToken } = this.bodyParams; + const { token, mobileToken } = this.urlParams; try { check(token, String); check(mobileToken, String); @@ -365,12 +370,13 @@ if (!Meteor.settings.private.internalApiToken) { } const failed = checkAuthToken(token); - if (failed) return failed; + if (failed) return failed('Auth api check failed'); - if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return failed; + if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return fail; const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failed; + if (!user) return failMsg('User not found'); + const toRemove = Subscriptions.find({ owner: user._id }).count(); Subscriptions.remove({ owner: user._id }); diff --git a/test/rest.test.js b/test/rest.test.js index 01d3194..281d93e 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -186,11 +186,7 @@ describe('basic api v1 returns', () => { }); it('should get all mobile user subscriptions', async (done) => { - HTTP.get(url('api/v1/mobile/subscriptions/all'), { - data: { - token, - mobileToken - } + HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); chai.expect(result.statusCode).equal(200); @@ -239,11 +235,7 @@ describe('basic api v1 returns', () => { }); it('should not get mobile user subscriptions with wrong token', async (done) => { - HTTP.get(url('api/v1/mobile/subscriptions/all'), { - data: { - token: 'wrongOne', - mobileToken - } + HTTP.get(url(`api/v1/mobile/subscriptions/all/wrongOne/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); chai.expect(result.statusCode).equal(401); @@ -252,13 +244,10 @@ describe('basic api v1 returns', () => { }); it('should get all mobile user subscriptions', async (done) => { - HTTP.get(url('api/v1/mobile/subscriptions/all'), { - data: { - token, - mobileToken - } + HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); + console.log(result); chai.expect(result.statusCode).equal(200); const jsendResult = result.data; chai.expect(jsendResult.status).equal('success'); @@ -272,11 +261,7 @@ describe('basic api v1 returns', () => { it('should del all mobile user subscriptions', async (done) => { // Add subs addSubs(() => { - HTTP.del(url('api/v1/mobile/subscriptions/all'), { - data: { - token, - mobileToken - } + HTTP.del(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); chai.expect(result.statusCode).equal(200); From 5642b106d44acaa3aacc49f962e467a9f1c75d5c Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 19 Jun 2018 00:18:18 +0200 Subject: [PATCH 26/84] Improved rest api calls --- imports/api/Rest/Rest.js | 8 +++++ imports/api/Subscriptions/Subscriptions.js | 2 ++ imports/api/Subscriptions/methods.js | 1 + test/rest.test.js | 42 +++++++++++++++++----- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index cd3df27..8be33e5 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -15,6 +15,7 @@ import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; import jsend from 'jsend'; import { Random } from 'meteor/random'; import { subscriptionsInsert } from '/imports/api/Subscriptions/methods.js'; +import { Mongo } from 'meteor/mongo'; const debug = false; @@ -275,18 +276,24 @@ if (!Meteor.settings.private.internalApiToken) { const { token, mobileToken, + id, lat, lon, distance } = this.bodyParams; + let oid; try { check(token, String); check(mobileToken, String); + check(id, String); + oid = new Mongo.ObjectID(id); + check(oid, Meteor.Collection.ObjectID); check(lat, Number); check(lon, Number); check(distance, Number); checkLatLonDist(distance, lat, lon); } catch (e) { + if (debug) console.log(e); return defaultFailParams(e); } @@ -297,6 +304,7 @@ if (!Meteor.settings.private.internalApiToken) { if (!user) return failMsg('User not found'); const newSubs = {}; + newSubs._id = new Meteor.Collection.ObjectID(id); newSubs.location = {}; newSubs.location.lat = lat; newSubs.location.lon = lon; diff --git a/imports/api/Subscriptions/Subscriptions.js b/imports/api/Subscriptions/Subscriptions.js index eb4be16..c69e20b 100644 --- a/imports/api/Subscriptions/Subscriptions.js +++ b/imports/api/Subscriptions/Subscriptions.js @@ -1,6 +1,7 @@ /* eslint-disable consistent-return */ /* eslint-disable import/no-absolute-path */ +import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; @@ -42,6 +43,7 @@ Subscriptions.deny({ Subscriptions.schema = new SimpleSchema({ + _id: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true }, location: Object, 'location.lat': Number, 'location.lon': Number, diff --git a/imports/api/Subscriptions/methods.js b/imports/api/Subscriptions/methods.js index 69456ef..cf927e0 100644 --- a/imports/api/Subscriptions/methods.js +++ b/imports/api/Subscriptions/methods.js @@ -12,6 +12,7 @@ function geo(doc) { export function subscriptionsInsert(doc, userId, type) { check(doc, { + _id: Match.Maybe(Meteor.Collection.ObjectID), location: Match.ObjectIncluding({ lat: Number, lon: Number }), distance: Number }); diff --git a/test/rest.test.js b/test/rest.test.js index 281d93e..52c7997 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -91,8 +91,9 @@ describe('basic api v1 returns', () => { })); const mobileToken = 'user-mobile-firebase-token'; - let testUserId; + let returnedSubsId; + const objId = new Meteor.Collection.ObjectID(); it('should create mobile users', async (done) => { // this are removed in the test database but we are testing in dev/ci database @@ -139,11 +140,9 @@ describe('basic api v1 returns', () => { }); }); - let subsId; function addSubs(done) { // this are removed in the test database but we are testing in dev/ci database - // Remove previous subs of this user // Subscriptions.remove({ owner: testUserId }); // chai.expect(Subscriptions.find({ owner: testUserId }).count()).to.equal(0); @@ -151,16 +150,19 @@ describe('basic api v1 returns', () => { data: { token, mobileToken, + id: objId._str, lat: 3.106111, lon: 53.5775, distance: 100 } }, (error, result) => { chai.expect(error, null); + if (result.statusCode !== 200) console.log(result); chai.expect(result.statusCode).equal(200); const jsendResult = result.data; chai.expect(jsendResult.status).equal('success'); - subsId = jsendResult.data.subsId; + returnedSubsId = jsendResult.data.subsId; + chai.expect(returnedSubsId.toString()).equal(objId._str); done(); }); } @@ -174,12 +176,14 @@ describe('basic api v1 returns', () => { data: { token: 'wrongOne', mobileToken, + id: objId._str, lat: 3.106111, lon: 53.5775, distance: 100 } }, (error, result) => { chai.expect(error, null); + if (result.statusCode !== 401) console.log(result); chai.expect(result.statusCode).equal(401); done(); }); @@ -189,6 +193,7 @@ describe('basic api v1 returns', () => { HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); + if (result.statusCode !== 200) console.log(result); chai.expect(result.statusCode).equal(200); const jsendResult = result.data; chai.expect(jsendResult.status).equal('success'); @@ -208,10 +213,11 @@ describe('basic api v1 returns', () => { data: { token, mobileToken, - subsId + subsId: returnedSubsId } }, (error, result) => { chai.expect(error, null); + if (result.statusCode !== 200) console.log(result); chai.expect(result.statusCode).equal(200); const jsendResult = result.data; chai.expect(jsendResult.status).equal('success'); @@ -219,16 +225,33 @@ describe('basic api v1 returns', () => { }); }); + it('should get all mobile user subscriptions after deletion', async (done) => { + HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), { + }, (error, result) => { + chai.expect(error, null); + if (result.statusCode !== 200) console.log(result); + chai.expect(result.statusCode).equal(200); + const jsendResult = result.data; + chai.expect(jsendResult.status).equal('success'); + chai.expect(typeof jsendResult.data.subscriptions).to.equal('object'); + chai.expect(typeof jsendResult.data.count).to.equal('number'); + chai.expect(jsendResult.data.count).to.equal(0); + // console.log(JSON.stringify(jsendResult.data.subscriptions)); + done(); + }); + }); + it('should not remove mobile user subscriptions with wrong token', async (done) => { HTTP.del(url('api/v1/mobile/subscriptions'), { data: { token: 'wrongOne', mobileToken, - subsId + subsId: returnedSubsId } }, (error, result) => { chai.expect(error, null); + if (result.statusCode !== 401) console.log(result); chai.expect(result.statusCode).equal(401); done(); }); @@ -238,6 +261,7 @@ describe('basic api v1 returns', () => { HTTP.get(url(`api/v1/mobile/subscriptions/all/wrongOne/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); + if (result.statusCode !== 401) console.log(result); chai.expect(result.statusCode).equal(401); done(); }); @@ -247,7 +271,8 @@ describe('basic api v1 returns', () => { HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); - console.log(result); + // console.log(result); + if (result.statusCode !== 200) console.log(result); chai.expect(result.statusCode).equal(200); const jsendResult = result.data; chai.expect(jsendResult.status).equal('success'); @@ -264,11 +289,12 @@ describe('basic api v1 returns', () => { HTTP.del(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), { }, (error, result) => { chai.expect(error, null); + if (result.statusCode !== 200) console.log(result); chai.expect(result.statusCode).equal(200); const jsendResult = result.data; chai.expect(jsendResult.status).equal('success'); chai.expect(typeof jsendResult.data.count).to.equal('number'); - chai.expect(jsendResult.data.count).to.equal(1); + chai.expect(jsendResult.data.count).to.be.at.most(1); done(); }); }); From 47e9c18607fa1baa57015296af850d226ef16cc2 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 19 Jun 2018 10:49:53 +0200 Subject: [PATCH 27/84] More work in REST api deletion --- imports/api/Rest/Rest.js | 7 +++++-- test/rest.test.js | 19 +++++-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index 8be33e5..a5296ae 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -317,13 +317,16 @@ if (!Meteor.settings.private.internalApiToken) { return fail(e); } return jsend.success({ subsId: result._str }); - }, + } + }); + + apiV1.addRoute('mobile/subscriptions/:token/:mobileToken/:subsId', { authRequired: false }, { delete: function del() { const { token, mobileToken, subsId - } = this.bodyParams; + } = this.urlParams; try { check(token, String); check(mobileToken, String); diff --git a/test/rest.test.js b/test/rest.test.js index 52c7997..d365ab2 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -140,7 +140,6 @@ describe('basic api v1 returns', () => { }); }); - function addSubs(done) { // this are removed in the test database but we are testing in dev/ci database // Remove previous subs of this user @@ -199,6 +198,7 @@ describe('basic api v1 returns', () => { chai.expect(jsendResult.status).equal('success'); chai.expect(typeof jsendResult.data.subscriptions).to.equal('object'); chai.expect(typeof jsendResult.data.count).to.equal('number'); + if (jsendResult.data.count !== 1) console.log(result); chai.expect(jsendResult.data.count).to.equal(1); // console.log(JSON.stringify(jsendResult.data.subscriptions)); done(); @@ -209,12 +209,7 @@ describe('basic api v1 returns', () => { it('should remove mobile user subscriptions', async (done) => { // this are removed in the test database but we are testing in dev/ci database - HTTP.del(url('api/v1/mobile/subscriptions'), { - data: { - token, - mobileToken, - subsId: returnedSubsId - } + HTTP.del(url(`api/v1/mobile/subscriptions/${token}/${mobileToken}/${returnedSubsId}`), { }, (error, result) => { chai.expect(error, null); if (result.statusCode !== 200) console.log(result); @@ -235,20 +230,15 @@ describe('basic api v1 returns', () => { chai.expect(jsendResult.status).equal('success'); chai.expect(typeof jsendResult.data.subscriptions).to.equal('object'); chai.expect(typeof jsendResult.data.count).to.equal('number'); + if (jsendResult.data.count !== 0) console.log(result); chai.expect(jsendResult.data.count).to.equal(0); // console.log(JSON.stringify(jsendResult.data.subscriptions)); done(); }); }); - it('should not remove mobile user subscriptions with wrong token', async (done) => { - HTTP.del(url('api/v1/mobile/subscriptions'), { - data: { - token: 'wrongOne', - mobileToken, - subsId: returnedSubsId - } + HTTP.del(url(`api/v1/mobile/subscriptions/wrongOne/${mobileToken}/${returnedSubsId}`), { }, (error, result) => { chai.expect(error, null); if (result.statusCode !== 401) console.log(result); @@ -278,6 +268,7 @@ describe('basic api v1 returns', () => { chai.expect(jsendResult.status).equal('success'); chai.expect(typeof jsendResult.data.subscriptions).to.equal('object'); chai.expect(typeof jsendResult.data.count).to.equal('number'); + if (jsendResult.data.count !== 0) console.log(result); chai.expect(jsendResult.data.count).to.equal(0); done(); }); From bf926514c9ccfe82a6c5b87a035973dca08cb240 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 2 Aug 2018 12:42:19 +0200 Subject: [PATCH 28/84] Mobile notifications --- README.md | 7 +- imports/api/Notifications/Notifications.js | 6 +- imports/api/Notifications/methods.js | 2 +- .../api/Notifications/server/publications.js | 2 +- imports/startup/server/migrations.js | 13 + .../startup/server/notificationsObserver.js | 51 +- imports/ui/components/Loading/LoadingBar.js | 2 +- .../NotificationsObserver.js | 4 +- package-lock.json | 1170 ++++++++++------- package.json | 4 +- public/locales/en/common.json | 3 +- public/locales/es/common.json | 3 +- settings-development-sample.json | 1 + 13 files changed, 762 insertions(+), 506 deletions(-) diff --git a/README.md b/README.md index 607eacc..e686902 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,12 @@ This is web service that notifies about fires detected in an area of your intere Prerrequisites: a running meteor (we share the `fuegos` database with the telegram bot). -Install `meteor` and run `npm start` +Install `meteor` and run `meteor npm install` and `npm start` + +Some other development deps (in debian & ubuntu): +``` +apt-get install libcairo2-dev libjpeg-dev libgif-dev pkg-config +``` ### GeoIP diff --git a/imports/api/Notifications/Notifications.js b/imports/api/Notifications/Notifications.js index 0c89c55..0355540 100644 --- a/imports/api/Notifications/Notifications.js +++ b/imports/api/Notifications/Notifications.js @@ -2,6 +2,7 @@ /* eslint-disable import/no-absolute-path */ import { Mongo } from 'meteor/mongo'; +import { Meteor } from 'meteor/meteor'; import SimpleSchema from 'simpl-schema'; import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; import LocationSchema from '/imports/api/Utility/LocationSchema.js'; @@ -22,11 +23,12 @@ Notifications.deny({ Notifications.schema = new SimpleSchema({ userId: String, + subsId: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true }, content: String, geo: LocationSchema, type: String, - webNotified: { type: Boolean, optional: true }, - webNotifiedAt: { type: Date, optional: true }, + notified: { type: Boolean, optional: true }, + notifiedAt: { type: Date, optional: true }, emailNotified: { type: Boolean, optional: true }, emailNotifiedAt: { type: Date, optional: true }, when: Date, diff --git a/imports/api/Notifications/methods.js b/imports/api/Notifications/methods.js index 604c6e0..05d7d08 100644 --- a/imports/api/Notifications/methods.js +++ b/imports/api/Notifications/methods.js @@ -8,7 +8,7 @@ Meteor.methods({ check(notifId, Meteor.Collection.ObjectID); try { - Notifications.update(notifId, { $set: { webNotified: true, webNotifiedAt: new Date() } }); + Notifications.update(notifId, { $set: { notified: true, notifiedAt: new Date() } }); return notifId; } catch (exception) { throw new Meteor.Error('500', exception); diff --git a/imports/api/Notifications/server/publications.js b/imports/api/Notifications/server/publications.js index 83ceaaf..d868692 100644 --- a/imports/api/Notifications/server/publications.js +++ b/imports/api/Notifications/server/publications.js @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import Notifications from '../Notifications'; Meteor.publish('mynotifications', function notifications() { - const notif = Notifications.find({ userId: this.userId, type: 'web', webNotified: null }); + const notif = Notifications.find({ userId: this.userId, type: 'web', notified: null }); // console.log(`Notifications for user ${this.userId}: ${notif.count()}`); return notif; }); diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index 158a75c..defc7c1 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -9,6 +9,7 @@ import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; import Industries from '/imports/api/Industries/Industries'; import IndustryRegistries from '/imports/api/Industries/IndustryRegistries'; +import Notifications from '/imports/api/Notifications/Notifications'; import { Mongo } from 'meteor/mongo'; Meteor.startup(() => { @@ -227,6 +228,18 @@ Meteor.startup(() => { } }); + Migrations.add({ + version: 17, + up: function renameWebNotifiedField() { + Notifications.update({ webNotified: { $exists: true } }, { + $rename: { webNotifiedAt: 'notifiedAt' } + }, { upsert: false, multi: true }); + Notifications.update({ webNotified: { $exists: true } }, { + $rename: { webNotified: 'notified' } + }, { upsert: false, multi: true }); + } + }); + // Set createdAt in users & subs Migrations.migrateTo('latest'); diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js index 85cd531..9058881 100644 --- a/imports/startup/server/notificationsObserver.js +++ b/imports/startup/server/notificationsObserver.js @@ -10,13 +10,19 @@ import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email'; // import { hr } from '/imports/startup/server/email'; import getEmailOf from '/imports/modules/get-email-of-user'; import image from 'google-maps-image-api-url'; +import gcm from 'node-gcm'; import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; Meteor.startup(() => { + if (!(Meteor.settings.private.fcmApiToken && Meteor.settings.private.fcmApiToken.length > 0)) { + console.warn('Missing settings.private.fcmApiToken key, mobile notifications will not work'); + } + + const fcmSender = new gcm.Sender(Meteor.settings.private.fcmApiToken); + // https://www.npmjs.com/package/google-maps-image-api-url // https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email // https://developers.google.com/maps/documentation/static-maps/intro - function imgUrl(lat, lng) { return image({ key: Meteor.settings.gmaps.key, @@ -36,7 +42,48 @@ Meteor.startup(() => { } */ function process(notif) { - if (notif.type === 'web' && !notif.emailNotified) { + if (notif.type === 'mobile' && !notif.notified) { + const user = Meteor.users.findOne({ _id: notif.userId }); + moment.locale(user.lang); + // duplicate code below + const body = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`; + + // https://firebase.google.com/docs/cloud-messaging/concept-options + const msg = new gcm.Message(); + + msg.addNotification({ + title: i18n.t('Alerta de fuego'), + body, + click_action: 'FLUTTER_NOTIFICATION_CLICK', + tag: notif._id, // prevent duplication of fire notifications + sound: 'default', // Indicates sound to be played. Supports only default currently. + icon: 'launch_image' // 'ic_launcher' + }); + + msg.addData('id', notif._id); + msg.addData('description', body); + msg.addData('lat', notif.geo.coordinates[1]); + msg.addData('lon', notif.geo.coordinates[0]); + msg.addData('when', notif.when); + msg.addData('subsId', notif.subsId._str); + msg.addData('sealed', notif.sealed); + + const registrationTokens = []; + if (!user.fireBaseToken) { + console.warn('This mobile user doesn\'t have a firebase registration token'); + } else { + registrationTokens.push(user.fireBaseToken); + // FIXME: better join users + fcmSender.send(msg, { registrationTokens }, (err, response) => { + if (err) { + console.error(err); + } else { + // console.log(response); + Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); + } + }); + } + } else if (notif.type === 'web' && !notif.emailNotified) { const user = Meteor.users.findOne({ _id: notif.userId }); const { firstName, emailAddress } = getEmailOf(user); diff --git a/imports/ui/components/Loading/LoadingBar.js b/imports/ui/components/Loading/LoadingBar.js index 93bba69..5dab00c 100644 --- a/imports/ui/components/Loading/LoadingBar.js +++ b/imports/ui/components/Loading/LoadingBar.js @@ -3,7 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Meteor } from 'meteor/meteor'; -import { Line } from 'react-progressbar.js'; +import { Line } from 'react-progress-bar.js'; import './LoadingBar.scss'; diff --git a/imports/ui/components/NotificationsObserver/NotificationsObserver.js b/imports/ui/components/NotificationsObserver/NotificationsObserver.js index e68b1ef..ac374b5 100644 --- a/imports/ui/components/NotificationsObserver/NotificationsObserver.js +++ b/imports/ui/components/NotificationsObserver/NotificationsObserver.js @@ -12,7 +12,7 @@ import { trim } from './util.js'; function process(notif) { // No already notified if (Push.Permission.has()) { - if (!notif.webNotified) { + if (!notif.notified && notif.type === 'web') { Push.create(i18n.t('AppName'), { body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`, icon: '/n-fire-marker.png', @@ -47,7 +47,7 @@ Meteor.startup(() => { if (Meteor.userId()) { Meteor.subscribe('mynotifications'); // Check for notifications not processed at startup - Notifications.find({ webNotified: null }).forEach((notif) => { + Notifications.find({ notified: null, type: 'web' }).forEach((notif) => { process(notif); }); } diff --git a/package-lock.json b/package-lock.json index 1a2b23f..5a3aab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,6 +128,11 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "acorn": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", @@ -278,6 +283,11 @@ "default-require-extensions": "1.0.0" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, "archiver": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", @@ -478,46 +488,12 @@ } }, "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { "delegates": "1.0.0", "readable-stream": "2.3.3" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "1.0.0", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", - "requires": { - "safe-buffer": "5.1.1" - } - } } }, "argparse": { @@ -602,10 +578,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, - "asn1": { - "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -639,7 +611,8 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, "asynckit": { - "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { @@ -658,8 +631,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" }, "axobject-query": { "version": "0.1.0", @@ -2456,14 +2430,6 @@ "node-pre-gyp": "0.6.36" } }, - "bcrypt-pbkdf": { - "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - } - }, "big-integer": { "version": "1.6.26", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.26.tgz", @@ -2542,7 +2508,14 @@ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "requires": { - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } } }, "bluebird": { @@ -2682,7 +2655,8 @@ } }, "caseless": { - "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "center-align": { @@ -3325,10 +3299,6 @@ "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, - "code-point-at": { - "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, "codecov": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/codecov/-/codecov-2.3.1.tgz", @@ -3588,8 +3558,9 @@ } }, "combined-stream": { - "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { "delayed-stream": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" } @@ -4073,19 +4044,6 @@ "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", "dev": true }, - "dashdash": { - "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - }, - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, "datauri": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/datauri/-/datauri-1.0.5.tgz", @@ -4319,14 +4277,6 @@ } } }, - "ecc-jsbn": { - "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" - } - }, "ejs": { "version": "2.5.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.9.tgz", @@ -5435,8 +5385,9 @@ } }, "extend": { - "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "external-editor": { "version": "2.1.0", @@ -6276,8 +6227,7 @@ }, "jsbn": { "version": "0.1.1", - "bundled": true, - "optional": true + "bundled": true }, "json-schema": { "version": "0.2.3", @@ -6702,18 +6652,33 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", "rimraf": "2.6.2" }, "dependencies": { - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { - "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" + "minimist": "0.0.8" } } } @@ -6724,8 +6689,42 @@ "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", "requires": { "fstream": "1.0.11", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + "inherits": "2.0.3", + "minimatch": "3.0.4" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + } } }, "function-bind": { @@ -6794,17 +6793,63 @@ "aproba": "1.2.0", "console-control-strings": "1.1.0", "has-unicode": "2.0.1", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "signal-exit": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "wide-align": "1.1.2" + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.3" }, "dependencies": { - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } } } }, @@ -6853,19 +6898,6 @@ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, - "getpass": { - "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - }, - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, "gherkin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/gherkin/-/gherkin-4.0.0.tgz", @@ -7943,13 +7975,6 @@ } } }, - "is-fullwidth-code-point": { - "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" - } - }, "is-generator": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz", @@ -9961,11 +9986,6 @@ } } }, - "jsbn": { - "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, "jsdom": { "version": "9.12.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-9.12.0.tgz", @@ -10017,10 +10037,6 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" }, - "json-schema": { - "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, "json-schema-traverse": { "version": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" @@ -11879,6 +11895,104 @@ "is-stream": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" } }, + "node-gcm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/node-gcm/-/node-gcm-1.0.2.tgz", + "integrity": "sha512-NEVb5jB06I/e9ZfJGWhHsRhJQLk1zO5iZjbQJ7su8j9vhHrpxc7KJHyBxWbv28+4uWFZ0AfTVHoMIyRpEVISUQ==", + "requires": { + "debug": "3.1.0", + "lodash": "4.17.10", + "request": "2.87.0" + }, + "dependencies": { + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.2", + "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "mime-types": "2.1.17", + "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.2", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "node-geocoder": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/node-geocoder/-/node-geocoder-3.21.1.tgz", @@ -11889,6 +12003,290 @@ "request-promise": "4.2.2" } }, + "node-gyp": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz", + "integrity": "sha512-qDQE/Ft9xXP6zphwx4sD0t+VhwV7yFaloMpfbL2QnnDZcyaiakWlLdtFGGQfTAwpFHdpbRhRxVhIHN1OKAjgbg==", + "requires": { + "fstream": "1.0.11", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.5", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.3.0", + "tar": "2.2.1", + "which": "1.3.1" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.17" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.2", + "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "mime-types": "2.1.17", + "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.2", + "stringstream": "0.0.6", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -11929,10 +12327,10 @@ "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", "integrity": "sha1-22BBEst04NR3VU6bUFsXq936t4Y=", "requires": { - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "mkdirp": "0.5.1", "nopt": "4.0.1", "npmlog": "4.1.2", - "rc": "1.2.2", + "rc": "1.2.8", "request": "2.83.0", "rimraf": "2.6.2", "semver": "5.4.1", @@ -11940,294 +12338,17 @@ "tar-pack": "3.4.1" }, "dependencies": { - "ajv": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", - "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", - "requires": { - "co": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "fast-deep-equal": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "requires": { - "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "mime-types": "2.1.17" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.1", - "har-schema": "2.0.0" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "verror": "1.10.0" - } - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" - }, - "rc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz", - "integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=", + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - } - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "1.0.0", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "caseless": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "mime-types": "2.1.17", - "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "tough-cookie": "2.3.3", - "tunnel-agent": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "uuid": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", - "requires": { - "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", - "requires": { - "hoek": "4.2.0" - } - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "dashdash": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "ecc-jsbn": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "getpass": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "jsbn": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "tweetnacl": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha1-4dvAOpudO6B+iWrQJzF+tnmhCh8=", - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "readable-stream": "2.3.3", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "extsprintf": "1.3.0" + "minimist": "0.0.8" } } } @@ -12266,14 +12387,7 @@ "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "requires": { "abbrev": "1.1.1", - "osenv": "0.1.4" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" - } + "osenv": "0.1.5" } }, "normalize-package-data": { @@ -16209,12 +16323,19 @@ "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { - "are-we-there-yet": "1.1.4", + "are-we-there-yet": "1.1.5", "console-control-strings": "1.1.0", "gauge": "2.7.4", - "set-blocking": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + "set-blocking": "2.0.0" + }, + "dependencies": { + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + } } }, "nth-check": { @@ -16225,10 +16346,6 @@ "boolbase": "1.0.0" } }, - "number-is-nan": { - "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, "nwmatcher": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.3.tgz", @@ -16553,10 +16670,6 @@ } } }, - "os-homedir": { - "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", @@ -16573,12 +16686,24 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { - "os-homedir": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + }, + "dependencies": { + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + } } }, "p-cancelable": { @@ -17394,10 +17519,6 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, "push.js": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/push.js/-/push.js-1.0.5.tgz", @@ -17480,6 +17601,29 @@ } } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "0.6.0", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + } + } + }, "rc-align": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-2.3.5.tgz", @@ -17808,13 +17952,13 @@ "prop-types": "15.6.0" } }, - "react-progressbar.js": { - "version": "git://github.com/squarecat/react-progressbar.js.git#1ff715a3c9dc0f8089127902824cf13a8907f96f", + "react-progress-bar.js": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/react-progress-bar.js/-/react-progress-bar.js-0.2.3.tgz", + "integrity": "sha512-dyPLW0Wux9uOgEdz9xCtdeS/FD14KwygyW3dNLkogZ7GM1SMaI2yQ3uhQ0S/CyyPhtjAKWbH5XvzI0bnytDbxQ==", "requires": { "lodash.isequal": "4.5.0", - "progressbar.js": "1.0.1", - "react": "16.2.0", - "react-dom": "16.2.0" + "progressbar.js": "1.0.1" } }, "react-resize-detector": { @@ -18619,8 +18763,9 @@ } }, "safe-buffer": { - "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "sane": { "version": "2.2.0", @@ -18994,10 +19139,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, - "set-blocking": { - "version": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", @@ -19344,15 +19485,6 @@ } } }, - "string-width": { - "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - } - }, "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", @@ -19369,15 +19501,9 @@ } }, "stringstream": { - "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" }, "strip-bom": { "version": "2.0.0", @@ -19511,7 +19637,44 @@ "requires": { "block-stream": "0.0.9", "fstream": "1.0.11", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "tar-pack": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } } }, "tar-stream": { @@ -19625,10 +19788,11 @@ "dev": true }, "tunnel-agent": { - "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz" + "safe-buffer": "5.1.2" } }, "turf-jsts": { @@ -19636,11 +19800,6 @@ "resolved": "https://registry.npmjs.org/turf-jsts/-/turf-jsts-1.2.2.tgz", "integrity": "sha1-uZkjZZpGc3uBQT7P+b1w42C6F1M=" }, - "tweetnacl": { - "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, "twit": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/twit/-/twit-2.2.9.tgz", @@ -19762,10 +19921,6 @@ "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "uuid": { - "version": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" - }, "valid-data-url": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-0.1.4.tgz", @@ -20116,11 +20271,40 @@ "dev": true }, "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { - "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } } }, "window-size": { diff --git a/package.json b/package.json index 65b675f..0954e23 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "modernizr": "^3.6.0", "moment": "^2.19.1", "moment-timezone": "^0.5.14", + "node-gcm": "^1.0.2", "node-geocoder": "^3.21.1", + "node-gyp": "^3.7.0", "nodemailer": "^4.4.2", "npm": "^5.8.0", "pm2-master": "^1.1.3", @@ -75,7 +77,7 @@ "react-leaflet-google": "^3.2.1", "react-leaflet-sidebarv2": "^0.5.1", "react-places-autocomplete": "^5.4.3", - "react-progressbar.js": "git://github.com/squarecat/react-progressbar.js.git", + "react-progress-bar.js": "^0.2.3", "react-resize-detector": "^1.1.0", "react-router-bootstrap": "^0.24.4", "react-router-dom": "^4.2.2", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index bc6ae5a..6ddd231 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -250,5 +250,6 @@ "Hay más información sobre un fuego": "There is more information about a fire", "esDecirTalDia": "That is on {{date}}", - "Actualizando...": "Updating..." + "Actualizando...": "Updating...", + "Alerta de fuego": "Alert of fire" } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index d6ec163..d46328a 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -335,5 +335,6 @@ "Hay más información sobre un fuego": "Hay más información sobre un fuego", "esDecirTalDia": "Es decir el {{date}}", - "Actualizando...": "Actualizando..." + "Actualizando...": "Actualizando...", + "Alerta de fuego": "Alerta de fuego" } diff --git a/settings-development-sample.json b/settings-development-sample.json index 9aa3ae3..3bbb1c7 100644 --- a/settings-development-sample.json +++ b/settings-development-sample.json @@ -31,6 +31,7 @@ "loginStyle": "popup" } }, + "fcmApiToken": "", "proxies_count": 0, "ironPassword": "SomePasswordAlLeast32LongYouKnowHowToCount", "internalApiToken": "someSimilarPasswordForUseOurAPIinternally", From dc0df7cf321e19f52c55c4ac8bf80302610c03ec Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 2 Aug 2018 23:13:15 +0200 Subject: [PATCH 29/84] Master sends only to mobiles --- imports/startup/server/notificationsObserver.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js index 9058881..b03dd94 100644 --- a/imports/startup/server/notificationsObserver.js +++ b/imports/startup/server/notificationsObserver.js @@ -7,6 +7,7 @@ import { dateLongFormat } from '/imports/api/Common/dates'; import Notifications from '/imports/api/Notifications/Notifications'; // import sendMail from '/imports/startup/server/email'; import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email'; +import { isMailServerMaster } from '/imports/startup/server/email'; // import { hr } from '/imports/startup/server/email'; import getEmailOf from '/imports/modules/get-email-of-user'; import image from 'google-maps-image-api-url'; @@ -42,7 +43,7 @@ Meteor.startup(() => { } */ function process(notif) { - if (notif.type === 'mobile' && !notif.notified) { + if (isMailServerMaster && notif.type === 'mobile' && !notif.notified) { const user = Meteor.users.findOne({ _id: notif.userId }); moment.locale(user.lang); // duplicate code below @@ -78,7 +79,7 @@ Meteor.startup(() => { if (err) { console.error(err); } else { - // console.log(response); + console.log(response); Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); } }); From 8780edc832ad92eb4637be8b2fac3ec2401582d0 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Fri, 3 Aug 2018 08:04:43 +0200 Subject: [PATCH 30/84] Correct ObjectId in fcm message --- imports/startup/server/notificationsObserver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js index b03dd94..589460c 100644 --- a/imports/startup/server/notificationsObserver.js +++ b/imports/startup/server/notificationsObserver.js @@ -61,7 +61,7 @@ Meteor.startup(() => { icon: 'launch_image' // 'ic_launcher' }); - msg.addData('id', notif._id); + msg.addData('id', notif._id._str); msg.addData('description', body); msg.addData('lat', notif.geo.coordinates[1]); msg.addData('lon', notif.geo.coordinates[0]); From 20359a79e32ffe438f92b0d60e2dc61d09f8ce95 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Fri, 3 Aug 2018 08:20:20 +0200 Subject: [PATCH 31/84] FCM error handling with Meteor.bind --- .../startup/server/notificationsObserver.js | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js index 589460c..6cadb3d 100644 --- a/imports/startup/server/notificationsObserver.js +++ b/imports/startup/server/notificationsObserver.js @@ -15,8 +15,11 @@ import gcm from 'node-gcm'; import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; Meteor.startup(() => { + let validFcmSender = true; + if (!(Meteor.settings.private.fcmApiToken && Meteor.settings.private.fcmApiToken.length > 0)) { console.warn('Missing settings.private.fcmApiToken key, mobile notifications will not work'); + validFcmSender = false; } const fcmSender = new gcm.Sender(Meteor.settings.private.fcmApiToken); @@ -38,9 +41,9 @@ Meteor.startup(() => { } /* - function imgEl(lat, lng) { - return ``; - } */ + function imgEl(lat, lng) { + return ``; + } */ function process(notif) { if (isMailServerMaster && notif.type === 'mobile' && !notif.notified) { @@ -75,14 +78,16 @@ Meteor.startup(() => { } else { registrationTokens.push(user.fireBaseToken); // FIXME: better join users - fcmSender.send(msg, { registrationTokens }, (err, response) => { - if (err) { - console.error(err); - } else { - console.log(response); - Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); - } - }); + if (validFcmSender) { + fcmSender.send(msg, { registrationTokens }, Meteor.bindEnvironment(function processResult(err, response) { + if (err) { + console.error(`FCM error: ${err}`); + } else { + console.log(`FCM response: ${response}`); + Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); + } + })); + } } } else if (notif.type === 'web' && !notif.emailNotified) { const user = Meteor.users.findOne({ _id: notif.userId }); From cfe6c2f5704908830caa596207664158fb4ff27b Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Fri, 3 Aug 2018 09:05:14 +0200 Subject: [PATCH 32/84] Better mobile notification desc --- imports/startup/server/notificationsObserver.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js index 6cadb3d..0065e15 100644 --- a/imports/startup/server/notificationsObserver.js +++ b/imports/startup/server/notificationsObserver.js @@ -50,7 +50,7 @@ Meteor.startup(() => { const user = Meteor.users.findOne({ _id: notif.userId }); moment.locale(user.lang); // duplicate code below - const body = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`; + const body = `${trim(notif.content)}`; // https://firebase.google.com/docs/cloud-messaging/concept-options const msg = new gcm.Message(); @@ -83,7 +83,7 @@ Meteor.startup(() => { if (err) { console.error(`FCM error: ${err}`); } else { - console.log(`FCM response: ${response}`); + // console.log(`FCM response: ${response}`); Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); } })); From d4be8c7c46201a1848adec636df6531ae21ee2ad Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Sat, 4 Aug 2018 18:42:37 +0200 Subject: [PATCH 33/84] Privacy updated --- private/pages/privacy-en.md | 161 ++++++++++++++++++++++++------------ private/pages/privacy-es.md | 160 +++++++++++++++++++++++------------ 2 files changed, 219 insertions(+), 102 deletions(-) diff --git a/private/pages/privacy-en.md b/private/pages/privacy-en.md index 183a1e7..4c42f76 100644 --- a/private/pages/privacy-en.md +++ b/private/pages/privacy-en.md @@ -1,97 +1,156 @@ -Your privacy is important to us. +# PRIVACY POLICY # -Comunes Association built the 'All Against the Fire' app and this SERVICE is provided by Comunes Association at no cost and is intended for use as is. +*Last updated July 12, 2018* -This page is used to inform website visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service. +**In Short**: *Your privacy is important to us. We do not do business with your data, we do not share it with third parties and we only worry about the fires and how to strengthen neighborhoods against them.* -If you choose to use our Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy. +Thank you for choosing to be part of our community 'All Against the Fires' at Comunes Association (“we”, “us”, or “our”). We are committed to protecting your personal information and your right to privacy. If you have any questions or concerns about our policy, or our practices with regards to your personal information, please contact us at info@comunes.org. -The terms used in this Privacy Policy have the same meanings as in our [Terms and Conditions](/terms), which is accessible at 'All Against the Fire' unless otherwise defined in this Privacy Policy. +When you visit our website [fires.comunes.org](https://fires.comunes.org), mobile application, and use our services, you trust us with your personal information. We take your privacy very seriously. In this privacy notice, we describe our privacy policy. We seek to explain to you in the clearest way possible what information we collect, how we use it and what rights you have in relation to it. We hope you take some time to read through it carefully, as it is important. If there are any terms in this privacy policy that you do not agree with, please discontinue use of our Sites or Apps and our services. -**Information Collection and Use** +This privacy policy applies to all information collected through our website (such as [fires.comunes.org](https://fires.comunes.org)), mobile application (the "**Apps**"), and/or any related services, or events (we refer to them collectively in this privacy policy as the "**Sites**"). -For a better experience, while using our Service, we may require you to provide us with certain personally identifiable information, including but not limited to name, email, language, and the telegram username (of our Telegram users). The information that we request is will be retained by us and used as described in this privacy policy. +**Please read this privacy policy carefully as it will help you make informed decisions about sharing your personal information with us.** -In addition, the app may collect certain information automatically, including, but not limited to, the type of device you use, the IP address of your device, your mobile operating system, the type of Internet browsers you use, and information about the way you use the app. -We may use the information we collect in the following ways: -* To personalize your experience and to allow us to deliver the type of content in which you are most interested. -* To send periodic emails regarding our services. +## 1. WHAT INFORMATION DO WE COLLECT? ## -The app does use third party services that may collect information used to identify you. +### Information automatically collected ### -Links to privacy policy of third party service providers used by the app: +**In Short:** *Some information – such as IP address and/or browser and device characteristics – is collected +automatically when you visit our Sites or Apps.* -* [OpenStreetMap](https://wiki.openstreetmap.org/wiki/Privacy_Policy/) -* [Google Maps](https://developers.google.com/maps/terms?hl=es#3-privacy-and-personal-information) +We automatically collect certain information when you visit, use or navigate the Sites or Apps. This information does not reveal your specific identity (like your name or contact information) but may include device and usage information, such as your IP address, browser and device characteristics, operating system, language preferences, referring URLs, device name, country, location, information about how and when you use our Sites or Apps and other technical information. This information is primarily needed to maintain the security and operation of our Sites or Apps, and for our internal analytics and reporting purposes. Like many businesses, we also collect information through cookies and similar technologies. -We have not enabled Google AdSense. +### Information collected through our Apps ### -**Does the app collect precise real time location information of the device?** +**In Short:** *We may collect information regarding your geo-location, push notifications, when you use our apps.* -When you visit the app, we may use GPS technology (or other similar technology) to determine your current location in order to determine the city you are located within and/or display a location map of local fires and/or provide alerts of future fires in your zone. We **will not** share your current location with other users or partners. +If you use our Apps, we may also collect the following information: -If you do not want us to use your location for the purposes set forth above, you should turn off the location services for the mobile app located in your account settings or in your mobile phone settings and/or within the app and/or when asked by your browser. +- *Geo-Location Information.* We may request at some point access or permission to location-based information from your mobile device to provide location-based services. If you wish to change our access or permissions, you may do so in your device's settings. +- *Push Notifications.* We may request to send you push notifications regarding your account or the mobile application. If you wish to opt-out from receiving these types of communications, you may turn them off in your device's settings. -**How do we protect your information?** -We do not use vulnerability scanning and/or scanning to PCI standards. We only provide articles and information. We never ask for credit card numbers. We do not use Malware Scanning. +## 2. HOW DO WE USE YOUR INFORMATION? ## -Your personal information is contained behind secured networks and is only accessible by a limited number of persons who have special access rights to such systems, and are required to keep the information confidential. In addition, all sensitive information you supply is encrypted via Secure Socket Layer (SSL) technology. +**In Short:** *We process your information for purposes based on legitimate interests, the fulfillment of our contract with you, compliance with our legal obligations, and/or your consent.* -We implement a variety of security measures when a user enters, submits, or accesses their information to maintain the safety of your personal information. +We use personal information collected via our Sites or Apps for a variety of purposes described below. We process your personal information for these purposes in order to enter into or perform a contract with you ("Contractual"), with your consent ("Consent"), and/or for compliance with our legal obligations ("Legal Reasons"). We indicate the specific processing grounds we rely on next to each purpose listed below. -**Log Data** +We use the information we collect or receive: -We want to inform you that whenever you use our Service, in a case of an error in the app we collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing our Service, the time and date of your use of the Service, and other statistics. +- **To facilitate account creation and logon process** with your Consent. If you choose to link your account with us to a third party account \*(such as your Google or Telegram account), we use the information you allowed us to collect from those third parties to facilitate account creation and logon process. +- **Request Feedback**. We may use your information to request feedback and to contact you about your use of our Sites or Apps. +- **To enable user-to-user communications** with your Consent. We may use your information in order to enable user-to-user communications with each user's consent. +- **To enforce our terms, conditions and policies**. +- **To respond to legal requests and prevent harm** for Legal Reasons. If we receive a subpoena or other legal request, we may need to inspect the data we hold to determine how to respond. -**Do we use 'cookies'?** -Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow) that enables the site's or service provider's systems to recognize your browser and capture and remember certain information. For instance, we use cookies to help us remember and process your prefered language. They are also used to help us understand your preferences based on previous or current site activity, which enables us to provide you with improved services. We also use cookies to help us compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. +## 3. WILL YOUR INFORMATION BE SHARED WITH ANYONE? ## -We use cookies to understand and save user's preferences for future visits. +**In Short:** *We only share information with your consent, to comply with laws or to protect your rights.* -You can choose to have your computer warn you each time a cookie is being sent, or you can choose to turn off all cookies. You do this through your browser settings. Since browser is a little different, look at your browser's Help Menu to learn the correct way to modify your cookies. +We only share and disclose your information in the following situations: -If you turn cookies off, some of the features that make your site experience more efficient may not function properly. +- **Compliance with Laws**. We may disclose your information where we are legally required to do so in order to comply with applicable law, governmental requests, a judicial proceeding, court order, or legal process, such as in response to a court order or a subpoena (including in response to public authorities to meet national security or law enforcement requirements). +- **Vital Interests and Legal Rights**. We may disclose your information where we believe it is necessary to investigate, prevent, or take action regarding potential violations of our policies, suspected fraud, situations involving potential threats to the safety of any person and illegal activities, or as evidence in litigation in which we are involved. +- **With your Consent**. We may disclose your personal information for any other purpose with your consent. -**Service Providers** -We may employ third-party companies and individuals to facilitate our Service. +## 4. DO WE USE COOKIES AND OTHER TRACKING TECHNOLOGIES? ## -**Third-party disclosure** +**In Short:** *We may use cookies and other tracking technologies to collect and store your information.* -We do not sell, trade, or otherwise transfer to outside parties your Personally Identifiable Information. +We may use cookies to access or store information about account login and your user preferences (like your prefered language). -**Security** -We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security. +## 5. DO WE USE GOOGLE MAPS? ## -**Links to Other Sites** +**In Short:** *Yes, we use Google Maps for the purpose of providing better service.* -We do not include or offer third-party products or services on our website but this Service may contain links to other sites in user comments. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. +This website, mobile application, or Telegram application uses Google Maps APIs. You may find the Google Maps APIs Terms of Service [here](https://developers.google.com/maps/terms). To better understand Google's Privacy Policy, please refer to this [link](https://policies.google.com/privacy). -**Children’s Privacy** +By using our Maps API Implementation, you agree to be bound by Google's Terms of Service. You agree to allow us to obtain or cache your location. You may revoke your consent at anytime. We use information about location in conjunction with data from other data providers. -These Services do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from children under 13. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions. -**Fair Information Practices** +## 6. HOW LONG DO WE KEEP YOUR INFORMATION? ## -The Fair Information Practices Principles form the backbone of privacy law in the United States and the concepts they include have played a significant role in the development of data protection laws around the globe. Understanding the Fair Information Practice Principles and how they should be implemented is critical to comply with the various privacy laws that protect personal information. +**In Short:** *We keep your information for as long as necessary to fulfill the purposes outlined in this privacy policy unless otherwise required by law.* -In order to be in line with Fair Information Practices we will take the following responsive action, should a data breach occur: -* We will notify you via email, within 7 business days +We will only keep your personal information for as long as it is necessary for the purposes set out in this privacy policy, unless a longer retention period is required or permitted by law (such as tax, accounting or other legal requirements). No purpose in this policy will require us keeping your personal information for longer than the period of time in which users have an account with us. -We also agree to the Individual Redress Principle which requires that individuals have the right to legally pursue enforceable rights against data collectors and processors who fail to adhere to the law. This principle requires not only that individuals have enforceable rights against data users, but also that individuals have recourse to courts or government agencies to investigate and/or prosecute non-compliance by data processors. +When we have no ongoing legitimate need to process your personal information, we will either delete or anonymize it, or, if this is not possible (for example, because your personal information has been stored in backup archives), then we will securely store your personal information and isolate it from any further processing until deletion is possible. -**Changes to This Privacy Policy** -We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page. +## 7.HOW DO WE KEEP YOUR INFORMATION SAFE? ## -**Your Consent** +**In Short:** *We aim to protect your personal information through a system of organizational and technical security measures.* -By using the app, you are consenting to our processing of your information as set forth in this Privacy Policy now and as amended by us. "Processing," means using cookies on a computer/hand held device or using or touching information in any way, including, but not limited to, collecting, storing, deleting, using, combining and disclosing information, all of which activities will take place in Spain. If you reside outside Spain your information will be transferred, processed and stored there under Spain privacy standards. +We have implemented appropriate technical and organizational security measures designed to protect the security of any personal information we process. However, please also remember that we cannot guarantee that the internet itself is 100% secure. Although we will do our best to protect your personal information, transmission of personal information to and from our Sites or Apps is at your own risk. You should only access the services within a secure environment. -**Contact Us** -If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us: info AT comunes DOT org. +## 8. DO WE COLLECT INFORMATION FROM MINORS? ## + +**In Short:** *We do not knowingly collect data from or market to children under 18 years of age.* + +We do not knowingly solicit data from or market to children under 18 years of age. By using the Sites or Apps, you represent that you are at least 18 or that you are the parent or guardian of such a minor and consent to such minor dependent's use of the Sites or Apps. If we learn that personal information from users less than 18 years of age has been collected, we will deactivate the account and take reasonable measures to promptly delete such data from our records. If you become aware of any data we have collected from children under age 18, please contact us at info@comunes.org. + + +## 9. WHAT ARE YOUR PRIVACY RIGHTS? ## + +**In Short:** *In some regions, such as the European Economic Area, you have rights that allow you greater access to and control over your personal information. You may review, change, or terminate your account at any time.* + +In some regions (like the European Economic Area), you have certain rights under applicable data protection laws. These may include the right (i) to request access and obtain a copy of your personal information, (ii) to request rectification or erasure; (iii) to restrict the processing of your personal information; and (iv) if applicable, to data portability. In certain circumstances, you may also have the right to object to the processing of your personal information. To make such a request, please use the contact details provided below. We will consider and act upon any request in accordance with applicable data protection laws. + +If we are relying on your consent to process your personal information, you have the right to withdraw your consent at any time. Please note however that this will not affect the lawfulness of the processing before its withdrawal. + +If you are resident in the European Economic Area and you believe we are unlawfully processing your personal information, you also have the right to complain to your local data protection supervisory authority. You can find their contact details here: [http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm](http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm) + +### Account Information ### + +If you would at any time like to review or change the information in your account or terminate your account, you can: + +- Log into your account settings and update your user account. + +Upon your request to terminate your account, we will deactivate or delete your account and information from our active databases. However, some information may be retained in our files to prevent fraud, troubleshoot problems, assist with any investigations, enforce our Terms of Use and/or comply with legal requirements. + +**Cookies and similar technologies:** Most Web browsers are set to accept cookies by default. If you prefer, you can usually choose to set your browser to remove cookies and to reject cookies. If you choose to remove cookies or reject cookies, this could affect certain features or services of our Sites or Apps. + +**Opting out of email notifications:** You can unsubscribe from our notification emails at any time by clicking on the unsubscribe link in the emails that we send or by contacting us using the details provided below. You will then be removed from these emails – however, we will still need to send you service-related emails that are necessary for the administration and use of your account. To otherwise opt-out, you may: + +- Note your preferences when you register an account with the site. +- Access your account settings and update preferences. + + +## 10. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? ## + +**In Short:** *Yes, if you are a resident of California, you are granted specific rights regarding access to your personal information.* + +California Civil Code Section 1798.83, also known as the "Shine The Light" law, permits our users who are California residents to request and obtain from us, once a year and free of charge, information about categories of personal information (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all third parties with which we shared personal information in the immediately preceding calendar year. If you are a California resident and would like to make such a request, please submit your request in writing to us using the contact information provided below. + +If you are under 18 years of age, reside in California, and have a registered account with the Sites or Apps, you have the right to request removal of unwanted data that you publicly post on the Sites or Apps. To request removal of such data, please contact us using the contact information provided below, and include the email address associated with your account and a statement that you reside in California. We will make sure the data is not publicly displayed on the Sites or Apps, but please be aware that the data may not be completely or comprehensively removed from our systems. + + +## 11. DO WE MAKE UPDATES TO THIS POLICY? ## + +**In Short:** *Yes, we will update this policy as necessary to stay compliant with relevant laws.* + +We may update this privacy policy from time to time. The updated version will be indicated by an updated "Revised" date and the updated version will be effective as soon as it is accessible. If we make material changes to this privacy policy, we may notify you either by prominently posting a notice of such changes or by directly sending you a notification. We encourage you to review this privacy policy frequently to be informed of how we are protecting your information. + + +## 12. HOW CAN YOU CONTACT US ABOUT THIS POLICY? ## + +If you have questions or comments about this policy, you may email us at info@comunes.org or by post to: + +ASOCIACIÓN COMUNES +C/ COLÓN 21, 5º DCHA, ESC DCHA +03001 ALICANTE +SPAIN + +If you have any further questions or comments about us or our policies, email us at info@comunes.org or by post to: + +ASOCIACIÓN COMUNES +C/ COLÓN 21, 5º DCHA, ESC DCHA +03001 ALICANTE +SPAIN diff --git a/private/pages/privacy-es.md b/private/pages/privacy-es.md index c494ca4..20d63d4 100644 --- a/private/pages/privacy-es.md +++ b/private/pages/privacy-es.md @@ -1,97 +1,155 @@ -Su privacidad es importante para nosotros. +# POLÍTICA DE PRIVACIDAD # -La Asociación Comunes ha creado esta aplicación 'Tod@s contra el fuego' y este SERVICIO que se provee por la Asociación de Comunes sin costo y está destinado a ser usado tal cual. +*Última actualización 12 de julio de 2018* -Esta página se utiliza para informar a los visitantes del sitio web sobre nuestras políticas con la recopilación, el uso y la divulgación de información personal si alguien decide utilizar nuestro Servicio. +**En resumen**: *Su privacidad es importante para nosotros/as. No hacemos negocios con sus datos, no los compartimos con terceros y solo nos preocupan los incendios y cómo fortalecer vecindarios contra ellos.* -Si elige usar nuestro Servicio, acepta la recopilación y el uso de la información en relación con esta política. La información personal que recopilamos se usa para proporcionar y mejorar el servicio. No usaremos ni compartiremos su información con nadie, excepto según se describe en esta Política de privacidad. +Gracias por elegir ser parte de nuestra comunidad 'Tod@s contra los Fuegos' en la Asociación de Comunes ("nosotras/os" o "nos"). Nos comprometemos a proteger su información personal y su derecho a la privacidad. Si tiene alguna pregunta o inquietud sobre nuestra política o nuestras prácticas con respecto a su información personal, contáctenos a info@comunes.org. -Los términos utilizados en esta Política de privacidad tienen los mismos significados que en nuestros [Términos y condiciones](/terms), a los que se puede acceder en "Tod@s contra el fuego" a menos que se defina lo contrario en esta Política de privacidad. +Cuando visita nuestro sitio web [fuegos.comunes.org](https://fuegos.comunes.org), la aplicación móvil y utiliza nuestros servicios, confía en nosotras/os con su información personal. Nos tomamos su privacidad muy en serio. En este aviso de privacidad, describimos nuestra política de privacidad. Buscamos explicarte de la manera más clara posible qué información recopilamos, cómo la utilizamos y qué derechos tienes en relación con ella. Esperamos que se tome un tiempo para leer detenidamente, ya que es importante. Si hay términos en esta política de privacidad con los que no está de acuerdo, interrumpa el uso de nuestros Sitios o Aplicaciones y nuestros servicios. -**Recopilación y uso de información** +Esta política de privacidad se aplica a toda la información recopilada a través de nuestro sitio web (como [fuegos.comunes.org](https://fuegos.comunes.org)), la aplicación móvil (las "**Aplicaciones**") y/o cualquier servicio relacionado o eventos (nos referimos a ellos colectivamente en esta política de privacidad como los "**Sitios**"). -Para una mejor experiencia, al utilizar nuestro Servicio, podemos solicitarle que nos brinde cierta información de identificación personal, que incluye, entre otros, nombre, correo electrónico, idioma y el nombre de usuario del telegrama (de nuestros usuarios de Telegram). La información que solicitamos será retenida por nosotros y utilizada como se describe en esta política de privacidad. +**Lea atentamente esta política de privacidad, ya que le ayudará a tomar decisiones informadas sobre cómo compartir su información personal con nosotras/os.** -Además, la aplicación puede recopilar cierta información automáticamente, que incluye, entre otros, el tipo de dispositivo que usa, la dirección IP de su dispositivo, su sistema operativo móvil, el tipo de navegador de Internet que utiliza e información sobre la forma que usas la aplicación. -Podemos usar la información que recopilamos de las siguientes maneras: -* Para personalizar su experiencia y permitirnos ofrecer el tipo de contenido que más le interesa. -* Para enviar correos electrónicos periódicos con respecto a nuestros servicios. +## 1. ¿QUÉ INFORMACIÓN RECOPILAMOS? ## -La aplicación utiliza servicios de terceros que pueden recopilar información utilizada para identificarlo. +### Información recopilada automáticamente ### -Enlaces a la política de privacidad de proveedores de servicios de terceros utilizados por la aplicación: +**En resumen:** *Recopilamos información automáticamente, como la dirección IP y/o las características del navegador y del dispositivo, cuando visita nuestros Sitios o Aplicaciones.* -* [OpenStreetMap](https://wiki.openstreetmap.org/wiki/Privacy_Policy/) -* [Google Maps](https://developers.google.com/maps/terms?hl=es#3-privacy-and-personal-information) +Recopilamos cierta información automáticamente cuando visita, utiliza o navega por nuestros Sitios o Aplicaciones. Esta información no revela su identidad específica (como su nombre o información de contacto) pero puede incluir información de uso y dispositivo, como su dirección IP, características del navegador y del dispositivo, sistema operativo, preferencias de idioma, URL de referencia, nombre del dispositivo, país, ubicación , información sobre cómo y cuándo utiliza nuestros sitios o aplicaciones y otra información técnica. Esta información es principalmente necesaria para mantener la seguridad y el funcionamiento de nuestros sitios o aplicaciones, y para nuestros fines de análisis internos e informes. Al igual que muchas empresas, también recopilamos información a través de cookies y tecnologías similares. -No hemos habilitado Google AdSense. +### Información recopilada a través de nuestras aplicaciones ### -**¿La aplicación recopila información precisa sobre la ubicación en tiempo real del dispositivo?** +**En resumen:** *Podemos recopilar información sobre su ubicación geográfica, notificaciones push, cuando usa nuestras aplicaciones.* -Cuando visita la aplicación, podemos usar tecnología GPS (u otra tecnología similar) para determinar su ubicación actual a fin de determinar la ciudad en la que se encuentra y/o mostrar un mapa de ubicación de incendios locales y/o proporcionar alertas de incendios futuros en tu zona. **No** compartiremos su ubicación actual con otros usuarios o socios. +Si usa nuestras aplicaciones, también podemos recopilar la siguiente información: -Si no desea que usemos su ubicación para los fines establecidos anteriormente, debe desactivar los servicios de ubicación para la aplicación móvil ubicados en la configuración de su cuenta o en la configuración de su teléfono móvil y/o dentro de la aplicación y/o cuando se le solicite por tu navegador. +- *Información de geolocalización.* Podemos solicitar en algún momento acceso o permiso a la información basada en la ubicación desde su dispositivo móvil para proporcionar servicios basados ​​en la ubicación. Si desea cambiar nuestro acceso o permisos, puede hacerlo en la configuración de su dispositivo. +- *Notificaciones Push.* Podemos solicitar el envío de notificaciones push con respecto a su cuenta o la aplicación móvil. Si desea optar por no recibir este tipo de comunicaciones, puede desactivarlas en la configuración de su dispositivo. -**¿Cómo protegemos tu información?** -No utilizamos el escaneo y/o escaneo de vulnerabilidades a estándares PCI. Solo proporcionamos artículos e información. Nunca pedimos números de tarjetas de crédito. No utilizamos el Malware Scanning. +## 2. ¿CÓMO USAMOS SU INFORMACIÓN? ## -Su información personal está almacenada detrás de redes seguras y solo es accesible para un número limitado de personas que tienen derechos especiales de acceso a dichos sistemas, y se les exige mantener la confidencialidad de la información. Además, toda la información sensible que usted suministra se cifra a través de la tecnología Secure Socket Layer (SSL). +**En resumen:** *Procesamos su información para fines basados ​​en intereses legítimos, el cumplimiento de nuestro contrato con usted, el cumplimiento de nuestras obligaciones legales y/o su consentimiento.* -Implementamos una variedad de medidas de seguridad cuando un usuario ingresa, envía o accede a su información para mantener la seguridad de su información personal. +Usamos información personal recopilada a través de nuestros Sitios o Aplicaciones para una variedad de propósitos que se describen a continuación. Procesamos su información personal para estos fines a fin de celebrar o realizar un contrato con usted ("Contractual"), con su consentimiento ("Consentimiento"), y/o para cumplir con nuestras obligaciones legales ("Razones legales"). Indicamos los motivos de procesamiento específicos en los que confiamos junto a cada uno de los propósitos enumerados a continuación. -**Dato de registro** +Usamos la información que recopilamos o recibimos: -Queremos informarle que cada vez que utiliza nuestro Servicio, en caso de error en la aplicación, recopilamos datos e información (a través de productos de terceros) en su teléfono, llamados Datos de registro. Este registro de datos puede incluir información como la dirección del protocolo de Internet ("IP"), el nombre del dispositivo, la versión del sistema operativo, la configuración de la aplicación al utilizar nuestro servicio, la hora y fecha de uso del servicio y otras estadísticas. +- **Para facilitar la creación de la cuenta y el proceso de inicio de sesión** con su consentimiento. Si elige vincular su cuenta con nosotras/os a una cuenta de terceros \*(como su cuenta de Google o Telegram), usamos la información que nos permitió recopilar de esos terceros para facilitar la creación de la cuenta y el proceso de inicio de sesión. +- **Solicitud de comentarios**. Podemos utilizar su información para solicitar comentarios y para comunicarnos con usted sobre su uso de nuestros Sitios o Aplicaciones. +- **Para habilitar las comunicaciones de usuario a usuario** con su consentimiento. Podemos usar su información para permitir las comunicaciones de usuario a usuario con el consentimiento de cada usuario. +- **Para hacer cumplir nuestros términos, condiciones y políticas**. +- **Para responder a solicitudes legales y evitar daños** por razones legales. Si recibimos una citación u otra solicitud legal, es posible que tengamos que inspeccionar los datos que tenemos para determinar cómo responder. -**¿Usamos 'cookies'?** -Sí. Las cookies son pequeños archivos que un sitio o su proveedor de servicios transfiere al disco duro de su computadora a través de su navegador web (si lo permite) que permite que los sistemas del sitio o del proveedor de servicios reconozcan su navegador y capturen y recuerden cierta información. Por ejemplo, usamos cookies para ayudarnos a recordar y procesar su idioma preferido. También se utilizan para ayudarnos a comprender sus preferencias en función de la actividad del sitio anterior o actual, lo que nos permite brindarle mejores servicios. También utilizamos cookies para ayudarnos a recopilar datos agregados sobre el tráfico del sitio y la interacción del sitio para que podamos ofrecer mejores experiencias y herramientas del sitio en el futuro. +## 3. ¿SU INFORMACIÓN SERÁ COMPARTIDA CON ALGUIEN? ## -Usamos cookies para comprender y guardar las preferencias del usuario para futuras visitas. +**En resumen:** *Solo compartimos información con su consentimiento, para cumplir con las leyes o proteger sus derechos* -Puede optar por que su computadora lo advierta cada vez que se envía una cookie, o puede optar por desactivar todas las cookies. Lo haces a través de la configuración de tu navegador. Dado que el navegador es un poco diferente, consulte el Menú de Ayuda de su navegador para conocer la forma correcta de modificar sus cookies. +Solo compartimos y divulgamos su información en las siguientes situaciones: -Si desactiva las cookies, es posible que algunas de las funciones que hacen que su sitio sea más eficiente no funcionen correctamente. +- **De acuerdo con las leyes**. Podemos divulgar su información cuando se nos requiera legalmente para cumplir con la ley aplicable, las solicitudes gubernamentales, un procedimiento judicial, una orden judicial o un proceso legal, como por ejemplo en respuesta a una orden judicial o una citación judicial (incluso en respuesta a las autoridades públicas para cumplir con los requisitos de seguridad nacional o cumplimiento de la ley). +- **Intereses vitales y derechos legales**. Podemos divulgar su información cuando creemos que es necesario investigar, prevenir o tomar medidas con respecto a posibles violaciones de nuestras políticas, sospecha de fraude, situaciones que involucren amenazas potenciales a la seguridad de cualquier persona y actividades ilegales, o como evidencia en un litigio en el cual estamos involucrados. +- **Con su consentimiento**. Podemos divulgar su información personal para cualquier otro propósito con su consentimiento. -**Proveedores de servicio** -Podemos emplear compañías e individuos de terceros para facilitar nuestro Servicio. +## 4. ¿USAMOS COOKIES Y OTRAS TECNOLOGÍAS DE SEGUIMIENTO? ## -**Divulgación de terceros** +**En resumen:** *Podemos utilizar cookies y otras tecnologías de seguimiento para recopilar y almacenar su información.* -No vendemos, comercializamos ni transferimos a terceros su información de identificación personal. +Podemos usar cookies para acceder o almacenar información sobre el inicio de sesión de la cuenta y sus preferencias de usuario (como su idioma preferido). -**Seguridad** -Valoramos su confianza al proporcionarnos su información personal, por lo tanto, nos esforzamos por utilizar medios comercialmente aceptables para protegerla. Pero recuerde que ningún método de transmisión a través de Internet o método de almacenamiento electrónico es 100% seguro y confiable, y no podemos garantizar su seguridad absoluta. +## 5. ¿USAMOS GOOGLE MAPS? ## -**Enlaces a otros sitios** +**En resumen:** *Sí, utilizamos Google Maps con el fin de proporcionar un mejor servicio.* -No incluimos ni ofrecemos productos o servicios de terceros en nuestro sitio web, pero este Servicio puede contener enlaces a otros sitios en los comentarios de los usuarios. Si hace clic en un enlace de un tercero, se lo dirigirá a ese sitio. Tenga en cuenta que estos sitios externos no son operados por nosotros. Por lo tanto, le recomendamos encarecidamente que revise la Política de privacidad de estos sitios web. No tenemos control ni asumimos ninguna responsabilidad por el contenido, las políticas de privacidad o las prácticas de sitios o servicios de terceros. +Este sitio web, aplicación móvil o aplicación Telegram utiliza las API de Google Maps. Puede encontrar las Condiciones del servicio de las API de Google Maps [aquí](https://developers.google.com/maps/terms). Para comprender mejor la Política de privacidad de Google, consulte este [enlace](https://policies.google.com/privacy). -**Privacidad de los niños** +Al utilizar nuestra implementación de Maps API, usted acepta estar sujeto a los Términos de Servicio de Google. Usted acepta permitirnos obtener o almacenar en caché su ubicación. Puede revocar su consentimiento en cualquier momento. Usamos información sobre la ubicación junto con datos de otros proveedores de datos. -Estos servicios no se dirigen a personas menores de 13 años. No recopilamos deliberadamente información personal identificable de niños menores de 13 años. En el caso de que descubramos que un niño menor de 13 años nos ha proporcionado información personal, la borramos inmediatamente de nuestros servidores. Si usted es un padre o tutor y sabe que su hijo nos ha proporcionado información personal, comuníquese con nosotros para que podamos hacer las acciones necesarias. -**Prácticas justas de información** +## 6. ¿CUÁNTO TIEMPO CONSERVAMOS SU INFORMACIÓN? ## -Los principios de prácticas justas de información (del inglés 'Fair Information Practices') forman la columna vertebral de la ley de privacidad en los Estados Unidos y los conceptos que incluyen han jugado un papel importante en el desarrollo de las leyes de protección de datos en todo el mundo. Comprender los principios prácticos de información justa y cómo deben implementarse es fundamental para cumplir con las diversas leyes de privacidad que protegen la información personal. +**En resumen:** *Conservamos su información el tiempo que sea necesario para cumplir con los fines descritos en esta política de privacidad, a menos que la ley así lo requiera.* -Para estar en línea con las prácticas de información justas, tomaremos la siguiente medida de respuesta, en caso de que se produzca una violación de datos: -* Le notificaremos por correo electrónico dentro de los 7 días hábiles +Solo conservaremos su información personal durante el tiempo que sea necesario para los fines establecidos en esta política de privacidad, a menos que la ley exija o permita un período de retención más largo (como impuestos, contabilidad u otros requisitos legales). Ningún propósito de esta política nos obligará a conservar su información personal por un período mayor al que el usuario tenga una cuenta con nosotras/os. -También aceptamos el principio de reparación individual que exige que las personas tengan derecho a buscar legalmente derechos exigibles contra los recolectores de datos y procesadores que no cumplan con la ley. Este principio requiere no solo que los individuos tengan derechos exigibles contra los usuarios de los datos, sino también que los individuos recurran a los tribunales o agencias gubernamentales para investigar y/o enjuiciar el incumplimiento por parte de los procesadores de datos. +Cuando no tenemos una necesidad legítima continua de procesar su información personal, la borraremos o la anonimizamos, o, si esto no es posible (por ejemplo, porque su información personal ha sido almacenada en archivos de respaldo), almacenaremos de manera segura su información personal y aislarlo de cualquier procesamiento posterior hasta que la eliminación sea posible. -**Cambios a esta política de privacidad** -Es posible que actualicemos nuestra política de privacidad de vez en cuando. Por lo tanto, se recomienda revisar esta página periódicamente para cualquier cambio. Le notificaremos de cualquier cambio publicando la nueva política de privacidad en esta página. Estos cambios entran en vigencia inmediatamente después de que se publiquen en esta página. +## 7. ¿CÓMO MANTENEMOS SU INFORMACIÓN SEGURA? ## -**Tu consentimiento** +**En resumen:** *Nuestro objetivo es proteger su información personal a través de un sistema de medidas de seguridad técnicas y organizativas.* -Al utilizar la aplicación, usted está dando su consentimiento para que procesemos su información según lo establecido en esta política de privacidad ahora y en la forma en que nosotros lo modifiquemos. "Procesamiento" significa usar cookies en una computadora/dispositivo de mano o usar o tocar información de cualquier manera, incluyendo, pero no limitado a, recolectar, almacenar, eliminar, usar, combinar y divulgar información, todas las cuales se llevarán a cabo en España. Si reside fuera de España, su información será transferida, procesada y almacenada allí según los estándares de privacidad de España. +Hemos implementado medidas de seguridad organizacionales y técnicas adecuadas diseñadas para proteger la seguridad de cualquier información personal que procesamos. Sin embargo, recuerde que no podemos garantizar que Internet sea 100% seguro. Aunque haremos nuestro mejor esfuerzo para proteger su información personal, la transmisión de información personal hacia y desde nuestros Sitios o Aplicaciones es bajo su propio riesgo. Solo debe acceder a los servicios dentro de un entorno seguro. -**Contáctenos** -Si tiene alguna pregunta o sugerencia sobre nuestra Política de privacidad, no dude en ponerse en contacto con nosotros: info ARROBA comunes PUNTO org. +## 8. ¿RECOPILAMOS INFORMACIÓN DE MENORES DE EDAD? ## + +**En resumen:** *No recopilamos deliberadamente ni comercializamos datos de niños menores de 18 años.* + +No solicitamos intencionalmente ni comercializamos datos de niños menores de 18 años. Al utilizar los Sitios o Aplicaciones, usted declara que tiene al menos 18 años o que es el padre o tutor de dicho menor y acepta el uso de los Sitios o Aplicaciones por parte de dicho menor dependiente. Si descubrimos que se ha recopilado información personal de usuarios menores de 18 años, desactivaremos la cuenta y tomaremos medidas razonables para eliminar dichos datos de inmediato de nuestros registros. Si toma conocimiento de cualquier información que hayamos recopilado de niños menores de 18 años, contáctenos a info@comunes.org. + + +## 9. ¿CUÁLES SON SUS DERECHOS DE PRIVACIDAD? ## + +**En resumen:** *En algunas regiones, como el Unión Europea, tiene derechos que le permiten un mayor acceso y control sobre su información personal. Puede revisar, cambiar o cancelar su cuenta en cualquier momento.* + +En algunas regiones (como la Unión Europea), usted tiene ciertos derechos bajo las leyes de protección de datos aplicables. Estos pueden incluir el derecho (i) de solicitar acceso y obtener una copia de su información personal, (ii) solicitar una rectificación o cancelación; (iii) restringir el procesamiento de su información personal; y (iv) si corresponde, a la portabilidad de datos. En determinadas circunstancias, también puede tener derecho a oponerse al procesamiento de su información personal. Para realizar dicha solicitud, utilice los datos de contacto que se proporcionan a continuación. Consideraremos y actuaremos en cualquier solicitud de acuerdo con las leyes de protección de datos aplicables. + +Si confiamos en su consentimiento para procesar su información personal, tiene derecho a retirar su consentimiento en cualquier momento. Sin embargo, tenga en cuenta que esto no afectará la legalidad del procesamiento antes de su retirada. + +Si usted reside en la Unión Europea y considera que estamos procesando ilegalmente su información personal, también tiene derecho a presentar una queja ante la autoridad de supervisión de protección de datos local. Puede encontrar sus datos de contacto aquí: [http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm](http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm) + +### Información de la cuenta ### + +Si en algún momento desea revisar o cambiar la información en su cuenta o cancelar su cuenta, puede: + +- Inicie sesión en la configuración de su cuenta y actualice su cuenta de usuario. + +Tras su solicitud para cancelar su cuenta, desactivaremos o eliminaremos su cuenta y la información de nuestras bases de datos activas. Sin embargo, parte de la información se puede conservar en nuestros archivos para evitar fraudes, solucionar problemas, ayudar con cualquier investigación, hacer cumplir nuestros Términos de uso y/o cumplir con los requisitos legales. + +**Cookies y tecnologías similares:** La mayoría de los navegadores web están configurados para aceptar cookies por defecto. Si lo prefiere, generalmente puede elegir configurar su navegador para que elimine las cookies y rechace las cookies. Si elige eliminar las cookies o rechazar las cookies, esto podría afectar ciertas funciones o servicios de nuestros sitios o aplicaciones. + +**Optar por no recibir notificaciones por correo electrónico:** Puede darse de baja de nuestros correos electrónicos de notificación en cualquier momento haciendo clic en el enlace para darse de baja en los correos electrónicos que le enviamos o poniéndose en contacto con nosotras/os utilizando los detalles que se proporcionan a continuación. A continuación, se le eliminará de estos correos electrónicos; sin embargo, aún así le enviaremos correos electrónicos relacionados con el servicio que son necesarios para la administración y el uso de su cuenta. Para optar por no participar, usted puede: + +- Tener en cuenta sus preferencias cuando registra una cuenta en el sitio. +- Acceder a la configuración de tu cuenta y actualizar tus preferencias. + + +## 10. ¿LOS RESIDENTES DE CALIFORNIA TIENEN DERECHOS DE PRIVACIDAD ESPECÍFICOS? ## + +**En resumen:** *Sí, si usted es residente de California, se le otorgan derechos específicos con respecto al acceso a su información personal.* + +La Sección 1798.83 del Código Civil de California, también conocida como la ley "Shine The Light", permite a nuestros usuarios que son residentes de California solicitar y obtener de nosotras/os, una vez al año y sin cargo, información sobre categorías de información personal (si corresponde) que divulgado a terceros para fines de marketing directo y los nombres y direcciones de todos los terceros con quienes compartimos información personal en el año calendario inmediatamente anterior. Si usted es residente de California y desea realizar dicha solicitud, envíe su solicitud por escrito a nosotras/os utilizando la información de contacto que se proporciona a continuación. + +Si eres menor de 18 años, resides en California y tienes una cuenta registrada en los Sitios o Aplicaciones, tienes el derecho de solicitar la eliminación de datos no deseados que publiques en los Sitios o Aplicaciones. Para solicitar la eliminación de dichos datos, contáctenos utilizando la información de contacto que se proporciona a continuación, e incluya la dirección de correo electrónico asociada a su cuenta y una declaración de que reside en California. Nos aseguraremos de que los datos no se muestren públicamente en los Sitios o Aplicaciones, pero tenga en cuenta que es posible que los datos no se eliminen completa o completamente de nuestros sistemas. + + +## 11. ¿HACEMOS ACTUALIZACIONES DE ESTA POLÍTICA? ## + +**En resumen:** *Sí, actualizaremos esta política según sea necesario para cumplir con las leyes pertinentes.* + +Podemos actualizar esta política de privacidad de vez en cuando. La versión actualizada se indicará con una fecha actualizada "Revisada" y la versión actualizada entrará en vigencia tan pronto como sea accesible. Si realizamos cambios sustanciales a esta política de privacidad, podemos notificarlo publicando de manera prominente un aviso de dichos cambios o enviándole directamente una notificación. Le recomendamos que revise esta política de privacidad con frecuencia para recibir información sobre cómo protegemos su información. + + +## 12. ¿CÓMO PUEDE CONTACTARNOS SOBRE ESTA POLÍTICA? ## + +Si tiene preguntas o comentarios sobre esta política, puede enviarnos un correo electrónico a info@comunes.org o por correo postal a: + +ASOCIACIÓN COMUNES +C/ COLÓN 21, 5º DCHA, ESC DCHA +03001 ALICANTE +ESPAÑA + +Si tiene más preguntas o comentarios sobre nosotras/os o nuestras políticas, envíenos un correo electrónico a info@comunes.org o por correo postal a: + +ASOCIACIÓN COMUNES +C/ COLÓN 21, 5º DCHA, ESC DCHA +03001 ALICANTE +ESPAÑA From 9556bf4e60b75c542f67e3adcfddb2aeb9c25240 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Sun, 5 Aug 2018 09:36:18 +0200 Subject: [PATCH 34/84] New rest methods (subs-union, falsePositive) for mobile --- imports/api/FalsePositives/methods.js | 45 +++++++++++--------- imports/api/Fires/server/publications.js | 10 ++++- imports/api/Rest/Rest.js | 53 +++++++++++++++++++++++- 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/imports/api/FalsePositives/methods.js b/imports/api/FalsePositives/methods.js index 7aa4ef1..85005ba 100644 --- a/imports/api/FalsePositives/methods.js +++ b/imports/api/FalsePositives/methods.js @@ -6,6 +6,29 @@ import FalsePositiveTypes from './FalsePositiveTypes'; import Fires from '../Fires/Fires'; import rateLimit from '../../modules/rate-limit'; +export function upsertFalsePositive(keyType, owner, fire) { + const fireType = fire.type; + if (fireType === 'vecinal') { + throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego'); + } + const date = new Date(); + const formated = moment(date).format('YYYYMMDD'); + try { + const set = { + owner, + type: keyType, + when: date, + whendateformat: formated, + fireId: fire._id, + geo: fire.ourid + }; + return FalsePositives.upsert({ geo: fire.ourid, owner }, { $set: set }, { multi: false, upsert: true }); + } catch (exception) { + console.log(exception); + throw new Meteor.Error('500', exception); + } +} + Meteor.methods({ 'falsePositives.insert': function falsePositivesInsert(fireId, keyType) { check(fireId, Meteor.Collection.ObjectID); @@ -17,27 +40,9 @@ Meteor.methods({ } check(this.userId, String); const fire = Fires.findOne(fireId); + const owner = this.userId; if (fire) { - const fireType = fire.type; - if (fireType === 'vecinal') { - throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego'); - } - const date = new Date(); - const formated = moment(date).format('YYYYMMDD'); - try { - const set = { - owner: this.userId, - type: keyType, - when: date, - whendateformat: formated, - fireId, - geo: fire.ourid - }; - return FalsePositives.upsert({ geo: fire.ourid, owner: this.userId }, { $set: set }, { multi: false, upsert: true }); - } catch (exception) { - console.log(exception); - throw new Meteor.Error('500', exception); - } + upsertFalsePositive(keyType, owner, fire); } else { throw new Meteor.Error('500', 'Fuego no encontrado'); } diff --git a/imports/api/Fires/server/publications.js b/imports/api/Fires/server/publications.js index fe315b0..2cce7b7 100644 --- a/imports/api/Fires/server/publications.js +++ b/imports/api/Fires/server/publications.js @@ -139,7 +139,7 @@ function logUrl(fireEnc, params) { ravenLogger.log(message); } -Meteor.publish('fireFromHash', function fireFromHash(fireEnc, params) { +export function fireFromHash(fireEnc, params) { check(fireEnc, String); check(params, Object); try { @@ -163,11 +163,17 @@ Meteor.publish('fireFromHash', function fireFromHash(fireEnc, params) { FiresCollection.schema.validate(unsealedFix); return findOrCreateFire(unsealedFix); /* console.log(`fires: ${fire.count()}`); - * return fire; */ + * return fire; */ } catch (e) { console.warn(e); logUrl(fireEnc, params); return this.ready(); } +} + +Meteor.publish('fireFromHash', function fireFromHashFunc(fireEnc, params) { + check(fireEnc, String); + check(params, Object); + return fireFromHash(fireEnc, params); }); diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index a5296ae..fec971d 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -9,6 +9,7 @@ import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; +import { fireFromHash } from '/imports/api/Fires/server/publications.js'; import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; import Industries from '/imports/api/Industries/Industries'; import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; @@ -16,7 +17,7 @@ import jsend from 'jsend'; import { Random } from 'meteor/random'; import { subscriptionsInsert } from '/imports/api/Subscriptions/methods.js'; import { Mongo } from 'meteor/mongo'; - +import { upsertFalsePositive } from '/imports/api/FalsePositives/methods.js'; const debug = false; const uptime = new Date(); @@ -394,4 +395,54 @@ if (!Meteor.settings.private.internalApiToken) { return jsend.success({ count: toRemove }); } }); + + apiV1.addRoute('status/subs-public-union/:token', { authRequired: false }, { + get: function get() { + const { token } = this.urlParams; + try { + check(token, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const currentUnion = SiteSettings.findOne({ name: 'subs-public-union' }); + const userSubsBounds = SiteSettings.findOne({ name: 'subs-public-union-bounds' }); + + return jsend.success({ union: currentUnion, bounds: userSubsBounds }); + } + }); + + apiV1.addRoute('mobile/falsepositive', { authRequired: false }, { + post: function post() { + const { + token, + mobileToken, + sealed, + type + } = this.bodyParams; + try { + check(token, String); + check(mobileToken, String); + check(sealed, String); + check(type, String); + } catch (e) { + if (debug) console.log(e); + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + if (!user) return failMsg('User not found'); + + // TODO + const fire = fireFromHash(sealed, {}); + const result = upsertFalsePositive(type, user._id, fire); + return jsend.success({ upsert: result }); + } + }); } From 5429dfbc045452dcbf806164246c1a5756493030 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Sun, 5 Aug 2018 10:44:17 +0200 Subject: [PATCH 35/84] bind publish --- imports/api/Fires/server/publications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/api/Fires/server/publications.js b/imports/api/Fires/server/publications.js index 2cce7b7..03d95c3 100644 --- a/imports/api/Fires/server/publications.js +++ b/imports/api/Fires/server/publications.js @@ -175,5 +175,5 @@ export function fireFromHash(fireEnc, params) { Meteor.publish('fireFromHash', function fireFromHashFunc(fireEnc, params) { check(fireEnc, String); check(params, Object); - return fireFromHash(fireEnc, params); + return fireFromHash(fireEnc, params).bind(this); }); From 5e6271bb8846d054c2f1e205a682916347be0a05 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Sun, 5 Aug 2018 10:58:35 +0200 Subject: [PATCH 36/84] Trying to fix fireToHash func export --- imports/api/Fires/server/publications.js | 53 ++++++++++++------------ 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/imports/api/Fires/server/publications.js b/imports/api/Fires/server/publications.js index 03d95c3..acb4d1e 100644 --- a/imports/api/Fires/server/publications.js +++ b/imports/api/Fires/server/publications.js @@ -142,38 +142,37 @@ function logUrl(fireEnc, params) { export function fireFromHash(fireEnc, params) { check(fireEnc, String); check(params, Object); - try { - // console.log(fireEnc); - // const unsealed = Promise.await(urlEnc.decrypt(fireEnc)); - const unsealed = Promise.await(unsealW(fireEnc)); - if (unsealed === undefined) { - logUrl(fireEnc, params); - // https://guide.meteor.com/data-loading.html - return this.ready(); - } - const w = unsealed.when; - unsealed.when = new Date(w); - const c = unsealed.createdAt; - unsealed.createdAt = !c ? new Date() : new Date(c); - const u = unsealed.updatedAt; - unsealed.updatedAt = !u ? new Date() : new Date(u); - // console.log(unsealed); - // FIXME: - const unsealedFix = fixConfidence(unsealed); - FiresCollection.schema.validate(unsealedFix); - return findOrCreateFire(unsealedFix); - /* console.log(`fires: ${fire.count()}`); - * return fire; */ - } catch (e) { - console.warn(e); - logUrl(fireEnc, params); - return this.ready(); + // console.log(fireEnc); + // const unsealed = Promise.await(urlEnc.decrypt(fireEnc)); + const unsealed = Promise.await(unsealW(fireEnc)); + if (unsealed === undefined) { + throw Error('Undefined unsealed'); } + const w = unsealed.when; + unsealed.when = new Date(w); + const c = unsealed.createdAt; + unsealed.createdAt = !c ? new Date() : new Date(c); + const u = unsealed.updatedAt; + unsealed.updatedAt = !u ? new Date() : new Date(u); + // console.log(unsealed); + // FIXME: + const unsealedFix = fixConfidence(unsealed); + FiresCollection.schema.validate(unsealedFix); + return findOrCreateFire(unsealedFix); + /* console.log(`fires: ${fire.count()}`); + * return fire; */ } Meteor.publish('fireFromHash', function fireFromHashFunc(fireEnc, params) { check(fireEnc, String); check(params, Object); - return fireFromHash(fireEnc, params).bind(this); + try { + return fireFromHash(fireEnc, params); + } catch (e) { + console.warn(e); + logUrl(fireEnc, params); + // https://guide.meteor.com/data-loading.html + return this.ready(); + } }); From 759797e412e4779470c2d953cf5c4fee84642077 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Wed, 8 Aug 2018 08:42:52 +0200 Subject: [PATCH 37/84] Fix for rest method for false positive stats when no fires --- .../api/FalsePositives/server/publications.js | 7 +++++ imports/api/Rest/Rest.js | 28 +++++++++---------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/imports/api/FalsePositives/server/publications.js b/imports/api/FalsePositives/server/publications.js index ca66105..1816344 100644 --- a/imports/api/FalsePositives/server/publications.js +++ b/imports/api/FalsePositives/server/publications.js @@ -27,6 +27,13 @@ export const firesUnion = (fires) => { return union; }; +export const zoneToUnion = (lat, lon, distance) => { + const group = new L.FeatureGroup(); + const remap = [{ location: { lat, lon }, distance }]; + const union = calcUnion(remap, group, sub => sub); + return union; +}; + export const whichAreFalsePositives = (collection, union) => { const result = collection.find({ geo: { diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index fec971d..b5a7635 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -8,7 +8,7 @@ import { check } from 'meteor/check'; import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; -import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; +import { whichAreFalsePositives, firesUnion, zoneToUnion } from '/imports/api/FalsePositives/server/publications'; import { fireFromHash } from '/imports/api/Fires/server/publications.js'; import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; import Industries from '/imports/api/Industries/Industries'; @@ -166,22 +166,22 @@ if (!Meteor.settings.private.internalApiToken) { return result; } + let union; + // TODO only get real const firesA = fires.fetch(); - if (firesA.length > 0) { - const union = firesUnion(fires); - const falsePos = whichAreFalsePositives(FalsePositives, union); - const industries = whichAreFalsePositives(Industries, union); - result.fires = firesA; - result.industries = industries.fetch(); - result.falsePos = falsePos.fetch(); - return result; - } - // otherwise - return { - total: 0, real: 0, fires: [], falsePos: [], industries: [] - }; + if (firesA.length > 0) { + union = firesUnion(fires); + } else { + union = zoneToUnion(lat, lng, km); + } + const falsePos = whichAreFalsePositives(FalsePositives, union); + const industries = whichAreFalsePositives(Industries, union); + result.fires = firesA; + result.industries = industries.fetch(); + result.falsePos = falsePos.fetch(); + return result; } // Maps to: /api/v1/fires-in/:lat/:lng/:km From 15ed7e917ab07408da5c15cf936a35483dc21939 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Fri, 10 Aug 2018 09:07:05 +0200 Subject: [PATCH 38/84] Adding log to false positive --- imports/api/Rest/Rest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index b5a7635..f663d1f 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -441,6 +441,7 @@ if (!Meteor.settings.private.internalApiToken) { // TODO const fire = fireFromHash(sealed, {}); + console.log(`Marking fire as false positive: ${fire}`); const result = upsertFalsePositive(type, user._id, fire); return jsend.success({ upsert: result }); } From 8a741dc729f3afc8f0185d5c7e86ee9634ffd4ca Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Fri, 10 Aug 2018 11:12:33 +0200 Subject: [PATCH 39/84] Adding log to false positive --- imports/api/Rest/Rest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index f663d1f..dc47750 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -441,7 +441,7 @@ if (!Meteor.settings.private.internalApiToken) { // TODO const fire = fireFromHash(sealed, {}); - console.log(`Marking fire as false positive: ${fire}`); + console.log(`Marking fire as false positive: ${JSON.stringify(fire)}`); const result = upsertFalsePositive(type, user._id, fire); return jsend.success({ upsert: result }); } From bbdc53bda1d3fb85ddcd23a6c7ee00e9af033fc7 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Sat, 11 Aug 2018 08:15:04 +0200 Subject: [PATCH 40/84] More logs in Rest --- imports/api/Rest/Rest.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index dc47750..14baca0 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -439,11 +439,14 @@ if (!Meteor.settings.private.internalApiToken) { const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); if (!user) return failMsg('User not found'); - // TODO - const fire = fireFromHash(sealed, {}); - console.log(`Marking fire as false positive: ${JSON.stringify(fire)}`); - const result = upsertFalsePositive(type, user._id, fire); - return jsend.success({ upsert: result }); + console.log(`Marking hash fire as '${type}' false positive: ${sealed}`); + const fire = fireFromHash(sealed, { id: sealed }); + if (fire && typeof fire === 'object') { + console.log(`Marking fire as false positive: ${fire._id}`); + const result = upsertFalsePositive(type, user._id, fire); + return jsend.success({ upsert: result }); + } + return failMsg('Cannot mark fire as false positive'); } }); } From 3defbb01a2979f7850b4a2bf9b4d42c63d1c2dbb Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Wed, 15 Aug 2018 06:18:51 +0200 Subject: [PATCH 41/84] Fix for false positive rest call --- imports/api/Rest/Rest.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index 14baca0..5166f12 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -440,8 +440,9 @@ if (!Meteor.settings.private.internalApiToken) { if (!user) return failMsg('User not found'); console.log(`Marking hash fire as '${type}' false positive: ${sealed}`); - const fire = fireFromHash(sealed, { id: sealed }); - if (fire && typeof fire === 'object') { + const fireC = fireFromHash(sealed, { id: sealed }); + if (fireC && typeof fireC === 'object') { + const fire = fireC.fetch()[0]; console.log(`Marking fire as false positive: ${fire._id}`); const result = upsertFalsePositive(type, user._id, fire); return jsend.success({ upsert: result }); From ffef42d19ad799dad26befd40eb6c71401c1ac05 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Wed, 15 Aug 2018 07:26:39 +0200 Subject: [PATCH 42/84] Added play button --- imports/ui/pages/Index/Index.js | 6 +++++- package-lock.json | 12 +++++++++++- package.json | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/imports/ui/pages/Index/Index.js b/imports/ui/pages/Index/Index.js index 5d6b130..6735852 100644 --- a/imports/ui/pages/Index/Index.js +++ b/imports/ui/pages/Index/Index.js @@ -11,6 +11,7 @@ import PropTypes from 'prop-types'; // import { Link } from 'react-router-dom'; // https://www.npmjs.com/package/react-resize-detector import ReactResizeDetector from 'react-resize-detector'; +import MobileStoreButton from 'react-mobile-store-button'; import _ from 'lodash'; import 'html5-device-mockups/dist/device-mockups.min.css'; import 'bootstrap-carousel-swipe/carousel-swipe'; @@ -20,7 +21,6 @@ import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap'; import ShareIt from '/imports/ui/components/ShareIt/ShareIt'; import getFallbackLang from '/imports/modules/lang-fallback'; import FiresMap from '../FiresMap/FiresMap'; - import './Index.scss'; import './Index-custom.scss'; @@ -102,6 +102,7 @@ class Index extends Component { render() { const { t } = this.props; const title = `${t('AppName')}: ${t('Inicio')}`; + const androidUrl = 'https://play.google.com/store/apps/details?id=org.comunes.fires'; return (
@@ -181,6 +182,9 @@ class Index extends Component {

Siempre alerta a los fuegos en nuestro vecindario

{/* {this.props.t('Participa')} */} +

+ +

diff --git a/package-lock.json b/package-lock.json index 5a3aab1..6d65294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6227,7 +6227,8 @@ }, "jsbn": { "version": "0.1.1", - "bundled": true + "bundled": true, + "optional": true }, "json-schema": { "version": "0.2.3", @@ -17922,6 +17923,15 @@ "resolved": "https://registry.npmjs.org/react-leaflet-sidebarv2/-/react-leaflet-sidebarv2-0.5.1.tgz", "integrity": "sha512-HaEeIq3TfK1Hy7xIuX3hv895LNpu1xIBCgW+I1INrqX/95nNRw6ItNQOxmk0YO5ImrSaDeV6rDXKsAGojjngdg==" }, + "react-mobile-store-button": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/react-mobile-store-button/-/react-mobile-store-button-0.0.3.tgz", + "integrity": "sha512-fuMUeaR2yxOvPfYV2TOsQT+bzenEHGSg5PqKrCyn34m13loZEUf2BJuJ5J8k4EvWPHmljLDfc5dnGhaKd4qVTQ==", + "requires": { + "prop-types": "15.6.0", + "react": "16.2.0" + } + }, "react-overlays": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.7.4.tgz", diff --git a/package.json b/package.json index 0954e23..11f0684 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "react-leaflet-fullscreen": "0.0.6", "react-leaflet-google": "^3.2.1", "react-leaflet-sidebarv2": "^0.5.1", + "react-mobile-store-button": "0.0.3", "react-places-autocomplete": "^5.4.3", "react-progress-bar.js": "^0.2.3", "react-resize-detector": "^1.1.0", From 8e729a6939820b6b00766fe37014113bbee5c5b1 Mon Sep 17 00:00:00 2001 From: lupe bao reixa Date: Thu, 16 Aug 2018 09:03:22 +0000 Subject: [PATCH 43/84] Translated using Weblate (Galician) Currently translated at 100.0% (221 of 221 strings) --- public/locales/gl/common.json | 224 +++++++++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/public/locales/gl/common.json b/public/locales/gl/common.json index 0967ef4..bea98c9 100644 --- a/public/locales/gl/common.json +++ b/public/locales/gl/common.json @@ -1 +1,223 @@ -{} +{ + "fireDetected": "lume detectado {{when}}", + "fireDetectedAt": "lume detectado o {{when}}", + "Sí": "Si", + "No": "Non", + "AppName": "Tod@s contra o Lume", + "AppNameFull": "Tod@s contra o Lume!", + "AppDescrip": "Colaboración masiva contra os incendios", + "AppDescripLong": "Empregamos diferentes fontes de datos para notificarche os lumes activos nas túas zonas de interese", + "OrgName": "Comúns", + "OrgNameFull": "Asociación Comúns", + "Privacidad": "Privacidade", + "Términos": "Termos", + "Términos de Servicio": "Termos do Servizo", + "Inicio": "Inicio", + "Licencia": "Licencia", + "Cerrar sesión": "Pechar sesión", + "Registrarse": "Rexistrarse", + "Regístrate": "Rexístrate", + "Iniciar sesión": "Iniciar sesión", + "o regístrate con un correo": "ou rexístrate cun correo", + "o con un correo": "ou cun correo", + "Nombre": "Nome", + "Apellidos": "Apelidos", + "Correo electrónico": "Correo electrónico", + "Contraseña": "Contrasinal", + "¿Ya tienes un cuenta?": "Xa tes unha conta?", + "Usa al menos seis caracteres.": "Emprega polo menos seis caracteres.", + "Iniciar sesión con Google": "Iniciar sesión con Google", + "¿Olvidaste tu contraseña?": "Esqueciches o teu contrasinal?", + "¿No tienes una cuenta?": "Non tes unha conta?", + "Introduce tu correo abajo para recibir un enlace para resetear tu contraseña.": "Introduce o teu correo abaixo para recibir un enlace para resetear o teu contrasinal.", + "Recupera tu contraseña": "Recupera o teu contrasinal", + "¿Recuerdas tu contraseña?": "Lembras o teu contrasinal?", + "Necesitamos un correo aquí.": "Necesitamos un correo aquí.", + "¿Es este correo correcto?": "É iste correo correcto?", + "Necesitamos una contraseña aquí.": "Necesitamos un contrasinal aquí.", + "Bienvenid@ de nuevo": "Benvid@ de novo", + "Guardar perfíl": "Gardar perfil", + "Contraseña actual": "Contrasinal actual", + "Nueva contraseña": "Novo contrasinal", + "Editar perfíl": "Editar perfil", + "Editar perfíl en": "Editar perfil en", + "¡Perfíl actualizado!": "Perfil actualizado!", + "¿Cuál es tu nombre?": "Cal é o teu nome?", + "¿Cuál es tu apellido?": "Cal é o teu apelido?", + "Necesito tu contraseña si la quieres cambiar.": "Necesito o teu contrasinal se o queres cambiar.", + "Necesito tu nueva contraseña si la quieres cambiar.": "Necesito o teu novo contrasinal se o queres cambiar.", + "¿Es correcto este correo?": "É correcto iste correo?", + "verifyEmail": "Ei! pódesnos verificar o teu correo electrónico ({{email}})?", + "Reenviar email de verificación": "Reenviar email de verificación", + "checkVerificationEmail": "Comproba no teu correo {{email}} o enlace de verificación!", + "checkResetEmail": "Comproba no teu correo {{email}} o enlace de reseteo!", + "¡Listo, gracias!": "Listo, grazas!", + "Por favor, inténtalo otra vez.": "Por favor, téntao de novo.", + "Verificando...": "Verificando...", + "Introduce una nueva contraseña, por favor.": "Introduce un novo contrasinal, por favor.", + "Repite tu nueva contraseña, por favor.": "Repite o teu novo contrasinal, por favor.", + "Mmmm, tus contraseñas no coinciden. Inténtalo otra vez": "Mmmm, os teus contrasinais non coinciden. Téntao de novo", + "Para resetear tu contraseña, introduce una nueva debajo. Iniciarás la sesión con la nueva contraseña.": "Para resetear o teu contrasinal, introduce un novo debaixo. Iniciarás a sesión co novo contrasinal.", + "Resetea tu contraseña": "Resetea o teu contrasinal", + "Repite la nueva contraseña": "Repite o novo contrasinal", + "Resetea la contraseña y entra": "Resetea o contrasinal e entra", + "Uso de Cookies": "Emprego de Cookies", + "Utilizamos cookies para asegurar un mejor uso de nuestra web. Si continúas navegando, consideramos que aceptas su uso": "Empregamos cookies para asegurar un mellor uso da nosa web. Se continúas a navegar, consideramos que aceptas o seu uso", + "Participa": "Participa", + "activeFires": "Lumes activos", + "Fuegos activos": "Lumes activos", + "noActiveFireInMapCount": "Non hai lumes activos nesta zona do mapa. <1><0>{{countTotal}} lumes activos no mundo.", + "activeFireInMapCount": "En vermello, <1><0>{{count,number}} lumes activos. <3><0>{{countTotal,number}} lumes activos no mundo.", + "activeNeigFireInMapCount": "En laranxa, os lumes notificados polas/os nosas/os usuarias/os recentemente.", + "Centrar en tu ubicación": "Centrar na túa ubicación", + "Resaltar en verde el área vigilada por nuestros usuarios/as": "Resaltar en verde a área vixiada por as/os nosas/os usuarias/os", + "Resaltar los fuegos con un marcador": "Resaltar os lumes cun marcador", + "mapPrivacy": "<0>Para preservar a privacidade das/os nosas/os usuarias/os, os datos reflectidos están aleatoriamente alterados e son só orientativos.", + "Nuevas notificaciones de {{app}}": "Novas notificacións de {{app}}", + "Imágenes capturadas por los satélites de la NASA muestran el humo de grandes incendios que se extienden sobre el Océano Pacífico. La actividad del fuego está delineada en rojo.": "Imaxes capturadas polos satélites da NASA amosan o fume de grandes incendios que se estenden sobre o Océano Pacífico. A actividade do lume esta deliñada en vermello.", + "Todavía sin subscriptiones": "Aínda sen subscricións", + "Suscripción añadida": "Suscrición engadida", + "Suscripción actualizada": "Suscrición actualizada", + "Desuscrito": "Desuscrita", + "Mis zonas": "As miñas zonas", + "Nueva zona": "Nova zona", + "Suscribirme a fuegos en este radio": "Suscribirme a lumes neste radio", + "Suscribirme a este radio": "Suscribirme a iste radio", + "Créditos": "Créditos", + "Escribe aquí un lugar": "Escribe aquí un lugar", + "Indícanos la posición de la zona a vigilar (por ej. tu pueblo, una calle, etc):": "Indícanos a posición da zona a vixiar (por ex. a túa aldea, unha rúa, etc):", + "También puedes seleccionar la zona en el mapa arrastrando el puntero naranja.": "Tamén podes seleccionar a zona no mapa arrastrando o punteiro laranxa.", + "¿A que distancia a la redonda quieres recibir notificaciones de fuegos?": "A que distancia darredor queres recibir notificacións de lumes?", + "Pulsa para activar": "Preme para activar", + "Arrastrar para seleccionar otro punto": "Arrastrar para seleccionar outro punto", + "Mapa gris de OpenStreetMap": "Mapa gris de OpenStreetMap", + "Mapa color de OpenStreetMap": "Mapa cor de OpenStreetMap", + "Mapa de carreteras de Google": "Mapa de estradas de Google", + "Mapa de terreno de Google": "Mapa de terreo de Google", + "Mapa de satélite de Google": "Mapa de satélite de Google", + "Zona añadida": "Zona engadida", + "Añadir zona": "Engadir zona", + "Editar": "Editar", + "Suscripciones a alertas de fuegos en zonas de mi interés": "Suscricións a alertas de lumes en zonas do meu interese", + "En verde, áreas de las que recibirás alertas de fuegos": "En verde, áreas das que recibirás alertas de lumes", + "Terminar": "Rematar", + "Pulsa para borrar": "Preme para borrar", + "Pulsa aquí para borrar la zona": "Preme aquí para borrar a zona", + "Los fuegos activos se actualizan en tiempo cuasi real.": "Os lumes activos actualízanse en tempo cuasi real.", + "Somos muchos ojos": "Somos moitos ollos", + "Usa nuestro bot de Telegram para estar al tanto de los fuegos en tus área": "Emprega o noso bot do Telegram para estar ó tanto dos lumes na túa área", + "Otros dispositivos": "Outros dispositivos", + "support-us-home": "Estamos a desenvolver as nosas ferramentas para outros dispositivos. Podes <1>contribuír a facelo posible.", + "Software Libre": "Software Libre", + "Suscríbete a alertas de fuegos": "Suscríbete a alertas de lumes", + "Gracias por Participar": "Grazas por Participar", + "También puedes seguirnos en la web": "Tamén nos podes seguir na web", + "Participar": "Participar", + "Dejarás de recibir notificaciones de fuegos en esa área ¿Estás seguro/a? ": "Deixarás de recibir notificacións de lumes nesa área. Estás segura/o? ", + "Sobre los datos y imágenes usados": "Sobre os datos e imaxes empregados", + "dev-with-us-home": "Todo o noso traballo é <1>sofware libre. <3>Traductoræs e <5>desenvolvedoræs sempre benvid@s.", + "Política de Privacidad": "Política de Privacidade", + "No estás suscrito a fuegos en ninguna zona": "Non tes suscrición a lumes en ningunha zona", + "Iniciar sesión con Telegram": "Iniciar sesión con Telegram", + "Idioma": "Idioma", + "Comentarios": "Comentarios", + "Guardar": "Gardar", + "Responder": "Responder", + "Borrar": "Borrar", + "Añadir un comentario": "Engadir un comentario", + "Añadir una respuesta": "Engadir unha resposta", + "Añadir comentario": "Engadir comentario", + "Necesitas iniciar sesión para": "Precisas iniciar a sesión para", + "añadir comentarios": "engadir comentarios", + "puntuar comentarios": "puntuar comentarios", + "responder": "responder", + "Más comentarios": "Máis comentarios", + "Elige un lugar": "Escolle un lugar", + "Elige un radio de vigilancia": "Escolle un radio de vixiancia", + "Recibe alertas de fuegos en esa zona": "Recibe alertas de lumes nesa zona", + "Alerta cuando hay un fuego": "Alerta cando hai un lume", + "Anterior": "Anterior", + "Siguiente": "Seguinte", + "Siempre alerta a los fuegos en nuestro vecindario": "Alerta sempre dos lumes na nosa veciñanza", + "Información adicional sobre fuego detectado en {{where}} el {{when}}": "Información adicional sobre lume detectado en {{where}} o {{when}}", + "Información adicional sobre fuego detectado el {{when}}": "Información adicional sobre lume detectado o {{when}}", + "Coordenadas:": "Coordenadas:", + "Fuego detectado por satélites de la NASA <1>": "Lume detectado por satélites da NASA <1>", + "Puedes añadir un comentario si tienes información adicional sobre este fuego.": "Podes engadir un comentario se tes información adicional sobre este lume", + "Por ejemplo:": "Por exemplo:", + "si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)": "se coñeces esta zona e como acceder ó lume (isto pode ser de moita axuda para apagalo se segue activo ou para investigalo nun futuro)", + "si conoces el motivo por el que comenzó el fuego": "se coñeces o motivo polo que empezou o lume", + "si quieres denunciar algún tipo de ilegalidad, incluso anónimamente": "se queres denunciar algún tipo de ilegalidade, incluso anonimamente", + "o cualquier otra información": "ou calquera outra información", + "Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.": "Faga zoom nunha zona do seu interese se quere que os lumes se actualicen en tempo real.", + "Notificaciones": "Notificacións", + "Recibe nuestras notificaciones de fuegos por correo o en tu navegador": "Recibe as nosas notificacións de lumes por correo ou no teu navegador", + "Fuego notificado por uno de nuestros usuarios/as <1>": "Lume notificado por un/ha das/os nosas/os usuarias/os", + "No recibirás notificaciones de fuegos en este equipo, solo por correo": "Non recibirás notificacións de lumes neste equipo, só por correo", + "not-found": "Upppps: Esta páxina non existe", + "general-error-title": "Upppps: Algo saiu mal", + "general-error-description": "Estamos a investigar o problema, volve tentalo nun cachiño", + "Más información sobre este fuego": "Máis información sobre este lume", + "Última actualización, {{when}}": "Última actualización, {{when}}", + "termsAccept": "Acepto as <1>condicións de servizo deste sitio", + "Bienvenid@!": "Benvid@!", + "welcome": "Benvid@", + "Has iniciado sesión con {{service}} usando la dirección de correo {{email}}.": "Iniciaches a sesión con {{service}} usando a dirección de correo {{email}}.", + "Verifica tu dirección de correo": "Verifica a túa dirección de correo", + "Zonas vigiladas": "Zonas vixiladas", + "En verde, las zonas vigiladas por nuestros usuari@s actualmente": "En verde, as zonas vixiladas polas/os nosas/os usuarias/os actualmente", + "Actualizado <1>, último fuego detectado <3>.": "Actualizado <1>, último lume detectado <3>.", + "Tomamos nota, ¡gracias por colaborar!": "Tomamos nota, grazas por colaborar!", + "Indícanos de que tipo de fuego se trata y ayúdanos así a mejorar nuestras notificaciones:": "Indícanos de que tipo de lume se trata e axúdanos así a mellorar as nosas notificacións:", + "¿No es un fuego forestal?": "Non é un lume forestal?", + "Regístrate o inicia sesión para aportar información sobre este fuego": "Rexístrate ou inicia sesión para aportar información sobre este lume", + "Fuego no encontrado": "Lume non atopado", + "Es una industria": "É unha industria", + "Es una quema controlada": "É unha queima controlada", + "Parece una falsa alarma": "Parece unha falsa alarma", + "Fuegos activos en el mundo actualizados en tiempo real": "Lumes activos no mundo actualizados en tempo real", + "Información adicional sobre fuego": "Información adicional sobre o lume", + "CO2emisions": "Sabías que os lumes <1>producen tanto CO² como os coches e <3>aproximadamente ⅕ de tódalas nosas emisións ?", + "Ayúdanos a combatir el cambio climático y a proteger el medioambiente": "Axúdanos a combatir o cambio climático e a protexer o medioambiente", + "Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017": "Lume en La Tuna, Los Ángeles, Estados Unidos, setembro do 2017", + "Polución en la ciudad de Almaty, Kazakhstan, enero de 2014": "Polución na cidade de Almaty, Kazakhstan, xaneiro do 2014", + "Fuente": "Fonte", + "NASA": "NASA", + "nuestros usuarios/as": "as nosas/os usuarias/os", + "Pulsa para más información": "Preme para máis información", + "Detectado": "Detectado", + "Zonas vigiladas por nuestros usuari@s actualmente": "Zonas vixiadas pol@s nos@s usuari@s actualmente", + "Iniciar sesión con Facebook": "Iniciar sesión con Facebook", + "Elige un tipo": "Escolle un tipo", + "Puedes participar en las traducciones": "Podes participar nas traducións", + "Aceptar": "Aceptar", + "Por favor, escribe aquí tu feedback...": "Por favor, escribe aquí o teu feedback...", + "Feedback": "Feedback", + "Enviar": "Enviar", + "Tu correo": "O teu correo", + "Desconectado del servidor, reconectando en %delay% segundos.": "Desconectando di servidor, reconectando en %delay% segundos.", + "Desconectado del servidor, reconectando...": "Desconectado do servidor, reconectando...", + "Reintentar ahora": "Reintentar agora", + "Feedback recibido, gracias...": "Feedback recibido, grazas...", + "Parece que este fuego no es un fuego forestal.": "Parece que este lume non é un lume forestal.", + "Sobre nosotr@s": "Sobre nós", + "Nosotr@s": "Nós", + "Sobre '{{appName}}'": "Sobre '{{appName}}'", + "¿Vecindarios vigilando y combatiendo fuegos? ¿de qué va todo esto?": "Veciñanzas vixiando e combatendo lumes? de vai todo isto?", + "noActiveNeigFireInMap": "Non hai lumes notificados recentemente polas/os nosas/os usuarias/os nesta zona.", + "Es una industria (fuente: registro oficial)": "É unha industria (fonte: rexistro oficial)", + "Es una industria (fuente: nuestros usuarios/as)": "É unha industria (fonte as/os nosas/os usuarias/os):", + "Pantalla completa": "Pantalla completa", + "Salir de pantalla completa": "Saír da pantalla completa", + "Escribe un lugar para centrar el mapa": "Escribe un lugar para centrar o mapa", + "geo-not-perms-error": "Upppps: parece que non temos permiso para coñecer a túa ubicación", + "geo-not-avail-error": "Upppps: parece que a túa ubicación non está dispoñible", + "geo-not-timeout-error": "Upppps: o teu dispositivo está a tardar demasiado en coñecer a túa ubicación", + "Estás subscrito a una zona muy grande": "Suscribícheste a unha zona grande de máis", + "Lugar no encontrado": "Lugar non atopado", + "on-already-subscribed": "Upppps: Xa te suscribiras a esta zona antes", + "Cancelar": "Cancelar", + "Nota: Las nubes pueden entorpecer la detección de fuegos activos.": "Nota: As nubes poden entorpecer a detección de lumes activos.", + "Hay más información sobre un fuego": "Hai máis información sobre un lume", + "esDecirTalDia": "É dicir o {{date}}" +} From f608fc4d2883f2d2b04dbc4120bc742acd0ae319 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 16 Aug 2018 17:32:45 +0200 Subject: [PATCH 44/84] Enabling Galego in interface --- imports/startup/common/i18n.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/imports/startup/common/i18n.js b/imports/startup/common/i18n.js index 9be09fd..36bf759 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -3,6 +3,8 @@ import moment from 'moment'; // Load the js langs import es from 'meteor-accounts-t9n/build/es'; import en from 'meteor-accounts-t9n/build/en'; +// TODO +import gl from 'meteor-accounts-t9n/build/gl'; const backOpts = { // path where resources get loaded from @@ -21,7 +23,10 @@ const shouldDebug = (forceDebug && !Meteor.isProduction); const i18nOpts = { backend: backOpts, // lng: 'es', - fallbackLng: ['es', 'en'], + fallbackLng: { + gl: ['es'], + default: ['en'] + }, sendMissingTo: 'fallback', interpolation: { escapeValue: false, // not needed for react!! @@ -35,7 +40,7 @@ const i18nOpts = { return value; } }, - whitelist: ['es', 'en'], // allowed languages + whitelist: ['es', 'en', 'gl'], // allowed languages load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en debug: shouldDebug, ns: 'common', From 9a31ab2457b71f30515b1cae6b5266aca773b665 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 16 Aug 2018 17:47:00 +0200 Subject: [PATCH 45/84] Commenting gl in t9 accounts --- imports/startup/common/i18n.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/startup/common/i18n.js b/imports/startup/common/i18n.js index 36bf759..218e46d 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -3,8 +3,8 @@ import moment from 'moment'; // Load the js langs import es from 'meteor-accounts-t9n/build/es'; import en from 'meteor-accounts-t9n/build/en'; -// TODO -import gl from 'meteor-accounts-t9n/build/gl'; +// TODO ask for translation of this +// import gl from 'meteor-accounts-t9n/build/gl'; const backOpts = { // path where resources get loaded from From ed08563cad225e80f192bab68fdef96d5ea8a5d2 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 16 Aug 2018 17:48:58 +0200 Subject: [PATCH 46/84] Thanks for translation --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e686902..2f0dee2 100644 --- a/README.md +++ b/README.md @@ -51,4 +51,5 @@ GNU APLv3. See our [LICENSE](https://github.com/comunes/todos-contra-el-fuego-we ## Thanks & other acknowlegments -(...) +Thanks indeed to: +- Lupe Bao Reixa for Galician translation From dc5e38f8d52372576126915c6931f02c52bc5951 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 16 Aug 2018 19:04:46 +0200 Subject: [PATCH 47/84] gl fallback to es in T9 --- imports/startup/client/i18n.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/imports/startup/client/i18n.js b/imports/startup/client/i18n.js index 2fdc6b4..384053f 100644 --- a/imports/startup/client/i18n.js +++ b/imports/startup/client/i18n.js @@ -17,7 +17,7 @@ import i18nOpts from '../common/i18n'; const detectorOptions = { // order and from where user language should be detected - order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'], + order: ['querystring', 'navigator', 'cookie', 'localStorage', 'htmlTag'], // keys or params to lookup language from lookupQuerystring: 'lng', @@ -61,6 +61,14 @@ if (sendMissing && Meteor.isDevelopment) { }; } +function setT9(lang) { + if (lang === 'gl') { + T9n.setLanguage('es'); + } else { + T9n.setLanguage(lang); + } +} + i18n.use(backend) .use(LngDetector) .use(Cache) @@ -75,7 +83,7 @@ i18n.use(backend) // document.title = t('AppName'); // Accounts translation // https://github.com/softwarerero/meteor-accounts-t9n - T9n.setLanguage(i18n.language); + setT9(i18n.language); // console.log(T9n.get('error.accounts.User not found')); moment.locale(i18n.language); @@ -104,7 +112,8 @@ Meteor.subscribe('userData'); // lang is there i18n.on('languageChanged', (lng) => { moment.locale(lng); - T9n.setLanguage(lng); + // TODO fix this when gl is translated + setT9(lng); Tracker.autorun((computation) => { if (Meteor.userId()) { // logged From e223ddb6439af1da2f7d4bbdfebad76a7c830e10 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 16 Aug 2018 22:44:38 +0200 Subject: [PATCH 48/84] Preload langs --- imports/startup/common/i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/startup/common/i18n.js b/imports/startup/common/i18n.js index 218e46d..e6ecb61 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -41,6 +41,7 @@ const i18nOpts = { } }, whitelist: ['es', 'en', 'gl'], // allowed languages + preload: ['es', 'en', 'gl'], load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en debug: shouldDebug, ns: 'common', From 829fae4f07d30bab1ffa9c63365df3422175b3cc Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Fri, 17 Aug 2018 05:39:03 +0200 Subject: [PATCH 49/84] Trying to get available list of langs in profile --- imports/startup/common/i18n.js | 1 - imports/ui/pages/Profile/Profile.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/imports/startup/common/i18n.js b/imports/startup/common/i18n.js index e6ecb61..218e46d 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -41,7 +41,6 @@ const i18nOpts = { } }, whitelist: ['es', 'en', 'gl'], // allowed languages - preload: ['es', 'en', 'gl'], load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en debug: shouldDebug, ns: 'common', diff --git a/imports/ui/pages/Profile/Profile.js b/imports/ui/pages/Profile/Profile.js index 665e9d7..38614ac 100644 --- a/imports/ui/pages/Profile/Profile.js +++ b/imports/ui/pages/Profile/Profile.js @@ -202,7 +202,7 @@ class Profile extends React.Component { {langName[i18n.language]}
- {i18n.languages.map(lang => ( + {Object.keys(i18n.services.resourceStore.data).map(lang => (
- {Object.keys(i18n.services.resourceStore.data).map(lang => ( + {enabledLangs.map(lang => (