Added web and email notifications. Tests. Fire page

This commit is contained in:
vjrj 2018-01-16 16:19:29 +01:00
parent df05b33e82
commit 3bf21c8caf
32 changed files with 696 additions and 88 deletions

View file

@ -37,7 +37,7 @@ alexwine:bootstrap-4
selaias:cookie-consent # Cookie consent selaias:cookie-consent # Cookie consent
gadicc:blaze-react-component # Add braze to react gadicc:blaze-react-component # Add braze to react
255kb:meteor-status # Connect status 255kb:meteor-status # Connect status
mixmax:smart-disconnect # Disconnect on lost focus # mixmax:smart-disconnect # Disconnect on lost focus (disabled to get notif)
flowkey:raven # js errors flowkey:raven # js errors
vjrj:piwik # Stats vjrj:piwik # Stats
mdg:geolocation mdg:geolocation
@ -45,7 +45,9 @@ natestrauser:publish-performant-counts
maximum:server-transform maximum:server-transform
mys:fonts # fonst in npm mys:fonts # fonst in npm
ostrio:mailer # mailer ostrio:mailer # mailer
mizzao:user-status # user status (online, etc)
# babrahams:constellation # dev utility
percolate:migrations # db migrations
ostrio:meteor-root ostrio:meteor-root
mizzao:user-status meteortesting:mocha
babrahams:constellation practicalmeteor:chai
percolate:migrations

View file

@ -7,16 +7,12 @@ accounts-oauth@1.1.15
accounts-password@1.5.0 accounts-password@1.5.0
alanning:roles@1.2.16 alanning:roles@1.2.16
aldeed:collection2-core@2.0.1 aldeed:collection2-core@2.0.1
aldeed:template-extension@4.0.0
alexwine:bootstrap-4@4.0.0-beta.2 alexwine:bootstrap-4@4.0.0-beta.2
allow-deny@1.1.0 allow-deny@1.1.0
audit-argument-checks@1.0.7 audit-argument-checks@1.0.7
autoupdate@1.3.12 autoupdate@1.3.12
babel-compiler@6.24.7 babel-compiler@6.24.7
babel-runtime@1.1.1 babel-runtime@1.1.1
babrahams:constellation@0.4.10
babrahams:editable-json@0.6.4
babrahams:temple@0.4.5
base64@1.0.10 base64@1.0.10
binary-heap@1.0.10 binary-heap@1.0.10
blaze@2.3.2 blaze@2.3.2
@ -27,13 +23,6 @@ caching-html-compiler@1.1.2
callback-hook@1.0.10 callback-hook@1.0.10
check@1.2.5 check@1.2.5
coffeescript@1.0.17 coffeescript@1.0.17
constellation:autopublish@0.4.4
constellation:console@1.4.5
constellation:plugins@0.4.7
constellation:position@0.4.4
constellation:session@0.4.5
constellation:subscriptions@0.4.5
constellation:tiny@0.4.4
dburles:mongo-collection-instances@0.3.5 dburles:mongo-collection-instances@0.3.5
ddp@1.4.0 ddp@1.4.0
ddp-client@2.2.0 ddp-client@2.2.0
@ -58,8 +47,6 @@ gadicc:blaze-react-component@1.4.0
geojson-utils@1.0.10 geojson-utils@1.0.10
github-oauth@1.2.0 github-oauth@1.2.0
google-oauth@1.2.4 google-oauth@1.2.4
gwendall:body-events@0.1.6
gwendall:session-json@0.1.7
hot-code-push@1.0.4 hot-code-push@1.0.4
html-tools@1.0.11 html-tools@1.0.11
htmljs@1.0.11 htmljs@1.0.11
@ -76,10 +63,11 @@ maximum:server-transform@0.5.0
mdg:geolocation@1.3.0 mdg:geolocation@1.3.0
meteor@1.8.0 meteor@1.8.0
meteor-base@1.2.0 meteor-base@1.2.0
meteortesting:browser-tests@0.1.2
meteortesting:mocha@0.5.0
minifier-css@1.2.16 minifier-css@1.2.16
minifier-js@2.2.1 minifier-js@2.2.1
minimongo@1.4.0 minimongo@1.4.0
mixmax:smart-disconnect@0.0.4
mizzao:timesync@0.3.4 mizzao:timesync@0.3.4
mizzao:user-status@0.6.7 mizzao:user-status@0.6.7
mobile-experience@1.0.5 mobile-experience@1.0.5
@ -105,6 +93,8 @@ peerlibrary:fiber-utils@0.6.0
peerlibrary:reactive-mongo@0.1.1 peerlibrary:reactive-mongo@0.1.1
peerlibrary:server-autorun@0.5.2 peerlibrary:server-autorun@0.5.2
percolate:migrations@1.0.2 percolate:migrations@1.0.2
practicalmeteor:chai@2.1.0_1
practicalmeteor:mocha-core@1.0.1
promise@0.10.0 promise@0.10.0
raix:eventemitter@0.1.3 raix:eventemitter@0.1.3
random@1.0.10 random@1.0.10

