Skip to content

Commit 6b78c2a

Browse files
authored
feat(user): add myBadges integration (#834)
* Add myBadges integration * initial user integrations * grant badge only if user has mybadges integration enabled * check integrations before updating user * check integrations object before usage
1 parent e4f3c3e commit 6b78c2a

File tree

7 files changed

+181
-28
lines changed

7 files changed

+181
-28
lines changed

config/config.example.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@
8484
// Default: 604800000 // 1 Week
8585
"validity_ms": 302400000
8686
},
87+
"integrations": {
88+
"mybadges": {
89+
"queue": "",
90+
"redis": {
91+
"host": "",
92+
"port": 6379,
93+
"username": "",
94+
"password": "",
95+
"db": 0
96+
}
97+
}
98+
},
8799
// Configuration for the @sensebox/opensensemap-api-models package
88100
"openSenseMap-API-models": {
89101
// Keys for configuring the mongo db connection

config/default.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,31 @@ const defaults = {
1919
boxes: '/boxes',
2020
users: '/users',
2121
statistics: '/statistics',
22-
management: '/management',
22+
management: '/management'
2323
},
2424
jwt: {
2525
secret: 'OH GOD THIS IS SO INSECURE PLS CHANGE ME', // should be at least 32 characters
2626
algorithm: 'HS256',
2727
validity_ms: 3600000, // 1 hour
28-
issuer: '', // usually the base url of the api. generated if not set from api_protocol and api_base_domain. for example https://api.opensensemap.org
28+
issuer: '' // usually the base url of the api. generated if not set from api_protocol and api_base_domain. for example https://api.opensensemap.org
2929
},
3030
refresh_token: {
3131
secret: 'I ALSO WANT TO BE CHANGED',
3232
algorithm: 'sha256',
33-
validity_ms: 604800000, // 1 week
33+
validity_ms: 604800000 // 1 week
3434
},
35+
integrations: {
36+
mybadges: {
37+
queue: 'badgr',
38+
redis: {
39+
host: 'localhost',
40+
port: 6379,
41+
username: 'queue',
42+
password: 'somepassword',
43+
db: 0
44+
}
45+
}
46+
}
3547
};
3648

3749
// computed keys

