From 60fcea86778056856a3266bcdcf6c8be8db08897 Mon Sep 17 00:00:00 2001 From: Valentine Zavgorodnev <i@valzav.com> Date: Fri, 7 Oct 2016 14:17:14 -0400 Subject: [PATCH] 304 phone verification and sign up flow refactoring (#433) * add newrelic; rebuild shrinkwrap * update shrinkwrap * A few more react 15.3.1 to 15.3.2 (#369) * Confirm mobile (work in progress) #304 * Update mobile verification with schema change. #304 * Update telesign example config * Progress with signature matching telesig ruby example. #304 * Telesign signature fix (includs missing content type header). * Cleanup warnings * Able to verify a phone number in the UI * Adds telesig verify block check and updating of mobile * Enable mobile recaptcha. SMS verify for FB users only. * Fixed verification code length #304 * add sign up progress bar; refactor submit_email page * integrate phone verification into sign up process * check for existing email * check if phone number was already used * show SignupProgressBar on create_account page; some refactoring * work in progress * move ip and email checks out of oath module * Add confirm_email path in console log to help with testing. #304 * Update email address is confirmation is re-sent. #304 * polishing sign up flow * update wording as per TeleSign's recomendations --- app/assets/stylesheets/app.scss | 7 +- app/components/App.jsx | 15 +- app/components/App.scss | 3 + app/components/all.scss | 1 + app/components/elements/SignupProgressBar.jsx | 17 ++ .../elements/SignupProgressBar.scss | 71 ++++++ app/components/modules/MiniHeader.jsx | 19 ++ app/components/pages/CreateAccount.jsx | 99 ++++---- app/components/pages/CreateAccount.scss | 9 + config/steem-example.json | 4 + scripts/send_waiting_list_invites.js | 2 +- server/api/general.js | 23 ++ server/api/oauth.js | 93 +++----- server/app_render.jsx | 1 + server/sendEmail.js | 4 + server/server.js | 25 +- server/server_pages/enter_confirm_email.jsx | 198 +++++++++------- server/server_pages/enter_confirm_mobile.jsx | 219 ++++++++++++++++++ server/teleSign.js | 138 +++++++++++ webpack/utils/start-koa.js | 12 +- 20 files changed, 734 insertions(+), 226 deletions(-) create mode 100644 app/components/elements/SignupProgressBar.jsx create mode 100644 app/components/elements/SignupProgressBar.scss create mode 100644 app/components/modules/MiniHeader.jsx create mode 100644 server/server_pages/enter_confirm_mobile.jsx create mode 100644 server/teleSign.js diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss index ad7923dba..830f271d1 100644 --- a/app/assets/stylesheets/app.scss +++ b/app/assets/stylesheets/app.scss @@ -37,6 +37,10 @@ a, path, circle { clear: right; } +.clear-left { + clear: left; +} + .clear-both { clear: both; } @@ -104,6 +108,7 @@ label { @media print { .noPrint { - display:none; + display: none; } } + diff --git a/app/components/App.jsx b/app/components/App.jsx index 547b96342..516905b31 100644 --- a/app/components/App.jsx +++ b/app/components/App.jsx @@ -5,7 +5,6 @@ import Header from 'app/components/modules/Header'; import LpFooter from 'app/components/modules/lp/LpFooter'; import user from 'app/redux/User'; import g from 'app/redux/GlobalReducer'; -import { Link } from 'react-router'; import TopRightMenu from 'app/components/modules/TopRightMenu'; import { browserHistory } from 'react-router'; import classNames from 'classnames'; @@ -14,7 +13,8 @@ import CloseButton from 'react-foundation-components/lib/global/close-button'; import Dialogs from 'app/components/modules/Dialogs'; import Modals from 'app/components/modules/Modals'; import Icon from 'app/components/elements/Icon'; -import {key_utils} from 'shared/ecc' +import {key_utils} from 'shared/ecc'; +import MiniHeader from 'app/components/modules/MiniHeader'; class App extends React.Component { constructor(props) { @@ -45,7 +45,6 @@ class App extends React.Component { const p = this.props; const n = nextProps; return p.location !== n.location || - p.loading !== n.loading || p.visitor !== n.visitor || p.flash !== n.flash || this.state !== nextState; } @@ -74,9 +73,10 @@ class App extends React.Component { } render() { - const {location, params, children, loading, flash, showSignUp, new_visitor, + const {location, params, children, flash, showSignUp, new_visitor, depositSteem, signup_bonus} = this.props; const lp = false; //location.pathname === '/'; + const miniHeader = location.pathname === '/create_account'; const params_keys = Object.keys(params); const ip = location.pathname === '/' || (params_keys.length === 2 && params_keys[0] === 'order' && params_keys[1] === 'category'); const alert = this.props.error || flash.get('alert'); @@ -149,7 +149,8 @@ class App extends React.Component { ); } - return <div className={'App' + (lp ? ' LP' : '') + (ip ? ' index-page' : '')} onMouseMove={this.onEntropyEvent}> + return <div className={'App' + (lp ? ' LP' : '') + (ip ? ' index-page' : '') + (miniHeader ? ' mini-header' : '')} + onMouseMove={this.onEntropyEvent}> <SidePanel ref="side_panel" alignment="right"> <TopRightMenu vertical navigate={this.navigate} /> <ul className="vertical menu"> @@ -171,7 +172,7 @@ class App extends React.Component { <li><a href="/tos.html" onClick={this.navigate} rel="nofollow">Terms of Service</a></li> </ul> </SidePanel> - <Header toggleOffCanvasMenu={this.toggleOffCanvasMenu} menuOpen={this.state.open} /> + {miniHeader ? <MiniHeader /> : <Header toggleOffCanvasMenu={this.toggleOffCanvasMenu} menuOpen={this.state.open} />} <div className="App__content"> {welcome_screen} {callout} @@ -189,7 +190,6 @@ App.propTypes = { children: AppPropTypes.Children, location: React.PropTypes.object, signup_bonus: React.PropTypes.string, - loading: React.PropTypes.bool, loginUser: React.PropTypes.func.isRequired, depositSteem: React.PropTypes.func.isRequired, }; @@ -200,7 +200,6 @@ export default connect( error: state.app.get('error'), flash: state.offchain.get('flash'), signup_bonus: state.offchain.get('signup_bonus'), - loading: state.app.get('loading'), new_visitor: !state.user.get('current') && !state.offchain.get('user') && !state.offchain.get('account') && diff --git a/app/components/App.scss b/app/components/App.scss index ff49db97d..0c2225da9 100644 --- a/app/components/App.scss +++ b/app/components/App.scss @@ -17,7 +17,10 @@ @media screen and (min-width: 64.063em) { margin-top: 3.5rem; } +} +.mini-header .App__content { + margin-top: 0; } .welcomeWrapper { diff --git a/app/components/all.scss b/app/components/all.scss index 7881cea67..51773df20 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -26,6 +26,7 @@ @import "./elements/Reputation"; @import "./elements/Reblog"; @import "./elements/YoutubePreview"; +@import "./elements/SignupProgressBar"; // modules @import "./modules/Header"; diff --git a/app/components/elements/SignupProgressBar.jsx b/app/components/elements/SignupProgressBar.jsx new file mode 100644 index 000000000..0927728ae --- /dev/null +++ b/app/components/elements/SignupProgressBar.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export default function SignupProgressBar({steps, current}) { + const lis = steps.map((s, i) => { + const cn = i + 1 < current ? 'done' : (i + 1 == current ? 'current' : ''); + return <li className={cn} key={i + 1}>{s}</li> + }); + return <div className="SignupProgressBar__container expanded row"> + <div className="column"> + <div className="SignupProgressBar"> + <ul> + {lis} + </ul> + </div> + </div> + </div>; +} diff --git a/app/components/elements/SignupProgressBar.scss b/app/components/elements/SignupProgressBar.scss new file mode 100644 index 000000000..a744e8e19 --- /dev/null +++ b/app/components/elements/SignupProgressBar.scss @@ -0,0 +1,71 @@ +.SignupProgressBar__container { + background-color: transparent; + padding-bottom: 0.5rem; + border-bottom: 1px solid $light-gray; +} + +.SignupProgressBar { + width: 100%; + margin: 0 auto 50px 0; + + > ul { + counter-reset: step; + } + > ul > li { + list-style-type: none; + width: 25%; + float: left; + font-size: 12px; + position: relative; + text-align: center; + text-transform: uppercase; + color: #7d7d7d; + } + > ul > li:before { + width: 30px; + height: 30px; + content: counter(step); + counter-increment: step; + line-height: 26px; + border: 2px solid #7d7d7d; + display: block; + text-align: center; + margin: 0 auto 10px auto; + border-radius: 50%; + background-color: white; + } + > ul > li:after { + width: 100%; + height: 2px; + content: ''; + position: absolute; + background-color: #7d7d7d; + top: 15px; + left: -50%; + z-index: -1; + } + > ul > li:first-child:after { + content: none; + } + > ul > li.done { + color: #1A5099; + } + > ul > li.done:before { + content: "\2713"; + color: #fff; + border-color: #4ba2f2; + background-color: #4ba2f2; + } + > ul > li.done:after { + background-color: #4ba2f2; + } + > ul > li.current { + color: #1A5099; + } + > ul > li.current:before { + border-color: #4ba2f2; + } + > ul > li.current:after { + background-color: #4ba2f2; + } +} diff --git a/app/components/modules/MiniHeader.jsx b/app/components/modules/MiniHeader.jsx new file mode 100644 index 000000000..3cee99e46 --- /dev/null +++ b/app/components/modules/MiniHeader.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Icon from 'app/components/elements/Icon.jsx'; + +export default function MiniHeader() { + return <header className="Header"> + <div className="Header__top header"> + <div className="expanded row"> + <div className="columns"> + <ul className="menu"> + <li className="Header__top-logo"> + <a href="/"><Icon name="steem" size="2x" /></a> + </li> + <li className="Header__top-steemit show-for-medium"><a href="/">steemit<span className="beta">beta</span></a></li> + </ul> + </div> + </div> + </div> + </header>; +} diff --git a/app/components/pages/CreateAccount.jsx b/app/components/pages/CreateAccount.jsx index ab2de37dd..38638bf77 100644 --- a/app/components/pages/CreateAccount.jsx +++ b/app/components/pages/CreateAccount.jsx @@ -1,6 +1,5 @@ /* eslint react/prop-types: 0 */ import React from 'react'; -import { browserHistory } from 'react-router'; import { connect } from 'react-redux'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; import Apis from 'shared/api_client/ApiInstances'; @@ -9,10 +8,8 @@ import user from 'app/redux/User'; import {validate_account_name} from 'app/utils/ChainValidation'; import SignUp from 'app/components/modules/SignUp'; import runTests from 'shared/ecc/test/BrowserTests'; -import g from 'app/redux/GlobalReducer'; import GeneratedPasswordInput from 'app/components/elements/GeneratedPasswordInput'; - -const PASSWORD_MIN_LENGTH = 32; +import SignupProgressBar from 'app/components/elements/SignupProgressBar'; class CreateAccount extends React.Component { @@ -31,7 +28,8 @@ class CreateAccount extends React.Component { name_error: '', server_error: '', loading: false, - cryptographyFailure: false + cryptographyFailure: false, + showRules: false }; this.onSubmit = this.onSubmit.bind(this); this.onNameChange = this.onNameChange.bind(this); @@ -42,7 +40,7 @@ class CreateAccount extends React.Component { const cryptoTestResult = runTests(); if (cryptoTestResult !== undefined) { console.error('CreateAccount - cryptoTestResult: ', cryptoTestResult); - this.setState({cryptographyFailure: true}); + this.setState({cryptographyFailure: true}); // TODO: do not use setState in componentDidMount } } @@ -63,6 +61,7 @@ class CreateAccount extends React.Component { }); } + // createAccount fetch('/api/v1/accounts', { method: 'post', mode: 'no-cors', @@ -142,8 +141,8 @@ class CreateAccount extends React.Component { } const { - name, password_valid, showPasswordString, - name_error, server_error, loading, cryptographyFailure + name, password_valid, //showPasswordString, + name_error, server_error, loading, cryptographyFailure, showRules } = this.state; const {loggedIn, logout, offchainUser, serverBusy} = this.props; @@ -203,45 +202,59 @@ class CreateAccount extends React.Component { </div>; } + const next_step = !server_error ? null : + server_error === 'Mobile is not confirmed' ? <div> + <a href="/enter_mobile">Verify a Mobile</a> + </div> : <div className="callout alert"> + <h5>Couldn't create account. Server returned the following error:</h5> + <p>{server_error}</p> + {server_error === 'Email address is not confirmed' && <a href="/enter_email">Confirm Email</a>} + </div> + return ( - <div className="CreateAccount row"> - <div className="column large-7 small-10"> - <h2>Sign Up</h2> - <div className="CreateAccount__rules"> - <hr /> - <p> - The first rule of Steemit is: Do not lose your password.<br /> - The second rule of Steemit is: Do <strong>not</strong> lose your password.<br /> - The third rule of Steemit is: We cannot recover your password.<br /> - The fourth rule: If you can remember the password, it's not secure.<br /> - The fifth rule: Use only randomly-generated passwords.<br /> - The sixth rule: Do not tell anyone your password.<br /> - The seventh rule: Always back up your password. - </p> - <hr /> - </div> - <form onSubmit={this.onSubmit} autoComplete="off" noValidate method="post"> - <div className={name_error ? 'error' : ''}> - <label>USERNAME - <input type="text" name="name" autoComplete="off" onChange={this.onNameChange} value={name} /> - </label> - <p>{name_error}</p> - </div> - <GeneratedPasswordInput onChange={this.onPasswordChange} disabled={loading} showPasswordString={name.length > 0 && !name_error} /> + <div> + <SignupProgressBar steps={[offchainUser.get('prv') || 'identity', 'email', 'phone', 'steem account']} current={4} /> + <div className="CreateAccount row"> + <div className="column" style={{maxWidth: '36rem', margin: '0 auto'}}> <br /> - {server_error && <div className="callout alert"> - <h5>Couldn't create account. Server returned the following error:</h5> - <p>{server_error}</p> - {server_error === 'Email address is not confirmed' && <a href="/enter_email">Confirm Email</a>} + {showRules ? <div className="CreateAccount__rules"> + <p> + The first rule of Steemit is: Do not lose your password.<br /> + The second rule of Steemit is: Do <strong>not</strong> lose your password.<br /> + The third rule of Steemit is: We cannot recover your password.<br /> + The fourth rule: If you can remember the password, it's not secure.<br /> + The fifth rule: Use only randomly-generated passwords.<br /> + The sixth rule: Do not tell anyone your password.<br /> + The seventh rule: Always back up your password. + </p> + <div className="text-center"> + <a className="CreateAccount__rules-button" href="#" onClick={() => this.setState({showRules: false})}> + <span style={{display: 'inline-block', transform: 'rotate(-90deg)'}}>»</span> + </a> + </div> + <hr /> + </div> : <div className="text-center"> + <a className="CreateAccount__rules-button" href="#" onClick={() => this.setState({showRules: true})}>Steemit Rules »</a> </div>} - <noscript> - <div className="callout alert"> - <p>This form requires javascript to be enabled in your browser</p> + <form onSubmit={this.onSubmit} autoComplete="off" noValidate method="post"> + <div className={name_error ? 'error' : ''}> + <label>ACCOUNT NAME + <input type="text" name="name" autoComplete="off" onChange={this.onNameChange} value={name} /> + </label> + <p>{name_error}</p> </div> - </noscript> - {loading && <LoadingIndicator type="circle" />} - <input disabled={submit_btn_disabled} type="submit" className={submit_btn_class} value="SIGN UP" /> - </form> + <GeneratedPasswordInput onChange={this.onPasswordChange} disabled={loading} showPasswordString={name.length > 0 && !name_error} /> + <br /> + {next_step && <div>{next_step}<br /></div>} + <noscript> + <div className="callout alert"> + <p>This form requires javascript to be enabled in your browser</p> + </div> + </noscript> + {loading && <LoadingIndicator type="circle" />} + <input disabled={submit_btn_disabled} type="submit" className={submit_btn_class} value="Create Account" /> + </form> + </div> </div> </div> ); diff --git a/app/components/pages/CreateAccount.scss b/app/components/pages/CreateAccount.scss index c87e6cf7b..e71164a9b 100644 --- a/app/components/pages/CreateAccount.scss +++ b/app/components/pages/CreateAccount.scss @@ -4,3 +4,12 @@ text-align: center; } } + +.CreateAccount__rules-button { + background-color: #f2f2f2; + padding: 0.2rem 1rem; + border-radius: 10px; + margin: 1rem 0; + font-size: 0.8rem; + color: $dark-gray; +} diff --git a/config/steem-example.json b/config/steem-example.json index b1b9c8f98..8ba3b057c 100644 --- a/config/steem-example.json +++ b/config/steem-example.json @@ -52,6 +52,10 @@ "confirm_email": "some_template_id" } }, + "telesign": { + "customer_id": "", + "rest_api_key": "" + }, "recaptcha": { "site_key": "", "secret_key": "" diff --git a/scripts/send_waiting_list_invites.js b/scripts/send_waiting_list_invites.js index 8e2e5ce74..651cb1709 100644 --- a/scripts/send_waiting_list_invites.js +++ b/scripts/send_waiting_list_invites.js @@ -18,7 +18,7 @@ function inviteUser(u, email, number) { models.User.findAll({ attributes: ['id', 'email'], - where: {waiting_list: true, email: {$ne: null}, id: {$gt: 0}}, + where: {waiting_list: true, email: {$ne: null}, id: {$gt: 0}}, order: 'id', limit: 1000 }).then(users => { diff --git a/server/api/general.js b/server/api/general.js index 8f7b7c4fb..f50dfad56 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -44,6 +44,18 @@ export default function useGeneralApi(app) { return; } + // check if user's ip is associated with any bot + const same_ip_bot = yield models.User.findOne({ + attributes: ['id', 'created_at'], + where: {remote_ip, bot: true} + }); + if (same_ip_bot) { + console.log('-- /accounts same_ip_bot -->', user_id, this.session.uid, remote_ip, user.email); + this.body = JSON.stringify({error: 'We are sorry, we cannot sign you up at this time because your IP address is associated with bots activity. Please contact support@steemit.com for more information.'}); + this.status = 401; + return; + } + const existing_account = yield models.Account.findOne({ attributes: ['id', 'created_at'], where: {user_id, ignored: false}, @@ -67,6 +79,8 @@ export default function useGeneralApi(app) { console.log(`api /accounts: waiting_list user ${this.session.uid} #${user_id}`); throw new Error('You are on the waiting list. We will get back to you at the earliest possible opportunity.'); } + + // check email const eid = yield models.Identity.findOne( {attributes: ['id'], where: {user_id, provider: 'email', verified: true}, order: 'id DESC'} ); @@ -75,6 +89,15 @@ export default function useGeneralApi(app) { throw new Error('Email address is not confirmed'); } + // check phone + const mid = yield models.Identity.findOne( + {attributes: ['id'], where: {user_id, provider: 'phone', verified: true}, order: 'id DESC'} + ); + if (!mid) { + console.log(`api /accounts: not confirmed sms for user ${this.session.uid} #${user_id}`); + throw new Error('Phone number is not confirmed'); + } + yield createAccount({ signingKey: config.registrar.signing_key, fee: config.registrar.fee, diff --git a/server/api/oauth.js b/server/api/oauth.js index e86db2868..df1a2dbbf 100644 --- a/server/api/oauth.js +++ b/server/api/oauth.js @@ -42,13 +42,13 @@ function retrieveFacebookUserData(access_token) { function* handleFacebookCallback() { console.log('-- /handle_facebook_callback -->', this.session.uid, this.query); - let verified_email = false; + let email = null; try { if (this.query['error[error][message]']) { return logErrorAndRedirect(this, 'facebook:1', this.query['error[error][message]']); } const u = yield retrieveFacebookUserData(this.query.access_token); - verified_email = false; // verified_email = !!(u.verified && u.email); + email = u.email; const attrs = { uid: this.session.uid, name: u.name, @@ -75,17 +75,17 @@ function* handleFacebookCallback() { verified: u.verified, provider_user_id: u.id }; - const i_attrs_email = { - provider: 'email', - email: u.email, - verified: verified_email - }; + // const i_attrs_email = { + // provider: 'email', + // email: u.email, + // verified: false + // }; let user = yield findUser({email: u.email, provider_user_id: u.id}); console.log('-- /handle_facebook_callback user id -->', this.session.uid, user ? user.id : 'not found'); let account_recovery_record = null; - const provider = 'facebook'; + const provider = this.session.prv = 'facebook'; if (this.session.arec) { const arec = yield models.AccountRecoveryRequest.findOne({ attributes: ['id', 'created_at', 'account_name', 'owner_key'], @@ -120,77 +120,40 @@ function* handleFacebookCallback() { } return null; } - if (!u.email) { - console.log('-- /handle_facebook_callback no email -->', this.session.uid, u); - this.flash = {alert: 'Facebook login didn\'t provide any email addresses. Please make sure your Facebook account has a primary email address and try again.'}; - this.redirect('/'); - return; - } - - if (!u.verified) { - throw new Error('Not verified Facebook account. Please verify your Facebook account and try again to sign up to Steemit.'); - } - - const same_ip_bot = yield models.User.findOne({ - attributes: ['id', 'created_at'], - where: {remote_ip: attrs.remote_ip, bot: true} - }); - if (same_ip_bot) { - console.log('-- /handle_facebook_callback same_ip_bot -->', this.session.uid, attrs.remote_ip, attrs.email); - this.flash = {alert: 'We are sorry, we cannot sign you up at this time because your IP address is associated with bots activity. Please contact support@steemit.com for more information.'}; - this.redirect('/'); - return; - } - - const email_provider = u.email.match(/([\w\d-]+\.\w+)$/)[1]; - if (!email_provider) throw new Error('Incorrect email format'); - const blocked_email = yield models.List.findOne({ - attributes: ['id'], - where: {kk: 'block-email-provider', value: email_provider} - }); - if (blocked_email) { - console.log('-- /handle_facebook_callback blocked_email -->', this.session.uid, u.email); - this.flash = {alert: 'Not supported email address: ' + u.email + '. Please make sure your you don\'t use any temporary email providers, contact support@steemit.com for more information.'}; - this.redirect('/'); - return; - } + // no longer necessary since there is phone verification now + // if (!u.email) { + // console.log('-- /handle_facebook_callback no email -->', this.session.uid, u); + // this.flash = {alert: 'Facebook login didn\'t provide any email addresses. Please make sure your Facebook account has a primary email address and try again.'}; + // this.redirect('/'); + // return; + // } + // if (!u.verified) { + // throw new Error('Not verified Facebook account. Please verify your Facebook account and try again to sign up to Steemit.'); + // } if (user) { - i_attrs_email.user_id = attrs.id = user.id; + attrs.id = user.id; yield models.User.update(attrs, {where: {id: user.id}}); yield models.Identity.update(i_attrs, {where: {user_id: user.id, provider: 'facebook'}}); - if (verified_email) { - const eid = yield models.Identity.findOne( - {attributes: ['id', 'verified'], where: {user_id: user.id, provider: 'email'}, order: 'id DESC'} - ); - if (eid) { - if (!eid.verified) yield eid.update({email: u.email, verified: true}); - } else { - yield models.Identity.create(i_attrs_email); - } - } console.log('-- fb updated user -->', this.session.uid, user.id, u.name, u.email); } else { user = yield models.User.create(attrs); - i_attrs_email.user_id = i_attrs.user_id = user.id; + i_attrs.user_id = user.id; console.log('-- fb created user -->', user.id, u.name, u.email); const identity = yield models.Identity.create(i_attrs); console.log('-- fb created identity -->', this.session.uid, identity.id); - if (i_attrs_email.email) { - const email_identity = yield models.Identity.create(i_attrs_email); - console.log('-- fb created email identity -->', this.session.uid, email_identity.id); - } + // if (i_attrs_email.email) { + // i_attrs_email.user_id = user.id + // const email_identity = yield models.Identity.create(i_attrs_email); + // console.log('-- fb created email identity -->', this.session.uid, email_identity.id); + // } } this.session.user = user.id; } catch (error) { return logErrorAndRedirect(this, 'facebook:2', error); } this.flash = {success: 'Successfully authenticated with Facebook'}; - if (verified_email) { - this.redirect('/create_account'); - } else { - this.redirect('/enter_email'); - } + this.redirect('/enter_email' + (email ? `?email=${email}` : '')); return null; } @@ -223,7 +186,7 @@ function* handleRedditCallback() { console.log('-- /handle_reddit_callback user id -->', this.session.uid, user ? user.id : 'not found'); let account_recovery_record = null; - const provider = 'reddit'; + const provider = this.session.prv = 'reddit'; if (this.session.arec) { const arec = yield models.AccountRecoveryRequest.findOne({ attributes: ['id', 'created_at', 'account_name', 'owner_key'], @@ -275,7 +238,7 @@ function* handleRedditCallback() { if (user) { if (!waiting_list) attrs.waiting_list = false; yield models.User.update(attrs, {where: {id: user.id}}); - yield models.Identity.update(i_attrs, {where: {user_id: user.id}}); + yield models.Identity.update(i_attrs, {where: {user_id: user.id, provider: 'reddit'}}); console.log('-- reddit updated user -->', this.session.uid, user.id, u.name); } else { attrs.waiting_list = waiting_list; diff --git a/server/app_render.jsx b/server/app_render.jsx index 3d4ff6593..82e382bfb 100644 --- a/server/app_render.jsx +++ b/server/app_render.jsx @@ -49,6 +49,7 @@ async function appRender(ctx) { name: user.name, email: user.email, picture: user.picture_small, + prv: ctx.session.prv, account } } diff --git a/server/sendEmail.js b/server/sendEmail.js index d8e1dada5..349196c8f 100644 --- a/server/sendEmail.js +++ b/server/sendEmail.js @@ -4,6 +4,10 @@ import config from '../config'; const sg = sendgrid(config.sendgrid.key); export default function sendEmail(template, to, params, from = null) { + if (process.env.NODE_ENV !== 'production') { + console.log(`mail: to <${to}>, from <${from}>, template ${template} (not sent due to not production env)`); + return; + } const tmpl_id = config.sendgrid.templates[template]; if (!tmpl_id) throw new Error(`can't find template ${template}`); diff --git a/server/server.js b/server/server.js index fc0a5c947..ccff0b7ae 100644 --- a/server/server.js +++ b/server/server.js @@ -13,6 +13,7 @@ import useOauthLogin from './api/oauth'; import useGeneralApi from './api/general'; import useAccountRecoveryApi from './api/account_recovery'; import useEnterAndConfirmEmailPages from './server_pages/enter_confirm_email'; +import useEnterAndConfirmMobilePages from './server_pages/enter_confirm_mobile'; import isBot from 'koa-isbot'; import session from 'koa-session'; import csrf from 'koa-csrf'; @@ -79,8 +80,22 @@ app.use(mount('/robots.txt', function* () { this.body = "User-agent: *\nAllow: /"; })); +// set user's uid - used to identify users in logs and some other places +app.use(function* (next) { + const last_visit = this.session.last_visit; + this.session.last_visit = (new Date()).getTime() / 1000 | 0; + if (!this.session.uid) { + this.session.uid = Math.random().toString(36).slice(2); + this.session.new_visit = true; + } else { + this.session.new_visit = this.session.last_visit - last_visit > 1800; + } + yield next; +}); + useRedirects(app); useEnterAndConfirmEmailPages(app); +useEnterAndConfirmMobilePages(app); if (env === 'production') { app.use(helmet.contentSecurityPolicy(config.helmet)); @@ -109,16 +124,6 @@ if (env === 'development') { if (env !== 'test') { const appRender = require('./app_render'); app.use(function* () { - this.first_visit = false; - this.last_visit = this.session.last_visit; - this.session.last_visit = (new Date()).getTime() / 1000 | 0; - if (!this.session.uid) { - this.session.uid = Math.random().toString(36).slice(2); - this.first_visit = true; - this.session.new_visit = true; - } else { - this.session.new_visit = this.session.last_visit - this.last_visit > 1800; - } yield appRender(this); // if (app_router.dbStatus.ok) recordWebEvent(this, 'page_load'); const bot = this.state.isBot; diff --git a/server/server_pages/enter_confirm_email.jsx b/server/server_pages/enter_confirm_email.jsx index 61a39bc67..c1ab1e73d 100644 --- a/server/server_pages/enter_confirm_email.jsx +++ b/server/server_pages/enter_confirm_email.jsx @@ -2,37 +2,19 @@ import koa_router from 'koa-router'; import koa_body from 'koa-body'; import request from 'co-request'; import React from 'react'; -import { renderToString } from 'react-dom/server'; +import {renderToString} from 'react-dom/server'; import models from 'db/models'; import {esc, escAttrs} from 'db/models'; import ServerHTML from '../server-html'; -import Icon from 'app/components/elements/Icon.jsx'; import sendEmail from '../sendEmail'; import {checkCSRF} from '../utils'; import config from '../../config'; +import SignupProgressBar from 'app/components/elements/SignupProgressBar'; +import MiniHeader from 'app/components/modules/MiniHeader'; -let assets; -if (process.env.NODE_ENV === 'production') { - assets = Object.assign({}, require('tmp/webpack-stats-prod.json'), {script: ['https://www.google.com/recaptcha/api.js']}); -} else { - assets = Object.assign({}, require('tmp/webpack-stats-dev.json')); - assets.script.push('https://www.google.com/recaptcha/api.js'); -} - -const header = <header className="Header"> - <div className="Header__top header"> - <div className="expanded row"> - <div className="columns"> - <ul className="menu"> - <li className="Header__top-logo"> - <a href="/"><Icon name="steem" size="2x" /></a> - </li> - <li className="Header__top-steemit show-for-medium"><a href="/">steemit<span className="beta">beta</span></a></li> - </ul> - </div> - </div> - </div> -</header>; +const assets_file = process.env.NODE_ENV === 'production' ? 'tmp/webpack-stats-prod.json' : 'tmp/webpack-stats-dev.json'; +const assets = Object.assign({}, require(assets_file), {script: []}); +assets.script.push('https://www.google.com/recaptcha/api.js'); function *confirmEmailHandler() { const confirmation_code = this.params && this.params.code ? this.params.code : this.request.body.code; @@ -45,18 +27,21 @@ function *confirmEmailHandler() { this.body = 'confirmation code not found'; return; } - this.session.user = eid.user_id; + if (eid.verified) { + this.flash = {success: 'Email has already been verified'}; + this.redirect('/enter_mobile'); + return; + } const hours_ago = (Date.now() - eid.updated_at) / 1000.0 / 3600.0; - if (hours_ago > 24.0 * 30) { + if (hours_ago > 24.0 * 10) { this.status = 401; this.body = 'confirmation code not found or expired'; return; } - if (!eid.verified) { - yield eid.update({verified: true}); - yield models.User.update({email: eid.email, waiting_list: false}, {where: {id: eid.user_id}}); - } - this.redirect('/create_account'); + this.session.user = eid.user_id; + yield eid.update({verified: true}); + yield models.User.update({email: eid.email, waiting_list: false}, {where: {id: eid.user_id}}); + this.redirect('/enter_mobile'); } export default function useEnterAndConfirmEmailPages(app) { @@ -67,41 +52,59 @@ export default function useEnterAndConfirmEmailPages(app) { router.get('/enter_email', function *() { console.log('-- /enter_email -->', this.session.uid, this.session.user); const user_id = this.session.user; - if (!user_id) { this.body = 'user not found'; return; } + if (!user_id) { + this.body = 'user not found'; + return; + } const eid = yield models.Identity.findOne( - {attributes: ['email'], where: {user_id, provider: 'email'}, order: 'id DESC'} + {attributes: ['email', 'verified'], where: {user_id, provider: 'email'}, order: 'id DESC'} ); + if (eid && eid.verified) { + this.flash = {success: 'Email has already been verified'}; + this.redirect('/enter_mobile'); + return; + } + console.log('-- this.request.query -->', this.request.query); + let default_email = ''; + if (this.request.query && this.request.query.email) default_email = this.request.query.email; const body = renderToString(<div className="App"> - {header} + <MiniHeader /> + <SignupProgressBar steps={[this.session.prv || 'identity', 'email', 'phone', 'steem account']} current={2} /> <br /> - <div className="row"> - <form className="column small-4" action="/submit_email" method="POST"> - <p> - Please provide your email address to continue the registration process.<br /> - <span className="secondary">This information allows Steemit to assist with Account Recovery in case your account is ever compromised.</span> - </p> - <input type="hidden" name="csrf" value={this.csrf} /> - <label> - Email - <input type="email" name="email" defaultValue={eid ? eid.email : ''} readOnly={eid && eid.email} /> - </label> - {eid && eid.email && <div className="secondary"><i>Email address cannot be changed at this moment, sorry for the inconvenience.</i></div>} - <br /> - <div className="g-recaptcha" data-sitekey={config.recaptcha.site_key}></div> - <br /> - <div className="error">{this.flash.error}</div> - <input type="submit" className="button" value="CONTINUE" /> - </form> + <div className="row" style={{maxWidth: '32rem'}}> + <div className="column"> + <form action="/submit_email" method="POST"> + <h4>Please provide your email address to continue the registration process</h4> + <p className="secondary"> + Email verification helps with preventing spam and allows Steemit to assist with Account Recovery in case your account is ever compromised. + </p> + <input type="hidden" name="csrf" value={this.csrf} /> + <label> + Email + <input type="email" name="email" defaultValue={default_email} /> + </label> + {/*eid && eid.email && + <div className="secondary"><i>Email address cannot be changed at this moment, sorry for the inconvenience.</i></div>*/} + <br /> + <div className="g-recaptcha" data-sitekey={config.recaptcha.site_key}></div> + <br /> + <div className="error">{this.flash.error}</div> + <input type="submit" className="button" value="CONTINUE" /> + </form> + </div> </div> </div>); - const props = { body, title: 'Email Address', assets, meta: [] }; + const props = {body, title: 'Email Address', assets, meta: []}; this.body = '<!DOCTYPE html>' + renderToString(<ServerHTML { ...props } />); }); router.post('/submit_email', koaBody, function *() { if (!checkCSRF(this, this.request.body.csrf)) return; const user_id = this.session.user; - if (!user_id) { this.body = 'user not found'; return; } + if (!user_id) { + this.body = 'user not found'; + return; + } const email = this.request.body.email; if (!email) { this.flash = {error: 'Please provide an email address'}; @@ -109,21 +112,52 @@ export default function useEnterAndConfirmEmailPages(app) { return; } - const recaptcha = this.request.body['g-recaptcha-response']; - const verificationUrl = 'https://www.google.com/recaptcha/api/siteverify?secret=' + config.recaptcha.secret_key + '&response=' + recaptcha + '&remoteip=' + this.req.connection.remoteAddress; - let captcha_failed; - try { - const recaptcha_res = yield request(verificationUrl); - const body = JSON.parse(recaptcha_res.body); - captcha_failed = !body.success; - } catch (e) { - captcha_failed = true; - console.error('-- /submit_email recaptcha request failed -->', verificationUrl, e); + if (process.env.NODE_ENV === 'production') { + const recaptcha = this.request.body['g-recaptcha-response']; + const verificationUrl = 'https://www.google.com/recaptcha/api/siteverify?secret=' + config.recaptcha.secret_key + '&response=' + recaptcha + '&remoteip=' + this.req.connection.remoteAddress; + let captcha_failed; + try { + const recaptcha_res = yield request(verificationUrl); + const body = JSON.parse(recaptcha_res.body); + captcha_failed = !body.success; + } catch (e) { + captcha_failed = true; + console.error('-- /submit_email recaptcha request failed -->', verificationUrl, e); + } + if (captcha_failed) { + console.log('-- /submit_email captcha verification failed -->', user_id, this.session.uid, email, this.req.connection.remoteAddress); + this.flash = {error: 'Failed captcha verification, please try again'}; + this.redirect('/enter_email?email=' + email); + return; + } } - if (captcha_failed) { - console.log('-- /submit_email captcha verification failed -->', user_id, this.session.uid, email, this.req.connection.remoteAddress); - this.flash = {error: 'Failed captcha verification, please try again.'}; - this.redirect('/enter_email'); + + const parsed_email = email.match(/^.+\@.*?([\w\d-]+\.\w+)$/); + if (!parsed_email || parsed_email.length < 2) { + console.log('-- /submit_email not valid email -->', user_id, this.session.uid, email); + this.flash = {error: 'Not valid email address'}; + this.redirect('/enter_email?email=' + email); + return; + } + const email_provider = parsed_email[1]; + const blocked_email = yield models.List.findOne({ + attributes: ['id'], + where: {kk: 'block-email-provider', value: email_provider} + }); + if (blocked_email) { + console.log('-- /submit_email blocked_email -->', this.session.uid, email); + this.flash = {error: 'Not supported email address: ' + email + '. Please make sure your you don\'t use any temporary email providers, contact support@steemit.com for more information.'}; + this.redirect('/enter_email?email=' + email); + return; + } + + const existing_email = yield models.Identity.findOne( + {attributes: ['user_id'], where: {email, provider: 'email', verified: true}, order: 'id'} + ); + if (existing_email && existing_email.user_id != user_id) { + console.log('-- /submit_email existing_email -->', user_id, this.session.uid, email, existing_email.user_id); + this.flash = {error: 'This email has already been taken'}; + this.redirect('/enter_email?email=' + email); return; } @@ -132,7 +166,7 @@ export default function useEnterAndConfirmEmailPages(app) { {attributes: ['id', 'email'], where: {user_id, provider: 'email'}, order: 'id'} ); if (eid) { - yield eid.update({confirmation_code}); + yield eid.update({confirmation_code, email}); } else { eid = yield models.Identity.create({ provider: 'email', @@ -147,32 +181,18 @@ export default function useEnterAndConfirmEmailPages(app) { sendEmail('confirm_email', email, {confirmation_code}); const body = renderToString(<div className="App"> - {header} + <MiniHeader /> + <SignupProgressBar steps={[this.session.prv || 'identity', 'email', 'phone', 'steem account']} current={2} /> <br /> - <div className="row"> + <div className="row" style={{maxWidth: '32rem'}}> <div className="column"> Thank you for providing your email address ({email}).<br /> - To continue please click on the link in the email we've sent you. - </div> - </div> - <br /> - <div className="row"> - <div className="column"> - <a href="/enter_email">Re-send email</a> + To continue please click on the link in the email we've sent you.<br /> + <span className="secondary">Didn't recieve email? <a href={`/enter_email?email=${email}`}>Re-send</a></span> </div> </div> - {/*<div className="row"> - <form className="column small-4" action="/confirm_email" method="POST"> - <label> - Confirmation code - <input type="text" name="code" /> - </label> - <br /> - <input type="submit" className="button" value="CONTINUE" /> - </form> - </div>*/} </div>); - const props = { body, title: 'Email Confirmation', assets, meta: [] }; + const props = {body, title: 'Email Confirmation', assets, meta: []}; this.body = '<!DOCTYPE html>' + renderToString(<ServerHTML { ...props } />); }); diff --git a/server/server_pages/enter_confirm_mobile.jsx b/server/server_pages/enter_confirm_mobile.jsx new file mode 100644 index 000000000..fd23b30b8 --- /dev/null +++ b/server/server_pages/enter_confirm_mobile.jsx @@ -0,0 +1,219 @@ +import koa_router from 'koa-router'; +import koa_body from 'koa-body'; +import request from 'co-request'; +import React from 'react'; +import {renderToString} from 'react-dom/server'; +import models from 'db/models'; +import ServerHTML from 'server/server-html'; +import Icon from 'app/components/elements/Icon.jsx'; +import {verify} from 'server/teleSign'; +import SignupProgressBar from 'app/components/elements/SignupProgressBar'; +import {getRemoteIp, checkCSRF} from 'server/utils'; +import MiniHeader from 'app/components/modules/MiniHeader'; + +const assets_file = process.env.NODE_ENV === 'production' ? 'tmp/webpack-stats-prod.json' : 'tmp/webpack-stats-dev.json'; +const assets = Object.assign({}, require(assets_file), {script: []}); +// assets.script.push('https://www.google.com/recaptcha/api.js'); + +function *confirmMobileHandler() { + const confirmation_code = this.params && this.params.code ? this.params.code : this.request.body.code; + console.log('-- /confirm_mobile -->', this.session.uid, this.session.user, confirmation_code); + const mid = yield models.Identity.findOne( + {attributes: ['id', 'user_id', 'verified', 'updated_at'], where: {user_id: this.session.user, confirmation_code}, order: 'id DESC'} + ); + if (!mid) { + this.status = 401; + this.body = 'Wrong confirmation code'; + return; + } + if (mid.verified) { + this.flash = {success: 'Phone number has already been verified'}; + this.redirect('/create_account'); + return; + } + this.session.user = mid.user_id; + const hours_ago = (Date.now() - mid.updated_at) / 1000.0 / 3600.0; + if (hours_ago > 24.0) { + this.status = 401; + this.body = 'Confirmation code has been expired'; + return; + } + yield mid.update({verified: true}); + this.redirect('/create_account'); +} + +export default function useEnterAndConfirmMobilePages(app) { + const router = koa_router(); + app.use(router.routes()); + const koaBody = koa_body(); + + router.get('/enter_mobile', function *() { + console.log('-- /enter_mobile -->', this.session.uid, this.session.user); + const user_id = this.session.user; + if (!user_id) { this.body = 'user not found'; return; } + const mid = yield models.Identity.findOne( + {attributes: ['phone'], where: {user_id, provider: 'phone'}, order: 'id DESC'} + ); + if (mid && mid.verified) { + this.flash = {success: 'Phone number has already been verified'}; + this.redirect('/create_account'); + return; + } + const body = renderToString(<div className="App"> + <MiniHeader /> + <SignupProgressBar steps={[this.session.prv || 'identity', 'email', 'phone', 'steem account']} current={3} /> + <br /> + <div className="row" style={{maxWidth: '32rem'}}> + <form className="column" action="/submit_mobile" method="POST"> + <h4>Please provide your phone number to continue the registration process</h4> + <div className="secondary">Phone verification helps with preventing spam and allows Steemit to assist with Account Recovery in case your account is ever compromised. + Your phone number will not be used for any other purpose other than phone verification and account recovery.</div> + <br /> + <input type="hidden" name="csrf" value={this.csrf} /> + <label> + Phone number + <input type="tel" name="mobile" defaultValue={mid ? mid.phone : ''} /> + </label> + <div className="secondary">Examples: 1-541-754-3010 | +1-541-754-3010 | +49-89-636-48018</div> + <br /> + <div className="secondary">* fixed line phones cannot receive SMS messages</div> + <div className="secondary">* message and data rates may apply</div> + <br /> + {/*<div className="g-recaptcha" data-sitekey={config.recaptcha.site_key}></div>*/} + <div className="error">{this.flash.error}</div> + <input type="submit" className="button" value="CONTINUE" /> + </form> + </div> + </div>); + const props = { body, title: 'Phone Number', assets, meta: [] }; + this.body = '<!DOCTYPE html>' + renderToString(<ServerHTML { ...props } />); + }); + + router.post('/submit_mobile', koaBody, function *() { + if (!checkCSRF(this, this.request.body.csrf)) return; + const user_id = this.session.user; + if (!user_id) { this.body = 'user not found'; return; } + let mobile = this.request.body.mobile; + if (!mobile) { + this.flash = {error: 'Please provide a mobile number'}; + this.redirect('/enter_mobile'); + return; + } + + mobile = mobile.match(/\d+/g).join('') + if(mobile.length < "9998887777".length) { + this.flash = {error: 'Please provide an area code'}; + this.redirect('/enter_mobile'); + return; + } + + if(mobile.length === "9998887777".length) { + mobile = `1${mobile}` + } + + const eid = yield models.Identity.findOne( + {attributes: ['id'], where: {user_id, provider: 'email', verified: true}, order: 'id DESC'} + ); + if (!eid) { + this.flash = {error: 'Please confirm your email address first'}; + this.redirect('/enter_mobile'); + return; + } + + // const recaptcha = this.request.body['g-recaptcha-response']; + // const verificationUrl = 'https://www.google.com/recaptcha/api/siteverify?secret=' + config.recaptcha.secret_key + '&response=' + recaptcha + '&remoteip=' + this.req.connection.remoteAddress; + // let captcha_failed; + // try { + // const recaptcha_res = yield request(verificationUrl); + // const body = JSON.parse(recaptcha_res.body); + // captcha_failed = !body.success; + // } catch (e) { + // captcha_failed = true; + // console.error('-- /submit_mobile recaptcha request failed -->', verificationUrl, e); + // } + // if (captcha_failed) { + // console.log('-- /submit_mobile captcha verification failed -->', user_id, this.session.uid, mobile, this.req.connection.remoteAddress); + // this.flash = {error: 'Failed captcha verification, please try again.'}; + // this.redirect('/enter_mobile'); + // return; + // } + + const existing_phone = yield models.Identity.findOne( + {attributes: ['user_id'], where: {phone: mobile, provider: 'phone', verified: true}, order: 'id'} + ); + if (existing_phone && existing_phone.user_id != user_id) { + console.log('-- /submit_email existing_phone -->', user_id, this.session.uid, mobile, existing_phone.user_id); + this.flash = {error: 'This phone number has already been used'}; + this.redirect('/enter_mobile'); + return; + } + + const confirmation_code = Math.random().toString().substring(2, 6); + let mid = yield models.Identity.findOne( + {attributes: ['id', 'phone', 'verified', 'updated_at'], where: {user_id, provider: 'phone'}, order: 'id'} + ); + if (mid) { + if (mid.verified) { + this.flash = {success: 'Phone number has been verified'}; + this.redirect('/create_account'); return; + } else { + const seconds_ago = (Date.now() - mid.updated_at) / 1000.0; + if (seconds_ago < 120) { + this.flash = {error: 'Confirmation was sent a moment ago. You can try again only in 2 minutes.'}; + this.redirect('/enter_mobile'); + return; + } + yield mid.update({confirmation_code, phone: mobile}); + } + } else { + mid = yield models.Identity.create({ + provider: 'phone', + user_id, + uid: this.session.uid, + phone: mobile, + verified: false, + confirmation_code + }); + } + console.log('-- /submit_mobile -->', this.session.uid, this.session.user, mobile, mid.id); + const ip = getRemoteIp(this.req) + + const verifyResult = yield verify({mobile, confirmation_code, ip}); + if (verifyResult && verifyResult.score) eid.update({score: verifyResult.score}); + if(verifyResult && verifyResult.error) { + this.flash = {error: verifyResult.error}; + this.redirect('/enter_mobile'); + return; + } + + const body = renderToString(<div className="App"> + <MiniHeader /> + <SignupProgressBar steps={[this.session.prv || 'identity', 'email', 'phone', 'steem account']} current={3} /> + <br /> + <div className="row" style={{maxWidth: '32rem'}}> + <div className="column"> + Thank you for providing your mobile number ({mobile}).<br /> + To continue please enter the SMS code we've sent you. + </div> + </div> + <br /> + <div className="row" style={{maxWidth: '32rem'}}> + <form className="column" action="/confirm_mobile" method="POST"> + <label> + Confirmation code + <input type="text" name="code" /> + </label> + <br /> + <div className="secondary">Didn't receive the verification code? <a href="/enter_mobile">Re-send</a></div> + <br /> + <input type="submit" className="button" value="CONTINUE" /> + </form> + </div> + </div>); + const props = { body, title: 'Mobile Confirmation', assets, meta: [] }; + this.body = '<!DOCTYPE html>' + renderToString(<ServerHTML { ...props } />); + }); + + router.get('/confirm_mobile/:code', confirmMobileHandler); + router.post('/confirm_mobile', koaBody, confirmMobileHandler); +} diff --git a/server/teleSign.js b/server/teleSign.js new file mode 100644 index 000000000..5b9710297 --- /dev/null +++ b/server/teleSign.js @@ -0,0 +1,138 @@ +import fetch from 'node-fetch'; +import config from '../config'; +import crypto from 'crypto' + +const {customer_id} = config.telesign +const api_key = new Buffer(config.telesign.rest_api_key, 'base64') +const use_case_code = 'BACS' // Use Case: avoid bulk attack and spammers + +// Testing, always blocked: 1-310-555-0100 + +/** @return {object} - {reference_id} or {error} */ +export function* verify({mobile, confirmation_code, ip}) { + try { + const result = yield getScore(mobile) + const {recommendation, score} = result.risk + if(recommendation !== 'allow') { + console.log(`TeleSign did not allow phone ${mobile} ip ${ip}. TeleSign responded: ${recommendation}`); + return {error: 'Unable to verify your phone number. Please try a different phone number.', score} + } + const {reference_id} = yield verifySms({mobile, confirmation_code, ip}) + return {reference_id, score} + } catch(error) { + console.log('-- verify score error -->', error); + return {error: 'Unable to verify phone, please try again later.'} + } +} + +function getScore(mobile) { + const fields = urlencode({ + ucid: use_case_code, + }) + const resource = '/v1/phoneid/score/' + mobile.match(/\d+/g).join('') + const method = 'GET' + return fetch( + `https://rest.telesign.com${resource}?${fields}`, { + method, + headers: authHeaders({resource, method}) + } + ) + .then(r => r.json()) + .catch(error => { + console.error(`ERROR: Phone ${mobile} score exception`, JSON.stringify(error, null, 0)); + return Promise.reject(error) + }) + .then(response => { + const {status} = response + if(status.code === 300) { + // Transaction successfully completed + console.log(`Phone ${mobile} score`, JSON.stringify(response, null, 0)) + return Promise.resolve(response) + } + console.error(`ERROR: Phone ${mobile} score`, JSON.stringify(response, null, 0)) + return Promise.reject(response) + }) +} + + +function verifySms({mobile, confirmation_code, ip}) { + // https://developer.telesign.com/v2.0/docs/rest_api-verify-sms + const f = { + phone_number: mobile, + language: 'en-US', + ucid: use_case_code, + verify_code: confirmation_code, + template: '$$CODE$$ is your Steemit confirmation code', + } + if(ip) f.originating_ip = ip + const fields = urlencode(f) + // console.log('fields', fields) // logspam + + const resource = '/v1/verify/sms' + const method = 'POST' + return fetch( + 'https://rest.telesign.com' + resource, { + method, + body: fields, + headers: authHeaders({resource, method, fields}) + } + ) + .then(r => r.json()) + .catch(error => { + console.error(`ERROR: SMS failed to ${mobile} code ${confirmation_code} req ip ${ip} exception`, JSON.stringify(error, null, 0)); + return Promise.reject(error) + }) + .then(response => { + const {status} = response + if(status.code === 290) { + // Message in progress + console.log(`Sent SMS to ${mobile} code ${confirmation_code}`, JSON.stringify(response, null, 0)) + return Promise.resolve(response) + } + console.error(`ERROR: SMS failed to ${mobile} code ${confirmation_code}:`, JSON.stringify(response, null, 0)) + return Promise.reject(response) + }) +} + +/** + @arg {string} resource `/v1/verify/AEBC93B5898342F790E4E19FED41A7DA` + @arg {string} method [GET|POST|PUT] + @arg {string} fields url query string +*/ +function authHeaders({ + resource, + fields, + method = 'GET', +}) { + const auth_method = 'HMAC-SHA256' + const currDate = new Date().toUTCString() + const nonce = Math.random().toString(36).slice(15) + + let content_type = '' + if(/POST|PUT/.test(method)) + content_type = 'application/x-www-form-urlencoded' + + let strToSign = `${method}\n${content_type}\n\nx-ts-auth-method:${auth_method}\nx-ts-date:${currDate}\nx-ts-nonce:${nonce}` + + if(fields) { + strToSign += '\n' + fields + } + strToSign += '\n' + resource + + // console.log('strToSign', strToSign) // logspam + const sig = crypto.createHmac('sha256', api_key).update(strToSign, 'utf8').digest('base64') + + const headers = { + Authorization: `TSA ${customer_id}:${sig}`, + 'Content-Type': content_type, + 'x-ts-date': currDate, + 'x-ts-auth-method': auth_method, + 'x-ts-nonce': nonce + } + return headers +} + +const urlencode = json => + Object.keys(json).map( + key => encodeURI(key) + '=' + encodeURI(json[key]) + ).join('&') diff --git a/webpack/utils/start-koa.js b/webpack/utils/start-koa.js index 410ac29a0..4d9ed203a 100644 --- a/webpack/utils/start-koa.js +++ b/webpack/utils/start-koa.js @@ -30,14 +30,6 @@ const startServer = () => { if (!started) { started = true; - // Start browserSync - //browserSync({ - // port: parseInt(process.env.PORT, 10) + 2 || 3002, - // proxy: `0.0.0.0:${parseInt(process.env.PORT, 10) || 3000}`, - // open: false, - // ui: false - //}); - // Listen for `rs` in stdin to restart server console.log('type `rs` in console for restarting koa application'); process.stdin.setEncoding('utf8'); @@ -47,7 +39,9 @@ const startServer = () => { }); // Start watcher on server files and restart server on change - watch(path.join(__dirname, '../../server'), () => restartServer()); + const server_path = path.join(__dirname, '../../server'); + // const app_path = path.join(__dirname, '../../app'); + watch([server_path], () => restartServer()); } } }); -- GitLab