From cfeb388943ad76060ccfd0aeceefa6afb43dc9c4 Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Feb 2019 09:28:00 -0800 Subject: [PATCH] Signup + Route Changes + Error Handling --- .eslintrc | 8 ++- src/config/app.json | 4 +- src/config/errors.js | 1 + src/config/router.js | 5 +- src/config/twilio.js | 18 ++++-- src/ups/auth/dao.js | 53 +++++++++++++-- src/ups/auth/handlers.js | 40 +++++++----- src/ups/profiles/dao.js | 122 ++++++++++++++++++----------------- src/ups/profiles/handlers.js | 68 +++++-------------- src/ups/utils/utils.js | 42 ++++++------ 10 files changed, 197 insertions(+), 164 deletions(-) diff --git a/.eslintrc b/.eslintrc index a9ef1a6..4ef2a4f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,10 @@ "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 2017, - "sourceType": "module" + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } }, "rules": { "indent": [ @@ -24,6 +27,7 @@ "semi": [ "error", "always" - ] + ], + "no-console": "off" } } \ No newline at end of file diff --git a/src/config/app.json b/src/config/app.json index d2e6cd4..cd78e26 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -65,6 +65,8 @@ "verificationTTL": 300000, "verificationTTLClear": 1800000 }, - "defaultProfileName": "Business Profile", + "baseURL": "https://tagfer.com", + "referralTokens": 50, + "defaultProfileName": "Public Profile", "suggestedUsersCount": 20 } \ No newline at end of file diff --git a/src/config/errors.js b/src/config/errors.js index 94bc884..55fdbbf 100644 --- a/src/config/errors.js +++ b/src/config/errors.js @@ -19,6 +19,7 @@ module.exports = { APP_NETWORK_TIMEOUT: 'app/network-timeout', APP_UNABLE_TO_PARSE_RESPONSE: 'app/unable-to-parse-response', APP_FIREBASE_DATABASE_ERROR: 'app/firebase-database-error', + APP_FIREBASE_STORAGE_ERROR: 'app/firebase-storage-error', //Request Errors MISSING_BODY_ATTRIBUTES: 'request/missing-body-attributes', //Profile Errors diff --git a/src/config/router.js b/src/config/router.js index c692f31..ac1d5e8 100644 --- a/src/config/router.js +++ b/src/config/router.js @@ -22,9 +22,8 @@ function router(app) { app.put('/auth/signup', AuthHandlers.signup); // Profile Endpoints - app.post('/profiles/me/:profileNumber', ProfileHandlers.updateUserProfile); - app.get('/profiles/me/:profileNumber', ProfileHandlers.getUserProfile); - app.put('/profiles/uploadImage/:profileNumber', ProfileHandlers.updateUserProfileImage); + app.post('/profiles/me/:profileN', ProfileHandlers.updateUserProfile); + app.get('/profiles/me/:profileN', ProfileHandlers.getUserProfile); app.get('/profiles/suggest', ProfileHandlers.suggestNProfiles); // Notes Endpoints diff --git a/src/config/twilio.js b/src/config/twilio.js index 7095599..138dd98 100644 --- a/src/config/twilio.js +++ b/src/config/twilio.js @@ -5,26 +5,30 @@ const appConfig = require('./app.json'); const authToken = appConfig.keys.twilio.authToken; const accountSid = appConfig.keys.twilio.accountSid; const tagferPhone = appConfig.keys.twilio.phoneNumber; -const client = new Twilio(accountSid, authToken); +var client; -/** - * - */ -function sendSMSTo(clientPhone, message, callback) { +function sendSMSTo(clientPhone, message, callback = () => {}) { const sms = { from: tagferPhone, body: message, to: clientPhone }; - client.messages.create(sms).then(() => { + getClient().messages.create(sms).then(() => { callback({result: true}); }).catch(error => { - //TODO: add twilio error handler callback({error: error.message}); }).done(); } +function getClient() { + if (!client) { + client = new Twilio(accountSid, authToken); + } + + return client; +} + module.exports = { sendSMSTo }; \ No newline at end of file diff --git a/src/ups/auth/dao.js b/src/ups/auth/dao.js index e790551..a42b0b1 100644 --- a/src/ups/auth/dao.js +++ b/src/ups/auth/dao.js @@ -1,11 +1,12 @@ const firebase = require('firebase'); const admin = require('firebase-admin'); const uuid = require('uuid/v4'); -const _ = require('lodash'); +const utils = require('../utils/utils'); const errors = require('../../config/errors'); const loki = require('../../config/loki'); const twilio = require('../../config/twilio'); +const appConfig = require('../../config/app.json'); /** * Checks if the email exists in our auth system @@ -30,6 +31,7 @@ function doesPhoneExist(phoneNumber) { /** * Sign in user using firebase client api + * * @param {String} email * @param {String} password */ @@ -39,6 +41,7 @@ function signinWithEmail(email, password) { /** * Sign in with tagferId using the admin sdk to get email and then the firebase client to sign in + * * @param {String} tagferId * @param {String} password */ @@ -48,6 +51,7 @@ function signinWithTagferId(tagferId, password) { /** * Creates a new user in firebase auth + * * @param {Object} user * @param {Function} callback */ @@ -56,9 +60,25 @@ function createNewUser(user) { uid: user.tagferId.toLowerCase(), email: user.email, password: user.password, - phoneNumber: user.phoneNumber, - displayName: _.startCase(user.fullName) - }); + phoneNumber: user.phoneNumber + }).catch(error => { throw error.code; }); +} + +/** + * Adds the user signup data to firebase database + */ +function signup(tagferId, phoneNumber, profile, requests = []) { + const ref = admin.database().ref(); + const updates = {}; + updates[`profiles/${tagferId}/profile1`] = profile; + updates[`mapper/phoneNumbers/${phoneNumber}`] = tagferId; + + for (let i = 0; i < requests.length; i++) { + updates[`requests/${tagferId}/sent/${requests[i]}`] = 1; + updates[`requests/${requests[i]}/received/${tagferId}`] = 1; + } + + return ref.update(updates).catch(utils.dbErrorHandler); } /** @@ -66,6 +86,7 @@ function createNewUser(user) { * 1. Contacts that are in the tagfer network. * 2. Contacts that are out of the network. * 3. Contacts that failed to be processed. + * * @param {Array} phoneNumbers * @returns {Object} groups of inNetwork, outNetwork, failed */ @@ -117,6 +138,28 @@ function sendVerificationCode(phoneNumber, callback) { twilio.sendSMSTo(phoneNumber, message, callback); } +/** + * Sends a mass text invite for all phone numbers supplied. Calls are subject to be throttled, since Twillio has a limit + * of 1 MPS, with a queue of max size of 1MPS * 14,400. + * + * @param {Array} phoneNumbers + * @param {String} fullName + * @param {String} tagferId + */ +async function sendMassTextInvites(phoneNumbers = [], fullName, tagferId) { + const promises = []; + const referralMessage = `${fullName} gave you ${appConfig.referralTokens} Tagfer Tokens! To claim use his referral link: ${appConfig.baseURL}/${tagferId}`; + for (let i = 0; i < phoneNumbers.length; i++) { + promises[i] = twilio.sendSMSTo(phoneNumbers[i], referralMessage); + } + + try { + await Promise.all(promises); + } catch(error) { + console.log(error); + } +} + /** * Creates a new session for security of users and to maintain states of the requests. * @returns Session identifier to manage state of the user @@ -215,8 +258,10 @@ module.exports = { doesEmailExist, doesTagferIdExist, doesPhoneExist, + sendMassTextInvites, sendVerificationCode, isVerificationCodeCorrect, + signup, signinWithTagferId, signinWithEmail, createNewUser, diff --git a/src/ups/auth/handlers.js b/src/ups/auth/handlers.js index e71b2c6..2da211a 100644 --- a/src/ups/auth/handlers.js +++ b/src/ups/auth/handlers.js @@ -4,8 +4,6 @@ const authDao = require('./dao'); const profileDao = require('./../profiles/dao'); const utils = require('../utils/utils'); const errors = require('../../config/errors'); -const appConfig = require('../../config/app.json'); -const http = require('../../config/http'); const twitter = require('../socials/twitter'); // Handlers @@ -87,6 +85,7 @@ function verifyPhoneCode(req, res) { /** * Endpoint: auth/signin * Signs the user in using firebase auth + * * @param {Object} req {email: String, password: String} | {tagferId: String, password: String} * @param {Object} res {sessionId: String} | {error: String} */ @@ -107,9 +106,9 @@ function signin(req, res) { promise.then( ({ user })=> { const sessionId = authDao.createNewSessionId(user.uid); - res.json({sessionId}); + res.json({ sessionId }); }).catch(error => { - res.json( {error: error.code} ); + res.json({ error: error.code }); }); } @@ -133,28 +132,35 @@ function signout(req, res) { /** * Endpoint: auth/signup * Signs up the user by creating a new user in firebase admin. + * * @param {Object} req { tagferId: String, email: String, password: String, phoneNumber: String, fullName: String } * @param {Object} res { sessionId: String } | { error: String } */ async function signup(req, res) { - const {user, profile} = req.body; - const userVerifier = () => user.tagferId && user.email && user.password && user.phoneNumber && user.fullName; - const profileVerifier = () => profile.company && profile.company.profession && profile.company.name && profile.company.address && profile.company.number; - - if (!utils.isAppSecretValid(req,res) || !utils.isBodyValid(userVerifier, res) || !utils.isBodyValid(profileVerifier, res)) { + const { user, profile, invites } = req.body; + if (!utils.isAppSecretValid(req,res)) { return; } - profile.name = appConfig.defaultProfileName; - profile.email = user.email; - try { - await authDao.createNewUser(user); - await profileDao.createInitialProfiles(profile, user.tagferId); - const sessionId = authDao.createNewSessionId(user.tagferId); - res.status(http.CREATED).json({sessionId}); + // ADD USER TO FIREBASE AUTH + const tagferId = (await authDao.createNewUser(user)).uid; + + // CREATE PROFILE OBJECT + const profileObject = await profileDao.createInitialProfileObject(profile, tagferId); + + // ADD PROFILE, MAPPER, REQUESTS TO DB + await authDao.signup(tagferId, user.phoneNumber, profileObject, invites.requests); + + // CREATE NEW SESSION ID + const sessionId = authDao.createNewSessionId(tagferId); + res.json({ sessionId }); + + // INVITES SENT IN THE BACKGROUND + authDao.sendMassTextInvites(invites.phoneNumbers, profile.fullName, tagferId); } catch (error) { - res.status(http.BAD_REQUEST).json({error:error.code}); + console.log(error); + res.json({ error }); } } diff --git a/src/ups/profiles/dao.js b/src/ups/profiles/dao.js index f6d6fc4..3d07288 100644 --- a/src/ups/profiles/dao.js +++ b/src/ups/profiles/dao.js @@ -2,56 +2,32 @@ const database = require('firebase-admin').database(); const utils = require('../utils/utils'); const appConfig = require('../../config/app.json'); -/** - * Blind initializing function for a new user's default profile - * @param {object} profileObj - * @param {string} tagferId - */ -function createInitialProfiles(profileObj, tagferId) { - //persist profile data to firebase - return database.ref(`/profiles/${tagferId}/profile1`).set(profileObj); -} - /** * Updates a user profile + * * @param {object} profileObj JSON object containing all data for a profile captured from the frontend * @param {number} profileNumber Number used to identify which profile a user wants to update/add to if the profile slot is empty * @param {string} tagferId tagferId obtained by extracting from authorization header - * @returns {Boolean} Boolean result of whether the */ -function updateProfile(profileObj, profileNumber, tagferId) { - //persist profile data to firebase - return database.ref(`/profiles/${tagferId}/profile${profileNumber}`).set(profileObj); +async function updateProfile(profileObj, profileN, tagferId) { + if (profileObj.photoBytes) { + profileObj.photoURL = await _uploadProfileImageToBucket(profileObj.photoBytes, profileN, tagferId).catch(utils.storageErrorHandler); + profileObj.photoBytes = null; + } + + return database.ref(`/profiles/${tagferId}/profile${profileN}`).update(profileObj).catch(utils.dbErrorHandler); } /** * Gets a user profile + * * @param {Number} profileNumber profile number that identifies which profile information to get for a user * @param {string} tagferId SessionId obtained by extracting from authorization header - * @returns {Object} Profile object containg information for a specific user's profile */ -function getProfile(profileNumber, tagferId) { - return database.ref(`/profiles/${tagferId}/profile${profileNumber}`).once('value').then(function (snapshot) { - return (snapshot.exists() ? snapshot.val() : {}); - }); -} - -/** - * Updates a user's profile image - * @param {object} profileImageData JSON object containing all image data for a profile captured from the frontend - * @param {number} profileNumber Number used to identify which profile a user wants to update/add image to - * @param {string} tagferId tagferId obtained by extracting from authorization header - * @returns {object} Object containing a Promise that contains the result of uploading profile image, and the image url | {promise, imageURL} - */ - -async function updateProfileImage(profileImageData, profileNumber, tagferId) { - //persist profile image data to firebase storage - try { - const downloadURL = await utils.uploadImage(profileImageData, `${tagferId}-profile${profileNumber}`, appConfig.buckets.profile); - return { promise: database.ref(`/profiles/${tagferId}/profile${profileNumber}/photoURL`).set(downloadURL), imageURL: downloadURL }; - } catch (error) { - throw error; - } +function getProfile(profileN, tagferId) { + return database.ref(`/profiles/${tagferId}/profile${profileN}`).once('value') + .then(snapshot => (snapshot.exists() ? snapshot.val() : {})) + .catch(utils.dbErrorHandler); } var lastRetrievedProfile = undefined; @@ -63,42 +39,70 @@ var lastRetrievedProfile = undefined; * * @param {Number} N number of profiles to retrieve */ -function suggestNProfiles(N) { - let promise = null; +async function suggestNProfiles(N) { + let data = null; if (lastRetrievedProfile) { - promise = database.ref('profiles').orderByKey().startAt(lastRetrievedProfile).limitToFirst(N).once('value'); + data = await database.ref('profiles').orderByKey().startAt(lastRetrievedProfile).limitToFirst(N).once('value').catch(utils.dbErrorHandler); } else { - promise = database.ref('profiles').limitToFirst(N).once('value'); + data = await database.ref('profiles').limitToFirst(N).once('value').catch(utils.dbErrorHandler); } - - return promise.then(data => { - const list = new Array(data.numChildren()); - let index = 0; - - data.forEach(profile => { list[index++] = _toLiteProfile(profile.val().profile1, profile.key); }); - if (index > 0) { - lastRetrievedProfile = list[index-1].tagferId; - } + const list = new Array(data.numChildren()); + let index = 0; - if (data.hasChildren && N !== data.numChildren()) { - lastRetrievedProfile = undefined; - return suggestNProfiles(N - index).then(list2 => list2.concat(list)); + data.forEach(profile => { list[index++] = _toLiteProfile(profile.val().profile1, profile.key); }); + if (index > 0) { + lastRetrievedProfile = list[index-1].tagferId; + } + + if (data.hasChildren && N !== data.numChildren()) { + lastRetrievedProfile = undefined; + return suggestNProfiles(N - index).then(list2 => list2.concat(list)); + } + + return list; +} + +/** + * Creates the initial profile object by formatting the object received from the app and adding the photoURL + * + * @param {Realm Profile} profile + * @param {String} tagferId + */ +async function createInitialProfileObject(profile, tagferId) { + const { fullName, jobTitle, companyName, companyEmail, companyPhoneNumber, photoBytes } = profile; + const photoURL = profile.photoBytes ? await _uploadProfileImageToBucket(photoBytes, 1, tagferId) : null; + + return { + profileName: appConfig.defaultProfileName, + fullName, + photoURL, + experience: { + jobTitle, + companyName + }, + emails: { + company: companyEmail + }, + phoneNumbers: { + company: companyPhoneNumber } - - return list; - }); + }; } function _toLiteProfile(profile, tagferId) { - const { fullName, jobTitle, companyName, photoURL } = profile; + const { fullName, experience, photoURL } = profile; + const { jobTitle, companyName } = experience; return { tagferId, fullName, jobTitle, companyName, photoURL }; } +function _uploadProfileImageToBucket(image, profileN, tagferId) { + return utils.uploadImage(image, `${tagferId}-profile${profileN}.jpeg`, appConfig.buckets.profile); +} + module.exports = { + createInitialProfileObject, updateProfile, - createInitialProfiles, - updateProfileImage, getProfile, suggestNProfiles }; diff --git a/src/ups/profiles/handlers.js b/src/ups/profiles/handlers.js index 0be1497..4784cca 100644 --- a/src/ups/profiles/handlers.js +++ b/src/ups/profiles/handlers.js @@ -1,76 +1,40 @@ const profileDao = require('./dao'); const authDao = require('../auth/dao'); const utils = require('../utils/utils'); -const errors = require('../../config/errors'); -const http = require('../../config/http'); const appConfig = require('../../config/app.json'); // Handlers /** * Endpoints: POST profiles/ * Updates a profile for a user based on session stored tagferId + * * @param {Object} req * @param {Object} res {result: Boolean} | {error: String} */ async function updateUserProfile(req, res) { const profileObj = req.body; - const profileNumber = req.params.profileNumber; - - if (!utils.isProfileNumberValid(profileNumber, res)) { - return; - } - const sessionId = utils.getSessionIdFromAuthHeader(req, res); - try { - const tagferId = authDao.getSession(sessionId).tagferId; - profileDao.updateProfile(profileObj, profileNumber, tagferId).then(() => { - res.status(http.CREATED).json({}); - }).catch(() => { - res.status(http.INTERNAL_SERVER_ERROR).json({ error: errors.APP_FIREBASE_DATABASE_ERROR }); - }); - } catch (error) { - res.status(http.UNAUTHORIZED).json({ error }); - } -} - -/** - * Endpoints: PUT profiles/uploadImage/:profileNumber - * Updates user's profile with a profile image based on profile number given. - * This endpoint is called seperately in order to add an image to a profile. - * @param {Object} req {profileNumber} - * @param {Object} res - */ -async function updateUserProfileImage(req, res) { - const profileImageObj = req.body; + const profileNumber = req.params.profileN; const sessionId = utils.getSessionIdFromAuthHeader(req, res); try { const tagferId = authDao.getSession(sessionId).tagferId; - const result = await profileDao.updateProfileImage(profileImageObj, req.params.profileNumber, tagferId); - result.promise.then(() => { - res.status(http.OK).json({ imageURL: result.imageURL }); - }).catch((error) => { - res.status(http.INTERNAL_SERVER_ERROR).json({ error }); - }); + await profileDao.updateProfile(profileObj, profileNumber, tagferId); + res.json({}); } catch (error) { - res.status(http.BAD_REQUEST).json({ error }); + res.json({ error }); } } async function getUserProfile(req, res) { - const profileNumber = req.params.profileNumber; - if (!utils.isProfileNumberValid(profileNumber)) { - return; - } + const profileNumber = req.params.profileN; const sessionId = utils.getSessionIdFromAuthHeader(req, res); + try { const tagferId = authDao.getSession(sessionId).tagferId; - profileDao.getProfile(profileNumber, tagferId).then((profile) => { - res.status(http.OK).json({ profile }); - }).catch(error => { - res.status(http.INTERNAL_SERVER_ERROR).json({ error: error.code }); - }); + const profile = await profileDao.getProfile(profileNumber, tagferId); + res.json({ ...profile }); } catch (error) { - res.status(http.UNAUTHORIZED).json({ error }); + res.json({ error }); } } @@ -81,19 +45,21 @@ async function getUserProfile(req, res) { * * LiteProfileObject = { tagferId, fullName, photoURL, jobTitle, companyName } */ -function suggestNProfiles(req, res) { +async function suggestNProfiles(req, res) { if (!utils.isAppSecretValid(req,res)) { return; } - profileDao.suggestNProfiles(appConfig.suggestedUsersCount) - .then(profiles => res.json({ profiles })) - .catch(() => res.json({ error: errors.APP_FIREBASE_DATABASE_ERROR })); + try { + const profiles = await profileDao.suggestNProfiles(appConfig.suggestedUsersCount); + res.json({ profiles }); + } catch(error) { + res.json({ error }); + } } module.exports = { updateUserProfile, - updateUserProfileImage, getUserProfile, suggestNProfiles }; \ No newline at end of file diff --git a/src/ups/utils/utils.js b/src/ups/utils/utils.js index eae19d1..dff4129 100644 --- a/src/ups/utils/utils.js +++ b/src/ups/utils/utils.js @@ -1,9 +1,10 @@ -const OAuth = require('oauth-1.0a'); +const OAuth = require('oauth-1.0a'); +const admin = require('firebase-admin'); const crypto = require('crypto'); + const appConfig = require('../../config/app.json'); const http = require('../../config/http'); const errors = require('../../config/errors'); -const firebase = require('firebase-admin'); /** * Verifies if the request is valid by checking if the request has the right app secret. @@ -55,8 +56,8 @@ function isProfileNumberValid(profileNumber, response) { } /** - * TODO: move this so other files in the socials dir can use it * Creates the OAuth header to be passed into a request. + * * @param {Object} request { method: String, url: String, data: Object } * @param {Object} app { consumer: Object, token: Object }, each object is of the following { key: String, secret: String } * @returns { Authorization: String } @@ -71,37 +72,38 @@ function createOAuthHeader(request, app) { return oauth.toHeader(oauth.authorize(request, app.token)); } -/** - * - * @param {*} image as raw bytes - * @param {*} path to saving image in firebase - * @param {*} bucketName bucketName - * - * @returns {*} imageURL - */ async function uploadImage(imageData, path, bucketName) { try { - const bucket = firebase.storage().bucket(`gs://${bucketName}`); + const bucket = admin.storage().bucket(`gs://${bucketName}`); - const file = bucket.file(`${path}.${appConfig.imageFormat[imageData.metaData.contentType]}`); - const imageBuffer = Buffer.from(imageData.base64Data, 'base64'); - - await file.save(imageBuffer); + const file = bucket.file(`${path}`); + await file.save(Buffer.from(imageData, 'base64')); const fileMetaData = await file.getMetadata(); - return fileMetaData[0].mediaLink; - } catch (error) { - throw error; + console.log(error); + throw errors.APP_FIREBASE_STORAGE_ERROR; } } +function dbErrorHandler(error) { + console.log(error); + throw errors.APP_FIREBASE_DATABASE_ERROR; +} + +function storageErrorHandler(error) { + console.log(error); + throw errors.APP_FIREBASE_STORAGE_ERROR; +} + module.exports = { isAppSecretValid, isBodyValid, getSessionIdFromAuthHeader, isProfileNumberValid, createOAuthHeader, - uploadImage + uploadImage, + dbErrorHandler, + storageErrorHandler }; \ No newline at end of file