packages/api/lib/controllers/usersController.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ const { User } = require('@sensebox/opensensemap-api-models'),
5353
* @apiSuccess (Created 201) {Object} data `{ "user": {"name":"fullname","email":"test@test.de","role":"user","language":"en_US","boxes":[],"emailIsConfirmed":false} }`
5454
*/
5555
const registerUser = async function registerUser (req, res) {
56-
const { email, password, language, name } = req._userParams;
56+
const { email, password, language, name, integrations } = req._userParams;
5757

5858
try {
59-
const newUser = await new User({ name, email, password, language }).save();
59+
const newUser = await new User({ name, email, password, language, integrations }).save();
6060
postToMattermost(
6161
`New User: ${newUser.name} (${redactEmail(newUser.email)})`
6262
);
@@ -402,51 +402,50 @@ module.exports = {
402402
{ predef: 'password' },
403403
{ name: 'name', required: true, dataType: 'as-is' },
404404
{ name: 'language', defaultValue: 'en_US' },
405+
{ name: 'integrations', dataType: 'object' }
405406
]),
406-
registerUser,
407+
registerUser
407408
],
408409
signIn: [
409410
checkContentType,
410411
retrieveParameters([
411412
{ name: 'email', required: true },
412-
{ predef: 'password' },
413+
{ predef: 'password' }
413414
]),
414-
signIn,
415+
signIn
415416
],
416417
signOut,
417418
resetPassword: [
418419
checkContentType,
419420
retrieveParameters([
420421
{ name: 'token', required: true },
421-
{ predef: 'password' },
422+
{ predef: 'password' }
422423
]),
423-
resetPassword,
424+
resetPassword
424425
],
425426
requestResetPassword: [
426427
checkContentType,
427428
retrieveParameters([{ name: 'email', dataType: 'email', required: true }]),
428-
requestResetPassword,
429+
requestResetPassword
429430
],
430431
confirmEmailAddress: [
431432
checkContentType,
432433
retrieveParameters([
433434
{ name: 'token', required: true },
434-
{ name: 'email', dataType: 'email', required: true },
435+
{ name: 'email', dataType: 'email', required: true }
435436
]),
436-
confirmEmailAddress,
437+
confirmEmailAddress
437438
],
438439
requestEmailConfirmation,
439440
getUserBox: [
440-
retrieveParameters([
441-
{ predef: 'boxId', required: true }
442-
]),
443-
getUserBox,
441+
retrieveParameters([{ predef: 'boxId', required: true }]),
442+
getUserBox
444443
],
445444
getUserBoxes: [
446445
retrieveParameters([
447-
{ name: 'page', dataType: 'Integer', defaultValue: 0, min: 0 },
446+
{ name: 'page', dataType: 'Integer', defaultValue: 0, min: 0 }
448447
]),
449-
getUserBoxes,
448+
getUserBoxes
450449
],
451450
updateUser: [
452451
checkContentType,
@@ -456,18 +455,19 @@ module.exports = {
456455
{ predef: 'password', name: 'newPassword', required: false },
457456
{ name: 'name', dataType: 'as-is' },
458457
{ name: 'language' },
458+
{ name: 'integrations', dataType: 'object' }
459459
]),
460-
updateUser,
460+
updateUser
461461
],
462462
getUser,
463463
refreshJWT: [
464464
checkContentType,
465465
retrieveParameters([{ name: 'token', required: true }]),
466-
refreshJWT,
466+
refreshJWT
467467
],
468468
deleteUser: [
469469
checkContentType,
470470
retrieveParameters([{ predef: 'password' }]),
471-
deleteUser,
472-
],
471+
deleteUser
472+
]
473473
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
const log = require('@sensebox/opensensemap-api-models/src/log');
4+
const { Queue } = require('bullmq');
5+
const config = require('config').get('integrations.mybadges');
6+
7+
let queue;
8+
9+
const requestQueue = () => {
10+
if (queue) {
11+
return queue;
12+
}
13+
queue = new Queue(config.get('queue'), {
14+
connection: {
15+
host: config.get('redis.host'),
16+
port: config.get('redis.port'),
17+
username: config.get('redis.username'),
18+
password: config.get('redis.password'),
19+
db: config.get('redis.db'),
20+
}
21+
});
22+
23+
return queue;
24+
};
25+
26+
const grantBadge = async function (req) {
27+
28+
const integrationEnabled = req.user.get('integrations.mybadges.enabled');
29+
30+
if (!integrationEnabled) {return;}
31+
32+
const payload = {
33+
email: req.user.email,
34+
route: req.route
35+
};
36+
37+
return requestQueue()
38+
.add('grant-badge', payload, {
39+
removeOnComplete: true
40+
})
41+
.then((response) => {
42+
log.info({
43+
msg: 'Successfully added grant-badge to queue',
44+
job_id: response.id,
45+
template: response.name
46+
});
47+
});
48+
};
49+
50+
module.exports = {
51+
grantBadge: grantBadge
52+
};

