Signup + Route Changes + Error Handling

This commit is contained in:
Omar 2019-02-05 09:28:00 -08:00
parent cbeb15936e
commit cfeb388943
10 changed files with 197 additions and 164 deletions

View File

@ -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"
}
}

View File

@ -65,6 +65,8 @@
"verificationTTL": 300000,
"verificationTTLClear": 1800000
},
"defaultProfileName": "Business Profile",
"baseURL": "https://tagfer.com",
"referralTokens": 50,
"defaultProfileName": "Public Profile",
"suggestedUsersCount": 20
}

View File

@ -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

View File

@ -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

View File

@ -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
};

View File

@ -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,

View File

@ -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 });
}
}

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};