View file

@ -2,6 +2,7 @@
import { Mongo } from 'meteor/mongo'; import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import firesCommonSchema from '../Common/FiresSchema';
const ActiveFires = new Mongo.Collection('activefires', { idGeneration: 'MONGO' }); const ActiveFires = new Mongo.Collection('activefires', { idGeneration: 'MONGO' });
@ -17,6 +18,13 @@ ActiveFires.deny({
remove: () => true remove: () => true
}); });
ActiveFires.schema = new SimpleSchema(firesCommonSchema);
ActiveFires.attachSchema(ActiveFires.schema);
export default ActiveFires;
/* Sample: /* Sample:
* { * {
* "_id" : ObjectId("5a208d7579095a1adba48191"), * "_id" : ObjectId("5a208d7579095a1adba48191"),
@ -46,19 +54,3 @@ ActiveFires.deny({
* "createdAt" : ISODate("2017-11-30T08:07:33.720Z") * "createdAt" : ISODate("2017-11-30T08:07:33.720Z")
* } * }
* */ * */
ActiveFires.schema = new SimpleSchema({
lat: Number,
lon: Number,
scan: Number,
type: String,
acq_date: String,
acq_time: String,
when: Date,
createdAt: Date,
updatedAt: Date
});
ActiveFires.attachSchema(ActiveFires.schema);
export default ActiveFires;

View file

@ -0,0 +1,29 @@
/* eslint-disable import/no-absolute-path */
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
const firesCommonSchema = {
ourid: LocationSchema,
lat: Number,
lon: Number,
scan: Number,
type: String,
when: Date,
track: { type: Number, optional: true },
acq_date: { type: String, optional: true },
acq_time: { type: String, optional: true },
satellite: { type: String, optional: true },
confidence: { type: Number, optional: true },
version: { type: String, optional: true },
frp: { type: Number, optional: true },
daynight: { type: String, optional: true },
brightness: { type: Number, optional: true },
bright_t31: { type: Number, optional: true },
bright_ti4: { type: Number, optional: true },
bright_ti5: { type: Number, optional: true },
createdAt: defaultCreatedAt,
updatedAt: defaultUpdateAt
};
export default firesCommonSchema;

View file

@ -0,0 +1,25 @@
/* eslint-disable consistent-return */
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import firesCommonSchema from '../Common/FiresSchema';
const Fires = new Mongo.Collection('fires', { idGeneration: 'MONGO' });
Fires.allow({
insert: () => false,
update: () => false,
remove: () => false
});
Fires.deny({
insert: () => true,
update: () => true,
remove: () => true
});
Fires.schema = new SimpleSchema(firesCommonSchema);
Fires.attachSchema(Fires.schema);
export default Fires;

View file

@ -0,0 +1,22 @@
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import rateLimit from '/imports/modules/rate-limit';
import urlEnc from '/imports/modules/url-encode';
Meteor.methods({
'fire.decrypt': async function fireDecode(fireEnc) {
check(fireEnc, String);
const unsealed = await urlEnc.decrypt(fireEnc);
return unsealed;
}
});
rateLimit({
methods: [
'fire.decode'
],
limit: 5,
timeRange: 1000
});

View file

@ -0,0 +1,37 @@
/* eslint-disable import/no-absolute-path */
/* eslint-disable prefer-arrow-callback */
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import urlEnc from '/imports/modules/url-encode';
import { Promise } from 'meteor/promise';
import FiresCollection from '../Fires';
function findFire(unsealed) {
const fire = FiresCollection.find({ ourid: { type: 'Point', coordinates: [unsealed.lon, unsealed.lat] } });
return fire;
}
Meteor.publish('fireFromHash', function fireFromHash(fireEnc) {
check(fireEnc, String);
try {
// console.log(fireEnc);
const unsealed = Promise.await(urlEnc.decrypt(fireEnc));
const w = unsealed.when;
// console.log(w);
unsealed.when = new Date(w);
// console.log(unsealed);
FiresCollection.schema.validate(unsealed);
const fire = findFire(unsealed);
if (fire.count() === 0) {
const result = FiresCollection.upsert({ ourid: unsealed.ourid }, { $set: unsealed }, { multi: false, upsert: true });
console.log(JSON.stringify(result));
}
return findFire(unsealed);
/* console.log(`fires: ${fire.count()}`);
* return fire; */
} catch (e) {
console.error(e);
throw new Meteor.Error('500', e);
}
});

View file

