const router = require('express').Router(); const debug = require('debug')('jambonz:api-server'); const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); const {promisePool} = require('../../db'); const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils'); const {validateEmail} = require('../../utils/email-utils'); const uuid = require('uuid').v4; const short = require('short-uuid'); const translator = short(); const jwt = require('jsonwebtoken'); const {setupFreeTrial, createTestCdrs, createTestAlerts} = require('./utils'); const {generateHashedPassword} = require('../../utils/password-utils'); const sysError = require('../error'); const insertUserSql = `INSERT into users (user_sid, account_sid, name, email, provider, provider_userid, email_validated) values (?, ?, ?, ?, ?, ?, 1)`; const insertUserLocalSql = `INSERT into users (user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password) values (?, ?, ?, ?, ?, 0, 'local', ?)`; const insertAccountSql = `INSERT into accounts (account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date) values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`; const queryRootDomainSql = `SELECT root_domain FROM service_providers WHERE service_providers.service_provider_sid = ?`; const insertSignupHistorySql = `INSERT into signup_history (email, name) values (?, ?)`; const addLocalUser = async(logger, user_sid, account_sid, name, email, email_activation_code, passwordHash) => { const [r] = await promisePool.execute(insertUserLocalSql, [ user_sid, account_sid, name, email, email_activation_code, passwordHash ]); debug({r}, 'Result from adding user'); }; const addOauthUser = async(logger, user_sid, account_sid, name, email, provider, provider_userid) => { const [r] = await promisePool.execute(insertUserSql, [ user_sid, account_sid, name, email, provider, provider_userid ]); logger.debug({r}, 'Result from adding user'); }; const validateRequest = async(req, user_sid) => { const payload = req.body || {}; /* check required properties are there */ ['provider', 'service_provider_sid'].forEach((prop) => { if (!payload[prop]) throw new DbErrorBadRequest(`missing ${prop}`); }); /* valid service provider? */ const [rows] = await promisePool.query('SELECT * from service_providers WHERE service_provider_sid = ?', payload.service_provider_sid); if (0 === rows.length) throw new DbErrorUnprocessableRequest('invalid service_provider_sid'); /* valid provider? */ if (!['local', 'github', 'google', 'twitter'].includes(payload.provider)) { throw new DbErrorUnprocessableRequest(`invalid provider: ${payload.provider}`); } /* if local provider then email/password */ if ('local' === payload.provider) { if (!payload.email || !payload.password) throw new DbErrorBadRequest('missing email or password'); /* valid email? */ if (!validateEmail(payload.email)) throw new DbErrorBadRequest('invalid email'); /* valid password? */ if (payload.password.length < 6) throw new DbErrorBadRequest('password must be at least 6 characters'); /* is this email available? */ if (user_sid) { const [rows] = await promisePool.query('SELECT * from users WHERE email = ? AND user_sid <> ?', [payload.email, user_sid]); if (rows.length > 0) throw new DbErrorUnprocessableRequest('account already exists for this email'); } else { const [rows] = await promisePool.query('SELECT * from users WHERE email = ?', payload.email); if (rows.length > 0) throw new DbErrorUnprocessableRequest('account already exists for this email'); } /* verify that we have a code to email them */ if (!payload.email_activation_code) throw new DbErrorBadRequest('email activation code required'); } else { ['oauth2_code', 'oauth2_state', 'oauth2_client_id', 'oauth2_redirect_uri'].forEach((prop) => { if (!payload[prop]) throw new DbErrorBadRequest(`missing ${prop} for provider ${payload.provider}`); }); } }; const parseAuthorizationToken = (logger, req) => { const notfound = {}; const authHeader = req.get('Authorization'); if (!authHeader) return Promise.resolve(notfound); return new Promise((resolve) => { const arr = /^Bearer (.*)$/.exec(req.get('Authorization')); if (!arr) return resolve(notfound); jwt.verify(arr[1], process.env.JWT_SECRET, async(err, decoded) => { if (err) return resolve(notfound); logger.debug({jwt: decoded}, 'register - create new user for existing account'); resolve(decoded); }); }); }; /** * called to create a new user and account * or new user with existing account, in case of "change auth mechanism" */ router.post('/', async(req, res) => { const {logger, writeCdrs, writeAlerts, AlertType} = req.app.locals; const userProfile = {}; try { const {user_sid, account_sid} = await parseAuthorizationToken(logger, req); await validateRequest(req, user_sid); logger.debug({payload: req.body}, 'POST /register'); if (req.body.provider === 'github') { const user = await doGithubAuth(logger, req.body); logger.info({user}, 'retrieved user details from github'); Object.assign(userProfile, { name: user.name, email: user.email, email_validated: user.email_validated, avatar_url: user.avatar_url, provider: 'github', provider_userid: user.login }); } else if (req.body.provider === 'google') { const user = await doGoogleAuth(logger, req.body); logger.info({user}, 'retrieved user details from google'); Object.assign(userProfile, { name: user.name, email: user.email, email_validated: user.verified_email, picture: user.picture, provider: 'google', provider_userid: user.id }); } else if (req.body.provider === 'local') { const user = await doLocalAuth(logger, req.body); logger.info({user}, 'retrieved user details for local provider'); debug({user}, 'retrieved user details for local provider'); Object.assign(userProfile, { name: user.name, email: user.email, provider: 'local', email_activation_code: user.email_activation_code }); } if (req.body.provider !== 'local') { /* when using oauth2, check to see if user already exists */ const [users] = await promisePool.query( 'SELECT * from users WHERE provider = ? AND provider_userid = ?', [userProfile.provider, userProfile.provider_userid]); logger.debug({users}, `Result from retrieving user for ${userProfile.provider}:${userProfile.provider_userid}`); if (1 === users.length) { /* if changing existing account to oauth, no other user with that provider/userid must exist */ if (user_sid) { throw new DbErrorUnprocessableRequest('account already exists for this oauth user/provider'); } Object.assign(userProfile, { user_sid: users[0].user_sid, account_sid: users[0].account_sid, name: users[0].name, email: users[0].email, phone: users[0].phone, pristine: false, email_validated: users[0].email_validated ? true : false, phone_validated: users[0].phone_validated ? true : false, scope: users[0].scope }); const [accounts] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', userProfile.account_sid); if (accounts.length === 0) throw new DbErrorUnprocessableRequest('user exists with no associated account'); Object.assign(userProfile, { is_active: accounts[0].is_active == 1, tutorial_completion: accounts[0].tutorial_completion }); } else { /* you can not register from the sign-in page */ if (req.body.locationBeforeAuth === '/sign-in') { logger.debug('redirecting user to /register so they accept Ts & Cs'); return res.status(404).json({msg: 'registering a new account not allowed from the sign-in page'}); } /* new user, but check if we already have an account with that email */ let sql = 'SELECT * from users WHERE email = ?'; const args = [userProfile.email]; if (user_sid) { sql += ' AND user_sid <> ?'; args.push(user_sid); } logger.debug(`sql is ${sql}`); const [accounts] = await promisePool.execute(sql, args); if (accounts.length > 0) { throw new DbErrorBadRequest(`user already exists with email ${userProfile.email}`); } } } if (userProfile.pristine !== false && !user_sid) { /* add a new user and account */ /* get root domain */ const [sp] = await promisePool.query(queryRootDomainSql, req.body.service_provider_sid); if (0 === sp.length) throw new Error(`service_provider not found for sid ${req.body.service_provider_sid}`); if (!sp[0].root_domain) { throw new Error(`root_domain missing for service provider ${req.body.service_provider_sid}`); } userProfile.root_domain = sp[0].root_domain; userProfile.account_sid = uuid(); userProfile.user_sid = uuid(); const [r1] = await promisePool.execute(insertAccountSql, [ userProfile.account_sid, req.body.service_provider_sid, userProfile.name || userProfile.email, req.body.provider !== 'local', `wh_secret_${translator.generate()}` ]); logger.debug({r1}, 'Result from adding account'); /* add to signup history */ let isReturningUser = false; try { await promisePool.execute(insertSignupHistorySql, [userProfile.email, userProfile.name || userProfile.email]); } catch (err) { if (err.code === 'ER_DUP_ENTRY') { logger.info(`register: user is signing up for a second trial: ${userProfile.email}`); isReturningUser = true; } } /* write sample cdrs and alerts in test environment */ if ('test' === process.env.NODE_ENV) { await createTestCdrs(writeCdrs, userProfile.account_sid); await createTestAlerts(writeAlerts, AlertType, userProfile.account_sid); logger.debug('added test data for cdrs and alerts'); } /* assign starter set of products */ await setupFreeTrial(logger, userProfile.account_sid, isReturningUser); /* add a user for the account */ if (req.body.provider === 'local') { /* hash password */ debug(`salting password: ${req.body.password}`); const passwordHash = await generateHashedPassword(req.body.password); debug(`hashed password: ${passwordHash}`); await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid, userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash); debug('added local user'); } else { await addOauthUser(logger, userProfile.user_sid, userProfile.account_sid, userProfile.name, userProfile.email, userProfile.provider, userProfile.provider_userid); } Object.assign(userProfile, { pristine: true, is_active: req.body.provider !== 'local', email_validated: userProfile.provider !== 'local', phone_validated: false, tutorial_completion: 0, scope: 'read-write' }); } else if (user_sid) { /* add a new user for existing account */ userProfile.user_sid = uuid(); userProfile.account_sid = account_sid; /* changing auth mechanism, add user for existing account */ logger.debug(`register - creating new user for existing account ${account_sid}`); if (req.body.provider === 'local') { /* hash password */ const passwordHash = await generateHashedPassword(req.body.password); await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid, userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash); /* note: we deactivate the old user once the new email is validated */ } else { await addOauthUser(logger, userProfile.user_sid, userProfile.account_sid, userProfile.name, userProfile.email, userProfile.provider, userProfile.provider_userid); /* deactivate the old/replaced user */ const [r] = await promisePool.execute('DELETE FROM users WHERE user_sid = ?', [user_sid]); logger.debug({r}, 'register - removed old user'); } } // generate a json web token for this user const token = jwt.sign({ user_sid: userProfile.user_sid, account_sid: userProfile.account_sid, email: userProfile.email, name: userProfile.name }, process.env.JWT_SECRET, { expiresIn: '1h' }); logger.debug({ user_sid: userProfile.user_sid, account_sid: userProfile.account_sid }, 'generated jwt'); res.json({jwt: token, ...userProfile}); } catch (err) { debug(err, 'Error'); sysError(logger, res, err); } }); module.exports = router;