diff --git a/application/configs/instances.js b/application/configs/instances.js new file mode 100644 index 0000000..e2c8dfa --- /dev/null +++ b/application/configs/instances.js @@ -0,0 +1,41 @@ +'use strict'; + +module.exports = { + org: { + url: 'https://slidewiki.org', + emailcheck: 'https://userservice.slidewiki.org/information/email/', + entry: 'https://slidewiki.org/SSO', + login: 'https://userservice.slidewiki.org/login', + validate: 'https://userservice.slidewiki.org/social/provider/slidewiki', + userinfo: 'https://userservice.slidewiki.org/user/{id}/profile', + finalize: 'https://userservice.slidewiki.org/social/finalize/{hash}' + }, + aksw: { + url: 'https://slidewiki.aksw.org', + emailcheck: 'https://userservice.slidewiki.aksw.org/information/email/', + entry: 'https://slidewiki.aksw.org/SSO', + login: 'https://userservice.slidewiki.aksw.org/login', + validate: 'https://userservice.slidewiki.aksw.org/social/provider/slidewiki', + userinfo: 'https://userservice.slidewiki.aksw.org/user/{id}/profile', + finalize: 'https://userservice.slidewiki.aksw.org/social/finalize/{hash}' + }, + exp: { + url: 'https://platform.experimental.slidewiki.org', + emailcheck: 'https://userservice.experimental.slidewiki.org/information/email/', + entry: 'https://platform.experimental.slidewiki.org/SSO', + login: 'https://userservice.experimental.slidewiki.org/login', + validate: 'https://userservice.experimental.slidewiki.org/social/provider/slidewiki', + userinfo: 'https://userservice.experimental.slidewiki.org/user/{id}/profile', + finalize: 'https://userservice.experimental.slidewiki.org/social/finalize/{hash}' + }, + local: { + url: 'http://localhost:3000', + emailcheck: 'http://localhost:1500/information/email/', + entry: 'http://localhost:3000/SSO', + login: 'http://localhost:1500/login', + validate: 'http://localhost:1500/social/provider/slidewiki', + userinfo: 'http://localhost:1500/user/{id}/profile', + finalize: 'http://localhost:1500/social/finalize/{hash}' + }, + _self: 'local' +}; diff --git a/application/controllers/handler_social.js b/application/controllers/handler_social.js index a9a6c37..23849ce 100644 --- a/application/controllers/handler_social.js +++ b/application/controllers/handler_social.js @@ -1,3 +1,5 @@ +/*eslint no-case-declarations: "warn"*/ + /* Handles the requests by executing stuff and replying to the client. Uses promises to get stuff done. Instance for social function - handler.js got too big @@ -11,11 +13,15 @@ const boom = require('boom'), //Boom gives us some predefined http codes and pro providerCtrl = require('../database/provider'), config = require('../configuration'), jwt = require('./jwt'), + request = require('request'), socialProvider = require('./social_provider'), - util = require('./util'); + util = require('./util'), + instances = require('../configs/instances.js'), + ObjectId = require('mongodb').ObjectID; const PROVIDERS = ['github', 'google', 'facebook'], PLATFORM_SOCIAL_URL = require('../configs/microservices').platform.uri + '/socialLogin', + PLATFORM_MIGRATE_URL = require('../configs/microservices').platform.uri + '/migrateUser', PLATFORM_INFORMATION_URL = require('../configs/microservices').platform.uri + ''; module.exports = { @@ -430,9 +436,259 @@ module.exports = { return res(providers); }); + }, + + slidewiki: (req, res) => { + if (!instances[req.query.instance]) { + return res(boom.notFound('Instance unknown.')); + } + + //get detailed user + let headers = {}; + headers[config.JWT.HEADER] = req.query.jwt; + const options = { + url: instances[req.query.instance].userinfo.replace('{id}', req.query.userid), + method: 'GET', + json: true, + headers: headers + }; + + function callback(error, response, body) { + console.log('got detailed user: ', error, response.statusCode, body, options, body.providers); + + if (!error && (response.statusCode === 200) && body.username) { + let user = body; + user.userid = user._id + 0; + user._id = undefined; + user.providers = []; + user.groups = undefined; + user.hasPassword = false; + if (!user.frontendLanguage) + user.frontendLanguage = 'en'; + + //check if user is already migrated + let query = { + migratedFrom: { + instance: req.query.instance, + userid: util.parseStringToInteger(req.query.userid) + } + }; + userCtrl.find(query) + .then((cursor) => cursor.toArray()) + .then((array) => { + console.log('searched for already migrated user:', query, array); + if (array === undefined || array === null || array.length < 1) { + //now migrate it + return migrateUser(req, res, user); + } + else { + //do a sign in + return signInMigratedUser(req, res, user, array[0]); + } + }); + } else { + console.log('Error', (response) ? response.statusCode : undefined, error, body); + return res(boom.badImplementation('Failed to retrieve user data')); //TODO redirect to homepage? + } + } + + // if (process.env.NODE_ENV === 'test') { + // callback(null, {statusCode: 200}, {}); + // } + // else + request(options, callback); + }, + + finalizeUser: (req, res) => { + return util.isIdentityAssigned(req.payload.email, req.payload.username) + .then((result) => { + if (result.assigned) { + return res(boom.conflict()); + } + + return userCtrl.find({_id: ObjectId(req.params.hash)}, true) + .then((cursor) => cursor.toArray()) + .then((result) => { + console.log('finalizeUser: result: ', result, req.params.hash); + + switch (result.length) { + case 0: res(boom.notFound()); + break; + case 1: + let user = result[0]; + user.username = req.payload.username; + user.email = req.payload.email; + user._id = undefined; + for(let k in user) { + if (user[k] === null) + user[k] = undefined; + } + + console.log('create new user'); + //save the user as a new one + //Send email before creating the user + util.sendEMail(user.email, + 'Your new account on SlideWiki', + 'Dear '+user.forename+' '+user.surname+',\n\nwelcome to SlideWiki! You have migrated your account with the username '+user.username+' from '+instances[user.migratedFrom.instance].url+' to '+instances[instances._self].url+'. In order to start using your account and learn how get started with the platform please navigate to the following link:\n\n'+PLATFORM_INFORMATION_URL+'/welcome\n\nGreetings,\nthe SlideWiki Team') + .then(() => { + return userCtrl.create(user) + .then((result) => { + console.log('migrated user: create result: ', result.result); + + if (result[0] !== undefined && result[0] !== null) { + //Error + console.log('ajv error', result, co.parseAjvValidationErrors(result)); + return res(boom.badImplementation('registration failed because data is wrong: ' + co.parseAjvValidationErrors(result))); + } + + if (result.insertedCount === 1) { + //success + user._id = result.insertedId; + + return userCtrl.delete(ObjectId(req.params.hash), true) + .then(() => { + return res({ + userid: result.insertedId, + username: user.username + }) + .header(config.JWT.HEADER, jwt.createToken(user)); + }) + .catch((error) => { + console.log('Error - unable to delete user from temp. collection:', error); + res(boom.badImplementation('Error', error)); + }); + } + + res(boom.badImplementation()); + }) + .catch((error) => { + console.log('Error - create user failed:', error, 'used user object:', user); + res(boom.badImplementation('Error', error)); + }); + }) + .catch((error) => { + console.log('Error sending the email:', error); + return res(boom.badImplementation('Error', error)); + }); + break; + default: return res(boom.badImplementation()); + } + }); + }); } }; +function migrateUser(req, res, user) { + console.log('migrateUser()'); + user.migratedFrom = { + instance: req.query.instance, + userid: util.parseStringToInteger(user.userid) + 0, + url: instances[req.query.instance].url + }; + user.userid = undefined; + user.reviewed = undefined; + user.suspended = undefined; + user.lastReviewDoneBy = undefined; + user.registered = (new Date()).toISOString(); + + return userCtrl.find({ + $or: [ + { + username: user.username + }, + { + email: user.email + } + ] + }) + .then((cursor) => cursor.toArray()) + .then((array) => { + if (array === undefined || array === null || array.length < 1) { + console.log('create new user'); + //save the user as a new one + //Send email before creating the user + return util.sendEMail(user.email, + 'Your new account on SlideWiki', + 'Dear '+user.forename+' '+user.surname+',\n\nwelcome to SlideWiki! You have migrated your account with the username '+user.username+' from '+instances[req.query.instance].url+' to '+instances[instances._self].url+'. In order to start using your account and learn how get started with the platform please navigate to the following link:\n\n'+PLATFORM_INFORMATION_URL+'/welcome\n\nGreetings,\nthe SlideWiki Team') + .then(() => { + return userCtrl.create(user) + .then((result) => { + console.log('migrated user: create result: ', result.result); + + if (result[0] !== undefined && result[0] !== null) { + //Error + console.log('ajv error', result, co.parseAjvValidationErrors(result)); + return res(boom.badImplementation('registration failed because data is wrong: ' + co.parseAjvValidationErrors(result))); + } + + if (result.insertedCount === 1) { + //success + user._id = result.insertedId; + + return res() + .redirect(PLATFORM_MIGRATE_URL + '?data=' + encodeURIComponent(JSON.stringify({ + userid: result.insertedId, + username: user.username, + jwt: jwt.createToken(user) + }))) + .temporary(true); + } + + res(boom.badImplementation()); + }) + .catch((error) => { + console.log('Error - create user failed:', error, 'used user object:', user); + res(boom.badImplementation('Error', error)); + }); + }) + .catch((error) => { + console.log('Error sending the email:', error); + return res(boom.badImplementation('Error', error)); + }); + } + else { + console.log('save user temp.'); + //save user temp. to change username + userCtrl.create(user, true) + .then((result) => { + if (result[0] !== undefined && result[0] !== null) { + //Error + console.log('ajv error', result, co.parseAjvValidationErrors(result)); + return res(boom.badData('migration failed because data is wrong: ', co.parseAjvValidationErrors(result))); + } + + if (result.insertedCount === 1) { + //success + user._id = result.insertedId; + + return res() + .redirect(PLATFORM_MIGRATE_URL + '?data=' + encodeURIComponent(JSON.stringify({ + hash: result.insertedId, + username: user.username, + email: user.email + }))) + .temporary(true); + } + + res(boom.badImplementation()); + }); + } + }); +} + +function signInMigratedUser(req, res, userFromOtherInstance, userFromThisInstance) { + console.log('signInMigratedUser()'); + + //TODO update userFromThisInstance? + return res() + .redirect(PLATFORM_MIGRATE_URL + '?data=' + encodeURIComponent(JSON.stringify({ + userid: userFromThisInstance._id, + username: userFromThisInstance.username, + jwt: jwt.createToken(userFromThisInstance) + }))) + .temporary(true); +} + function isProviderSupported(provider) { return PROVIDERS.indexOf(provider) !== -1; } diff --git a/application/database/user.js b/application/database/user.js index 2e98c11..a133442 100644 --- a/application/database/user.js +++ b/application/database/user.js @@ -2,7 +2,8 @@ const helper = require('./helper'), userModel = require('../models/user.js'), - collectionName = 'users'; + collectionName = 'users', + collectionNameTemp = 'temp_users'; // hardcoded (static) users // the attributes included are only the public user attributes needed @@ -18,28 +19,49 @@ const staticUsers = [ ]; let self = module.exports = { - create: (user) => { - return helper.connectToDatabase() - .then((dbconn) => helper.getNextIncrementationValueForCollection(dbconn, collectionName)) - .then((newId) => { - // console.log('newId', newId); - return helper.connectToDatabase() //db connection have to be accessed again in order to work with more than one collection - .then((db2) => db2.collection(collectionName)) - .then((collection) => { - let isValid = false; - user._id = newId; - try { - isValid = userModel(user); - if (!isValid) { - return userModel.errors; - } - return collection.insertOne(user); - } catch (e) { - console.log('user validation failed', e); - throw e; + create: (user, temp = false) => { + let name = (temp) ? collectionNameTemp : collectionName; + + if (temp) { + return helper.connectToDatabase() //db connection have to be accessed again in order to work with more than one collection + .then((db2) => db2.collection(name)) + .then((collection) => { + let isValid = false; + try { + isValid = userModel(user); + if (!isValid) { + return userModel.errors; } - }); //id is created and concatinated automatically - }); + return collection.insertOne(user); + } catch (e) { + console.log('user validation failed', e); + throw e; + } + }); //id is created and concatinated automatically + } + else { + return helper.connectToDatabase() + .then((dbconn) => helper.getNextIncrementationValueForCollection(dbconn, name)) + .then((newId) => { + // console.log('newId', newId); + return helper.connectToDatabase() //db connection have to be accessed again in order to work with more than one collection + .then((db2) => db2.collection(name)) + .then((collection) => { + let isValid = false; + user._id = newId; + try { + isValid = userModel(user); + if (!isValid) { + return userModel.errors; + } + return collection.insertOne(user); + } catch (e) { + console.log('user validation failed', e); + throw e; + } + }); //id is created and concatinated automatically + }); + } }, read: (userid) => { @@ -70,17 +92,19 @@ let self = module.exports = { }); }, - delete: (userid) => { + delete: (userid, temp = false) => { + let name = (temp) ? collectionNameTemp : collectionName; return helper.connectToDatabase() - .then((dbconn) => dbconn.collection(collectionName)) + .then((dbconn) => dbconn.collection(name)) .then((collection) => collection.remove({ _id: userid })); }, - find: (query) => { + find: (query, temp = false) => { + let name = (temp) ? collectionNameTemp : collectionName; return helper.connectToDatabase() - .then((dbconn) => dbconn.collection(collectionName)) + .then((dbconn) => dbconn.collection(name)) .then((collection) => collection.find(query)); }, diff --git a/application/models/user.js b/application/models/user.js index b442970..bc7be0b 100644 --- a/application/models/user.js +++ b/application/models/user.js @@ -138,6 +138,20 @@ const user = { }, lastReviewDoneBy: { type: 'integer' + }, + migratedFrom: { + type: 'object', + properties: { + instance: { + type: 'string' + }, + userid: { + type: 'integer' + }, + url: { + type: 'string' + } + } } }, required: ['email', 'username', 'frontendLanguage'] diff --git a/application/routes.js b/application/routes.js index 6385aa8..7ebe3c3 100644 --- a/application/routes.js +++ b/application/routes.js @@ -575,6 +575,77 @@ module.exports = function (server) { } }); + server.route({ + method: 'GET', + path: '/social/provider/slidewiki', + handler: handlers_social.slidewiki, + config: { + validate: { + query: { + jwt: Joi.string().required().description('JWT header provided by /login'), + instance: Joi.string().required(), + userid: Joi.string() + } + }, + tags: ['api'], + description: 'Uses JWT and gets userdata from the given instance to sign up or sign in this user', + plugins: { + 'hapi-swagger': { + responses: { + ' 200 ': { + 'description': 'Successful', + }, + ' 404 ': { + 'description': 'Instance or userid unknown.' + } + }, + payloadType: 'form' + }, + yar: { + skip: true + } + } + } + }); + + server.route({ + method: 'POST', + path: '/social/finalize/{hash}', + handler: handlers_social.finalizeUser, + config: { + validate: { + params: { + hash: Joi.string() + }, + payload: Joi.object().keys({ + username: Joi.string().regex(USERNAME_REGEX), + email: Joi.string().email() + }).requiredKeys('email', 'username'), + }, + tags: ['api'], + description: 'Finalizes the migration of a user.', + plugins: { + 'hapi-swagger': { + responses: { + ' 200 ': { + 'description': 'Successful', + }, + ' 404 ': { + 'description': 'User not prepared for migration.' + }, + ' 409 ': { + 'description': 'Username or email is already in use.' + }, + }, + payloadType: 'form' + }, + yar: { + skip: true + } + } + } + }); + server.route({ method: 'PUT', path: '/social/provider/{provider}', diff --git a/application/server.js b/application/server.js index feddd86..f9884e2 100644 --- a/application/server.js +++ b/application/server.js @@ -15,7 +15,7 @@ const hapi = require('hapi'), //Initiate the webserver with standard or given port const server = new hapi.Server({ connections: {routes: {validate: { options: {convert : false}}}}}); -let port = (!co.isEmpty(process.env.APPLICATION_PORT)) ? process.env.APPLICATION_PORT : 3000; +let port = 1500;//(!co.isEmpty(process.env.APPLICATION_PORT)) ? process.env.APPLICATION_PORT : 3000; server.connection({ port: port });