@ -0,0 +1,39 @@
/* eslint-disable consistent-return */
/* eslint-disable import/no-absolute-path */
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
const Notifications = new Mongo.Collection('notifications', { idGeneration: 'MONGO' });
Notifications.allow({
insert: () => false,
update: () => false,
remove: () => false
});
Notifications.deny({
insert: () => true,
update: () => true,
remove: () => true
});
Notifications.schema = new SimpleSchema({
userId: String,
content: String,
geo: LocationSchema,
type: String,
webNotified: { type: Boolean, optional: true },
webNotifiedAt: { type: Date, optional: true },
emailNotified: { type: Boolean, optional: true },
emailNotifiedAt: { type: Date, optional: true },
when: Date,
createdAt: defaultCreatedAt,
updatedAt: defaultUpdateAt
});
Notifications.attachSchema(Notifications.schema);
export default Notifications;

View file

@ -0,0 +1,25 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Notifications from './Notifications';
import rateLimit from '../../modules/rate-limit';
Meteor.methods({
'notifications.sent': function notificationsUpdate(notifId) {
check(notifId, Meteor.Collection.ObjectID);
try {
Notifications.update(notifId, { $set: { webNotified: true, webNotifiedAt: new Date() } });
return notifId;
} catch (exception) {
throw new Meteor.Error('500', exception);
}
}
});
rateLimit({
methods: [
'notifications.sent'
],
limit: 5,
timeRange: 1000
});

View file

@ -0,0 +1,10 @@
/* eslint-disable prefer-arrow-callback */
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 });
console.log(`Notifications for user ${this.userId}: ${notif.count()}`);
return notif;
});

View file

@ -4,6 +4,7 @@
import { Mongo } from 'meteor/mongo'; import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
const Subscriptions = new Mongo.Collection('subscriptions', { idGeneration: 'MONGO' }); const Subscriptions = new Mongo.Collection('subscriptions', { idGeneration: 'MONGO' });
@ -39,36 +40,6 @@ Subscriptions.deny({
* } * }
* */ * */
// https://stackoverflow.com/questions/24492333/meteor-simple-schema-for-mongo-geo-location-data
// https://github.com/aldeed/meteor-simple-schema/issues/606
const LocationSchema = new SimpleSchema({
type: {
type: String,
allowedValues: ['Point']
},
coordinates: {
type: Array,
minCount: 2,
maxCount: 2,
custom: function custom() {
if (!(this.value[0] >= -90 && this.value[0] <= 90)) {
return 'lngOutOfRange';
}
if (!(this.value[1] >= -180 && this.value[1] <= 180)) {
return 'latOutOfRange';
}
return true;
}
},
'coordinates.$': {
type: Number
}
});
LocationSchema.messageBox.messages({
lonOutOfRange: '[label] longitude should be between -90 and 90',
latOutOfRange: '[label] latitude should be between -180 and 180'
});
Subscriptions.schema = new SimpleSchema({ Subscriptions.schema = new SimpleSchema({
location: Object, location: Object,

View file

@ -16,10 +16,10 @@ export default (options, user) => {
templateVars: { templateVars: {
applicationName, applicationName,
firstName, firstName,
welcomeUrl: Meteor.absoluteUrl('documents'), // e.g., returns http://localhost:3000/documents welcomeUrl: Meteor.absoluteUrl('subscriptions') // e.g., returns http://localhost:3000/documents
}, }
}) })
.catch((error) => { .catch((error) => {
throw new Meteor.Error('500', `${error}`); throw new Meteor.Error('500', `${error}`);
}); });
}; };

View file

@ -0,0 +1,34 @@
import SimpleSchema from 'simpl-schema';
// https://stackoverflow.com/questions/24492333/meteor-simple-schema-for-mongo-geo-location-data
// https://github.com/aldeed/meteor-simple-schema/issues/606
const LocationSchema = new SimpleSchema({
type: {
type: String,
allowedValues: ['Point']
},
coordinates: {
type: Array,
minCount: 2,
maxCount: 2,
custom: function custom() {
if (!(this.value[0] >= -90 && this.value[0] <= 90)) {
return 'lngOutOfRange';
}
if (!(this.value[1] >= -180 && this.value[1] <= 180)) {
return 'latOutOfRange';
}
return true;
}
},
'coordinates.$': {
type: Number
}
});
LocationSchema.messageBox.messages({
lonOutOfRange: '[label] longitude should be between -90 and 90',
latOutOfRange: '[label] latitude should be between -180 and 180'
});
export default LocationSchema;

View file