packages/api/lib/routes.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const { usersController,
99
config = require('config'),
1010
{ getVersion } = require('./helpers/apiUtils'),
1111
{ verifyJwt } = require('./helpers/jwtHelpers'),
12-
{ initUserParams, checkPrivilege } = require('./helpers/userParamHelpers');
12+
{ initUserParams, checkPrivilege } = require('./helpers/userParamHelpers'),
13+
{ grantBadge } = require('./helpers/badgrQuery');
1314

1415
const spaces = function spaces (num) {
1516
let str = ' ';
@@ -140,7 +141,11 @@ const initRoutes = function initRoutes (server) {
140141
// The .use() method runs now for all routes
141142
// https://github.com/restify/node-restify/issues/1685
142143
for (const route of routes.auth) {
143-
server[route.method]({ path: route.path }, [verifyJwt, route.handler]);
144+
server[route.method]({ path: route.path }, [
145+
verifyJwt,
146+
route.handler,
147+
grantBadge
148+
]);
144149
}
145150

146151
// Attach verifyJwt and checkPrivilage routes (needs authorization through jwt)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
const { mongoose } = require('../db');
4+
5+
const myBadgesSchema = new mongoose.Schema(
6+
{
7+
enabled: {
8+
type: Boolean,
9+
default: false,
10+
required: false
11+
}
12+
},
13+
{ _id: false, usePushEach: true }
14+
);
15+
16+
const integrationSchema = new mongoose.Schema(
17+
{
18+
mybadges: {
19+
type: myBadgesSchema,
20+
required: false
21+
}
22+
},
23+
{ _id: false, usePushEach: true }
24+
);
25+
26+
const addIntegrationsToSchema = function addIntegrationsToSchema (schema) {
27+
schema.add({
28+
integrations: {
29+
type: integrationSchema
30+
}
31+
});
32+
};
33+
34+
module.exports = {
35+
schema: integrationSchema,
36+
addToSchema: addIntegrationsToSchema,
37+
// no model, because it is used as subdocument in userSchema
38+
};

packages/models/src/user/user.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const integrations = require('./integrations');
4+
35
/**
46
* Interesting reads:
57
* https://blogs.dropbox.com/tech/2016/09/how-dropbox-securely-stores-your-passwords/
@@ -11,6 +13,7 @@
1113
const { mongoose } = require('../db'),
1214
bcrypt = require('bcrypt'),
1315
crypto = require('crypto'),
16+
util = require('util'),
1417
{ min_length: password_min_length, salt_factor: password_salt_factor } = require('config').get('openSenseMap-API-models.password'),
1518
{ max_boxes: pagination_max_boxes } = require('config').get('openSenseMap-API-models.pagination'),
1619
{ v4: uuidv4 } = require('uuid'),
@@ -103,8 +106,22 @@ const userSchema = new mongoose.Schema({
103106
}, { usePushEach: true });
104107
userSchema.plugin(timestamp);
105108

106-
const toJSONProps = ['name', 'email', 'role', 'language', 'boxes', 'emailIsConfirmed'],
107-
toJSONSecretProps = ['_id', 'unconfirmedEmail', 'lastUpdatedBy', 'createdAt', 'updatedAt'];
109+
const toJSONProps = [
110+
'name',
111+
'email',
112+
'role',
113+
'language',
114+
'boxes',
115+
'emailIsConfirmed',
116+
'integrations'
117+
],
118+
toJSONSecretProps = [
119+
'_id',
120+
'unconfirmedEmail',
121+
'lastUpdatedBy',
122+
'createdAt',
123+
'updatedAt'
124+
];
108125

109126
// only send out names and email..
110127
userSchema.set('toJSON', {
@@ -469,7 +486,7 @@ userSchema.methods.resendEmailConfirmation = function resendEmailConfirmation ()
469486
});
470487
};
471488

472-
userSchema.methods.updateUser = function updateUser ({ email, language, name, currentPassword, newPassword }) {
489+
userSchema.methods.updateUser = function updateUser ({ email, language, name, currentPassword, newPassword, integrations }) {
473490
const user = this;
474491

475492
// don't allow email and password change in one request
@@ -531,6 +548,20 @@ userSchema.methods.updateUser = function updateUser ({ email, language, name, cu
531548
somethingsChanged = true;
532549
}
533550

551+
const existingUserIntegrations = user.get('integrations') ? user.get('integrations').toObject() : undefined;
552+
if (integrations && !existingUserIntegrations) {
553+
user.set('integrations', integrations);
554+
somethingsChanged = true;
555+
} else if (integrations && !util.isDeepStrictEqual(existingUserIntegrations, integrations)) {
556+
const mergedProperties = {
557+
...existingUserIntegrations,
558+
...integrations
559+
};
560+
user.set('integrations', mergedProperties);
561+
562+
somethingsChanged = true;
563+
}
564+
534565
if (somethingsChanged === false) {
535566
return { updated: false };
536567
}
@@ -680,6 +711,9 @@ userSchema.post('update', handleE11000);
680711
userSchema.post('findOneAndUpdate', handleE11000);
681712
userSchema.post('insertMany', handleE11000);
682713

714+
// add integrations schema as user.integrations
715+
integrations.addToSchema(userSchema);
716+
683717
const userModel = mongoose.model('User', userSchema);
684718

685719
module.exports = {

0 commit comments

Comments
 (0)