@ -7,6 +7,8 @@ import templateToHTML from './handlebars-email-to-html';
const sendEmail = (options, { resolve, reject }) => { const sendEmail = (options, { resolve, reject }) => {
try { try {
Meteor.defer(() => { Meteor.defer(() => {
// TODO: replace with import sendMail from '/imports/startup/server/email';
console.log(`Email options: ${options}`);
Email.send(options); Email.send(options);
resolve(); resolve();
}); });
@ -15,13 +17,15 @@ const sendEmail = (options, { resolve, reject }) => {
} }
}; };
export default ({ text, html, template, templateVars, ...rest }) => { export default ({
text, html, template, templateVars, ...rest
}) => {
if (text || html || template) { if (text || html || template) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
sendEmail({ sendEmail({
...rest, ...rest,
text: template ? templateToText(getPrivateFile(`email-templates/${template}.txt`), (templateVars || {})) : text, text: template ? templateToText(getPrivateFile(`email-templates/${template}.txt`), (templateVars || {})) : text,
html: template ? templateToHTML(getPrivateFile(`email-templates/${template}.html`), (templateVars || {})) : html, html: template ? templateToHTML(getPrivateFile(`email-templates/${template}.html`), (templateVars || {})) : html
}, { resolve, reject }); }, { resolve, reject });
}); });
} }

View file

@ -0,0 +1,6 @@
import { Meteor } from 'meteor/meteor';
import Iron from 'iron';
exports.encrypt = obj => Iron.seal(obj, Meteor.settings.private.ironPassword, Iron.defaults);
exports.decrypt = obj => Iron.unseal(obj, Meteor.settings.private.ironPassword, Iron.defaults);

View file

@ -13,4 +13,11 @@ import '../../api/FireAlerts/server/publications';
import '../../api/Subscriptions/methods'; import '../../api/Subscriptions/methods';
import '../../api/Subscriptions/server/publications'; import '../../api/Subscriptions/server/publications';
// TODO add rate-limit to these publications // TODO add rate-limit to these publications
import '../../api/Notifications/methods';
import '../../api/Notifications/server/publications';
import '../../api/Fires/methods';
import '../../api/Fires/server/publications';

View file

@ -18,7 +18,7 @@ const db = Meteor.users.rawDatabase(); // new Mongo.Collection('__mailTimeQueue_
// db.getCollection("__mailTimeQueue__").count() // db.getCollection("__mailTimeQueue__").count()
// https://litmus.com/community/discussions/4633-is-there-a-reliable-1px-horizontal-rule-method // https://litmus.com/community/discussions/4633-is-there-a-reliable-1px-horizontal-rule-method
const hr = `<table cellspacing="0" cellpadding="0" border="0" width="100%" style="width: 100% !important;"> export const hr = `<table cellspacing="0" cellpadding="0" border="0" width="100%" style="width: 100% !important;">
<tr> <tr>
<td align="left" valign="top" width="600px" height="1" style="background-color: #f0f0f0; border-collapse:collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; mso-line-height-rule: exactly; line-height: 1px;"><!--[if gte mso 15]>&nbsp;<![endif]--></td> <td align="left" valign="top" width="600px" height="1" style="background-color: #f0f0f0; border-collapse:collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; mso-line-height-rule: exactly; line-height: 1px;"><!--[if gte mso 15]>&nbsp;<![endif]--></td>
</tr> </tr>
@ -32,7 +32,11 @@ const MailQueue = new MailTime({
from(transport) { from(transport) {
// To pass spam-filters `from` field should be correctly set // To pass spam-filters `from` field should be correctly set
// for each transport, check `transport` object for more options // for each transport, check `transport` object for more options
return `"${i18n.t('AppName')}" <${transport.options.auth.user}>`; if (!Meteor.isProduction) {
// only for test purposes
return `${i18n.t('AppName')} <notify@example.org>`;
}
return `${i18n.t('AppName')} <${transport.options.auth.user}>`;
}, },
debug: true, debug: true,
concatEmails: true, // Concatenate emails to the same addressee concatEmails: true, // Concatenate emails to the same addressee

View file

@ -5,6 +5,7 @@ import i18nOpts from '../common/i18n';
// import moment from 'moment'; // import moment from 'moment';
// import { T9n } from 'meteor-accounts-t9n'; // import { T9n } from 'meteor-accounts-t9n';
// vi ostrio:meteor-root
i18nOpts.backend.loadPath = `${Meteor.absolutePath}/public${i18nOpts.backend.loadPath}`; i18nOpts.backend.loadPath = `${Meteor.absolutePath}/public${i18nOpts.backend.loadPath}`;
i18nOpts.backend.addPath = `${Meteor.absolutePath}/public${i18nOpts.backend.addPath}`; i18nOpts.backend.addPath = `${Meteor.absolutePath}/public${i18nOpts.backend.addPath}`;

View file

@ -5,3 +5,4 @@ import './fixtures';
import './email'; import './email';
import './IPGeocoder'; import './IPGeocoder';
import './migrations'; import './migrations';
import './notificationsObserver';

View file

@ -0,0 +1,89 @@
/* eslint-disable import/no-absolute-path */
import i18n from 'i18next';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import Notifications from '/imports/api/Notifications/Notifications';
import sendMail from '/imports/startup/server/email';
import { hr } from '/imports/startup/server/email';
import getOAuthProfile from '/imports/modules/get-oauth-profile';
import image from 'google-maps-image-api-url';
import { trim } from '/imports/ui/components/NotificationsObserver/util.js';
Meteor.startup(() => {
// https://stackoverflow.com/questions/1199352/smart-way-to-shorten-long-strings-with-javascript
function truncate(n, useWordBoundary) {
if (this.length <= n) {
return this;
}
const subString = this.substr(0, n - 1);
return `${useWordBoundary ?
subString.substr(0, subString.lastIndexOf(' ')) :
subString}...`;
}
// 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,
type: 'staticmap',
center: `${lat},${lng}`,
size: '640x480',
zoom: 16,
maptype: 'hybrid',
language: 'es',
markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}`
});
}
function imgEl(lat, lng) {
return `<img src="${imgUrl(lat, lng)}" width="640" height="480"/>`;
}
function process(notif) {
if (notif.type === 'web' && !notif.emailNotified) {
const user = Meteor.users.findOne({ _id: notif.userId });
const hasPassword = user.services && user.services.password && user.services.password.bcrypt;
// console.log(`Has password: ${hasPassword}`);
const OAuthProfile = getOAuthProfile({
password: hasPassword,
profile: user.profile
}, user);
const firstName = OAuthProfile ? OAuthProfile.name.first : user.profile.name.first;
const emailAddress = OAuthProfile ? OAuthProfile.email :
user && user.emails[0] && user.emails[0].verified ? user.emails[0].address : null;
if (emailAddress) {
const img = imgEl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
// FIXME use our map as url and static map as img
moment.locale(user.lang);
const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: moment(notif.when).format('LLL') })}).`;
const emailOpts = {
to: emailAddress,
userName: firstName,
sendAt: new Date(),
subject: truncate.apply(message, [50, true]),
text: `${message} ${url}`,
template: '<body><h2>{{appName}}</h2>{{{html}}}</body>',
appName: i18n.t('AppName'),
html: `<p>${message}</p>${hr}<p>${img}</p>`
};
sendMail(emailOpts, true);
Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } });
}
}
}
Notifications.find().observe({
added: function notifAdded(notif) {
process(notif);
},
changed: function notifChanged(updatedNotif, oldNotif) {
process(updatedNotif);
}
});
});

View file

@ -0,0 +1,12 @@
import { Meteor } from 'meteor/meteor';
import createHistory from 'history/createBrowserHistory';
const history = createHistory();
history.listen((location) => { // , action ) => {
// console.log(location.pathname);
// console.log(action); // PUSH, etc
Meteor.Piwik.trackPage(location.pathname);
});
export default history;

View file

@ -0,0 +1,70 @@
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Bert } from 'meteor/themeteorchef:bert';
import moment from 'moment';
import Notifications from '/imports/api/Notifications/Notifications';
import Push from 'push.js/bin/push.min.js';
import i18n from '/imports/startup/client/i18n';
import history from '../../components/History/History';
import { trim } from './util.js';
function process(notif) {
// No already notified
if (Push.Permission.has()) {
if (!notif.webNotified) {
Push.create(i18n.t('AppName'), {
body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: moment(notif.when).fromNow() })})`,
icon: '/n-fire-marker.png',
requireInteraction: true,
onClick: function onClickFocus() {
window.focus();
this.close();
history.push('/fires');
}
});
Meteor.call('notifications.sent', notif._id, (error) => {
if (error) {
Bert.alert(error.reason, 'danger');
}
});
}
}
}
// Observe for new notifications
Notifications.find().observe({
added: function notifAdded(notif) {
process(notif);
},
changed: function notifChanged(updatedNotif, oldNotif) {
process(updatedNotif);
}
});
Meteor.startup(() => {
if (Meteor.userId()) {
Meteor.subscribe('mynotifications');
// Check for notifications not processed at startup
Notifications.find({ webNotified: null }).forEach((notif) => {
process(notif);
});
}
Tracker.autorun(() => {
if (Meteor.userId()) {
Meteor.subscribe('mynotifications');
// console.log('Started notifications listener');
if (!Push.Permission.has()) {
Push.Permission.request(() => {
// on granted
Bert.alert(i18n.t('Perfecto, recibirás notificaciones de fuegos en este equipo y también por correo cuando no estés conectado'), 'success');
}, () => {
// on denied
Bert.alert(i18n.t('No recibirás notificaciones de fuegos en este equipo, solo por correo'), 'warning');
});
}
}
});
});

View file

@ -0,0 +1,4 @@
export const trim = function trimMess(message) {
// Removing utf8 suffix and prefix characters used in telegram but not useful here in browser
return message.replace(/^🔥 /, '').replace(/:$/, '');
};

View file

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import Blaze from 'meteor/gadicc:blaze-react-component'; import Blaze from 'meteor/gadicc:blaze-react-component';
const Reconnect = props => ( const Reconnect = props => (
@ -7,3 +9,12 @@ const Reconnect = props => (
); );
export default translate([], { wait: true })(Reconnect); export default translate([], { wait: true })(Reconnect);
if (!Meteor.isProduction) {
// We clear the console on disconnect during development
Tracker.autorun(() => {
if (Meteor.status().status === 'waiting') {
// console.clear();
}
});
}

View file

@ -4,7 +4,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Router, Switch, Route } from 'react-router-dom'; import { Router, Switch, Route } from 'react-router-dom';
import createHistory from 'history/createBrowserHistory';
import { Grid } from 'react-bootstrap'; import { Grid } from 'react-bootstrap';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
@ -41,17 +40,13 @@ import Privacy from '../../pages/Privacy/Privacy';
import License from '../../pages/License/License'; import License from '../../pages/License/License';
import Credits from '../../pages/Credits/Credits'; import Credits from '../../pages/Credits/Credits';
import ReSendEmail from '../../components/ReSendEmail/ReSendEmail'; import ReSendEmail from '../../components/ReSendEmail/ReSendEmail';
import history from '../../components/History/History';
import '../../components/NotificationsObserver/NotificationsObserver';
import FiresMap from '../../pages/FiresMap/FiresMap'; import FiresMap from '../../pages/FiresMap/FiresMap';
import Fires from '../../pages/Fires/Fires';
import Sandbox from '../../pages/Sandbox/Sandbox'; import Sandbox from '../../pages/Sandbox/Sandbox';
import './App.scss'; import './App.scss';
const history = createHistory();
history.listen((location) => { // , action ) => {
// console.log(location.pathname);
// console.log(action); // PUSH, etc
Meteor.Piwik.trackPage(location.pathname);
});
const App = props => ( const App = props => (
/* https://react.i18next.com/components/i18nextprovider.html */ /* https://react.i18next.com/components/i18nextprovider.html */
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
@ -72,9 +67,9 @@ const App = props => (
<Authenticated exact path="/subscriptions/new" component={NewSubscription} {...props} /> <Authenticated exact path="/subscriptions/new" component={NewSubscription} {...props} />
<Authenticated exact path="/subscriptions/:_id" component={ViewSubscription} {...props} /> <Authenticated exact path="/subscriptions/:_id" component={ViewSubscription} {...props} />
<Authenticated exact path="/subscriptions/:_id/edit" component={EditSubscription} {...props} /> <Authenticated exact path="/subscriptions/:_id/edit" component={EditSubscription} {...props} />
<Authenticated exact path="/profile" component={Profile} {...props} /> <Authenticated exact path="/profile" component={Profile} {...props} />
<Route path="/fires" component={FiresMap} {...props} /> <Route path="/fires" component={FiresMap} {...props} />
<Route path="/fire/:id" component={Fires} {...props} />
<Public path="/signup" component={Signup} {...props} /> <Public path="/signup" component={Signup} {...props} />
<Public path="/login" component={Login} {...props} /> <Public path="/login" component={Login} {...props} />
<Route path="/logout" component={Logout} {...props} /> <Route path="/logout" component={Logout} {...props} />

View file

@ -0,0 +1,116 @@
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import { translate } from 'react-i18next';
import { Col, Row } from 'react-bootstrap';
import { Meteor } from 'meteor/meteor';
import { Map, CircleMarker, Circle } from 'react-leaflet';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import moment from 'moment';
import FiresCollection from '/imports/api/Fires/Fires';
class Fire extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
/* componentDidUpdate() {
* const map = this.firemap.leafletElement;
* map.invalidateSize();
* } */
handleLeafletLoad(map) {
console.log(map);
if (map) {
// map.leafletELement.invalidateSize();
// map.invalidateSize();
}
}
render() {
const { loading, fire, t } = this.props;
return (
<div className="ViewFire">
{!loading &&
<Fragment>
<h4 className="page-header">
{t('Información sobre fuego detectado el día {{when}}', { when: moment(fire.when).format('LLLL') })}
</h4>
<Row>
<Col xs={12} sm={6} md={6} lg={6} >
{(fire.type === 'modis' || fire.type === 'virrs') &&
<p>{t('Detectado por satélites de la NASA')}
</p>
}
<h5>Comentarios</h5>
<p>{t('Añade un comentario si tienes información adicional sobre este fuego (por ejemplo, si está aún activo cómo acceder a él, o si conoces el motivo por el que comenzó el fuego, o si quieres denunciar algún tipo de ilegalidad relacionada con el fuego)')}
</p>
</Col>
<Col xs={12} sm={6} md={6} lg={6} >
<Map
ref={(map) => {
this.fireMap = map;
this.handleLeafletLoad(map);
}}
animate
sleep={false}
center={[fire.lat, fire.lon]}
zoom={8}
>
<Fragment>
<CircleMarker
center={[fire.lat, fire.lon]}
color="red"
stroke={false}
fillOpacity="1"
fill
radius={3}
/>
<Circle
center={[fire.lat, fire.lon]}
color="red"
fillColor="red"
fillOpacity={0.1}
radius={fire.scan * 1000}
/>
</Fragment>
<DefMapLayers />
</Map>
</Col>
</Row>
</Fragment>
}
</div>
);
}
}
Fire.propTypes = {
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
fire: PropTypes.object // .isRequired
};
Fire.defaultProps = {
};
// export default translate([], { wait: true })(withTracker((props) => {
const FireContainer = withTracker(({ match }) => {
const fireEncrypt = match.params.id;
const subscription = Meteor.subscribe('fireFromHash', fireEncrypt);
// console.log(`Subs ready: ${subscription.ready()}, fire: ${JSON.stringify(FiresCollection.findOne())}`);
return {
loading: !subscription.ready(),
fire: FiresCollection.findOne()
};
})(Fire);
// export default FireContainer;
export default translate([], { wait: true })(FireContainer);

View file

@ -53,7 +53,7 @@ class ResetPassword extends React.Component {
if (error) { if (error) {
Bert.alert(T9n.get(`error.accounts.${error.reason}`), 'danger'); Bert.alert(T9n.get(`error.accounts.${error.reason}`), 'danger');
} else { } else {
history.push('/documents'); history.push('/subscriptions');
} }
}); });
} }

View file

@ -19,12 +19,12 @@ class VerifyEmail extends React.Component {
Accounts.verifyEmail(match.params.token, (error) => { Accounts.verifyEmail(match.params.token, (error) => {
if (error) { if (error) {
Bert.alert(T9n.get(`error.accounts.${error.reason}`), 'danger'); Bert.alert(T9n.get(`error.accounts.${error.reason}`), 'danger');
this.setState({ error: T9n.get(`error.accounts.${error.reason}`) + ". " + this.t("Por favor, inténtalo otra vez.")}); this.setState({ error: `${T9n.get(`error.accounts.${error.reason}`)}. ${this.t('Por favor, inténtalo otra vez.')}` });
// this.setState({ error: `${error.reason}. Please try again.` }); // this.setState({ error: `${error.reason}. Please try again.` });
} else { } else {
setTimeout(() => { setTimeout(() => {
Bert.alert(this.t("¡Listo, gracias!"), 'success'); Bert.alert(this.t('¡Listo, gracias!'), 'success');
history.push('/documents'); history.push('/subscriptions');
}, 1500); }, 1500);
} }
}); });
@ -41,7 +41,7 @@ class VerifyEmail extends React.Component {
VerifyEmail.propTypes = { VerifyEmail.propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired
}; };
export default translate([], { wait: true })(VerifyEmail); export default translate([], { wait: true })(VerifyEmail);

43
package-lock.json generated
View file

@ -4741,6 +4741,11 @@
"resolved": "https://registry.npmjs.org/google-maps/-/google-maps-3.2.1.tgz", "resolved": "https://registry.npmjs.org/google-maps/-/google-maps-3.2.1.tgz",
"integrity": "sha1-wHWPKxdaSo0RDBq7H8TMOMEXbgY=" "integrity": "sha1-wHWPKxdaSo0RDBq7H8TMOMEXbgY="
}, },
"google-maps-image-api-url": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/google-maps-image-api-url/-/google-maps-image-api-url-1.0.3.tgz",
"integrity": "sha1-2ezc4OnvS+pIGZD05ZC0slg8jbo="
},
"graceful-fs": { "graceful-fs": {
"version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
@ -5175,6 +5180,39 @@
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
"dev": true "dev": true
}, },
"iron": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/iron/-/iron-5.0.4.tgz",
"integrity": "sha512-7iQ5/xFMIYaNt9g2oiNiWdhrOTdRUMFaWENUd0KghxwPUhrIH8DUY8FEyLNTTzf75jaII+jMexLdY/2HfV61RQ==",
"requires": {
"boom": "7.1.1",
"cryptiles": "4.1.1",
"hoek": "5.0.2"
},
"dependencies": {
"boom": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-7.1.1.tgz",
"integrity": "sha512-qwEARHTliqgEQiVkzKkkbLt3q0vRPIW60VRZ8zRnbjsm7INkPe9NxfAYDDYLZOdhxyUHa1gIe639Cx7t6RH/4A==",
"requires": {
"hoek": "5.0.2"
}
},
"cryptiles": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.1.tgz",
"integrity": "sha512-YuQUPbcOmaZsdvxJZ25DCA1W+lLIRoPJKBDKin+St1RCYEERSfoe1d25B1MvWNHN3e8SpFSVsqYvEUjp8J9H2w==",
"requires": {
"boom": "7.1.1"
}
},
"hoek": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.2.tgz",
"integrity": "sha512-NA10UYP9ufCtY2qYGkZktcQXwVyYK4zK0gkaFSB96xhtlo6V8tKXdQgx8eHolQTRemaW0uLn8BhjhwqrOU+QLQ=="
}
}
},
"is-arrayish": { "is-arrayish": {
"version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
@ -9607,6 +9645,11 @@
"version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
}, },
"push.js": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/push.js/-/push.js-1.0.5.tgz",
"integrity": "sha512-RPnZ7tc6oTPWf2g4dCIxIRgaJUENjor9CbyDg/o0JlsYqLOM0AjwHOndT9woZkt+7KI1srB9J70d5i6xiPd/HQ=="
},
"qs": { "qs": {
"version": "6.5.1", "version": "6.5.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",

View file

@ -16,6 +16,7 @@
"crypto-random-hex": "^1.0.0", "crypto-random-hex": "^1.0.0",
"fs": "0.0.1-security", "fs": "0.0.1-security",
"google-maps": "^3.2.1", "google-maps": "^3.2.1",
"google-maps-image-api-url": "^1.0.3",
"handlebars": "^4.0.11", "handlebars": "^4.0.11",
"history": "^4.7.2", "history": "^4.7.2",
"html5-device-mockups": "^3.2.0", "html5-device-mockups": "^3.2.0",
@ -26,6 +27,7 @@
"i18next-xhr-backend": "^1.5.0", "i18next-xhr-backend": "^1.5.0",
"immutability-helper": "^2.5.1", "immutability-helper": "^2.5.1",
"indexof": "0.0.1", "indexof": "0.0.1",
"iron": "^5.0.4",
"jquery": "^2.2.4", "jquery": "^2.2.4",
"jquery-validation": "^1.17.0", "jquery-validation": "^1.17.0",
"juice": "^4.2.2", "juice": "^4.2.2",
@ -43,6 +45,7 @@
"nodemailer": "^4.4.1", "nodemailer": "^4.4.1",
"popper.js": "^1.12.7", "popper.js": "^1.12.7",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"push.js": "^1.0.5",
"rc-slider": "^8.5.0", "rc-slider": "^8.5.0",
"rc-tooltip": "^3.7.0", "rc-tooltip": "^3.7.0",
"react": "^16.0.0", "react": "^16.0.0",

View file

@ -185,5 +185,9 @@
"Iniciar sesión con Telegram": "Iniciar sesión con Telegram":
"Iniciar sesión con Telegram", "Iniciar sesión con Telegram",
"Idioma": "Idioma":
"Idioma" "Idioma",
"fireDetected":
"fuego detectado {{when}}",
"fireDetectedAt":
"fuego detectado el {{when}}"
} }

62
test/encode.test.js Normal file
View file

@ -0,0 +1,62 @@
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
import { chai } from 'meteor/practicalmeteor:chai';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import urlEnc from '/imports/modules/url-encode';
describe('url encoding', () => {
it('should encrypt and dcrypt basic objects', async () => {
const obj = { lat: 2 };
const sealed = await urlEnc.encrypt(obj);
const unsealed = await urlEnc.decrypt(sealed);
chai.expect(unsealed).to.deep.equal(obj);
});
it('should encrypt and dcrypt objects with date', async () => {
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);
});
it('should encrypt complete objects', async () => {
const obj = {
ourid: {
type: 'Point',
coordinates: [
24.813,
9.223
]
},
type: 'modis',
lat: 9.223,
lon: 24.813,
when: new Date('2017-12-13T00:00:00.000Z').toISOString(),
scan: 1,
track: 1,
satellite: 'A',
confidence: 60,
version: '6.0NRT',
frp: 6.5,
daynight: null,
brightness: 305.9,
bright_t31: 292.6
};
const sealed = await urlEnc.encrypt(obj);
console.log(encodeURI(sealed));
const unsealed = await urlEnc.decrypt(sealed);
chai.expect(unsealed).to.deep.equal(obj);
chai.expect(sealed).to.equal(encodeURI(sealed));
});
// This fails because Date is return as String (not as Date)
/* it('should encrypt and dcrypt collection objects', async () => {
const obj = ActiveFiresCollection.findOne();
const sealed = await urlEnc.encrypt(obj);
const unsealed = await urlEnc.decrypt(sealed);
chai.expect(unsealed).to.deep.equal(obj);
}); */
});