Skip to content

Commit

Permalink
ask users that didn't provide email to enter and confirm their email …
Browse files Browse the repository at this point in the history
…addresses
  • Loading branch information
valzav committed Aug 17, 2016
1 parent e55dddb commit 6c31d7f
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 10 deletions.
7 changes: 7 additions & 0 deletions config/steem-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
"ws://localhost:8091",
"ws://localhost:8092"
],
"sendgrid": {
"key": "SG.xxx_yyyy",
"from": "[email protected]",
"templates": {
"confirm_email": "some_template_id"
}
},
"img_proxy_prefix": "",
"ipfs_prefix": "",
"google_analytics_id": "",
Expand Down
2 changes: 1 addition & 1 deletion db/migrations/20160419161331-create-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module.exports = {
type: Sequelize.DATE
}
}).then(function () {
queryInterface.addIndex('users', ['email'], {indicesType: 'UNIQUE'});
queryInterface.addIndex('users', ['email']);
queryInterface.addIndex('users', ['uid'], {indicesType: 'UNIQUE'});
});
},
Expand Down
5 changes: 4 additions & 1 deletion db/migrations/20160420133848-create-identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ module.exports = {
email: {
type: Sequelize.STRING
},
confirm_token: {
type: Sequelize.STRING
},
verified: {
type: Sequelize.BOOLEAN
},
Expand All @@ -43,7 +46,7 @@ module.exports = {
type: Sequelize.DATE
}
}).then(function () {
queryInterface.addIndex('identities', ['email'], {indicesType: 'UNIQUE'});
queryInterface.addIndex('identities', ['email']);
});
},
down: function (queryInterface, Sequelize) {
Expand Down
1 change: 1 addition & 0 deletions db/models/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = function (sequelize, DataTypes) {
provider_user_id: {type: DataTypes.STRING, unique: true},
name: DataTypes.STRING,
email: {type: DataTypes.STRING, unique: true},
confirmation_code: {type: DataTypes.STRING, unique: true},
verified: DataTypes.BOOLEAN,
score: DataTypes.INTEGER
}, {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"sanitize-html": "^1.11.4",
"sass-loader": "^3.1.2",
"secure-random": "^1.1.1",
"sendgrid": "^4.0.1",
"sequelize": "^3.21.0",
"sequelize-cli": "^2.3.1",
"speakingurl": "^9.0.0",
Expand Down
2 changes: 1 addition & 1 deletion server/api/account_recovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import koa_body from 'koa-body';
import models from 'db/models';
import config from 'config';
import {esc, escAttrs} from 'db/models';
import {getRemoteIp, rateLimitReq, checkCSRF} from './utils';
import {getRemoteIp, rateLimitReq, checkCSRF} from '../utils';

export default function useAccountRecoveryApi(app) {
const router = koa_router();
Expand Down
9 changes: 8 additions & 1 deletion server/api/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import findUser from 'db/utils/find_user';
import config from 'config';
import recordWebEvent from 'server/record_web_event';
import {esc, escAttrs} from 'db/models';
import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from './utils';
import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from '../utils';
import coBody from 'co-body';

export default function useGeneralApi(app) {
Expand Down Expand Up @@ -69,6 +69,13 @@ 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.');
}
const eid = yield models.Identity.findOne(
{attributes: ['id'], where: {user_id, provider: 'email', verified: true}, order: 'id DESC'}
);
if (!eid) {
console.log(`api /accounts: not confirmed email for user ${this.session.uid} #${user_id}`);
throw new Error('Please confirm your email address first');
}

yield createAccount({
signingKey: config.registrar.signing_key,
Expand Down
24 changes: 18 additions & 6 deletions server/api/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ function retrieveFacebookUserData(access_token) {

function* handleFacebookCallback() {
console.log('-- /handle_facebook_callback -->', this.session.uid, this.query);
let verified_email = false;
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 = u.verified && u.email;
const attrs = {
uid: this.session.uid,
name: u.name,
Expand All @@ -73,6 +75,11 @@ function* handleFacebookCallback() {
verified: u.verified,
provider_user_id: u.id
};
const i_attrs_email = {
provider: 'email',
email: u.email,
verified: verified_email
};

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');
Expand Down Expand Up @@ -114,26 +121,31 @@ function* handleFacebookCallback() {
return null;
}

if (!attrs.email) throw new Error('not valid email');

if (user) {
attrs.id = user.id;
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: 'facebook'}});
if(verified_email) yield models.Identity.update(i_attrs_email, {where: {user_id: user.id, provider: 'email', email: u.email}});
console.log('-- fb updated user -->', this.session.uid, user.id, u.name, u.email);
} else {
user = yield models.User.create(attrs);
console.log('-- fb created user -->', user.id, u.name, u.email);
i_attrs.user_id = user.id;
i_attrs_email.user_id = i_attrs.user_id = user.id;
const identity = yield models.Identity.create(i_attrs);
console.log('-- fb created identity -->', this.session.uid, identity.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'};
this.redirect('/create_account');
if (verified_email) {
this.redirect('/create_account');
} else {
this.redirect('/enter_email');
}
return null;
}

Expand Down Expand Up @@ -236,7 +248,7 @@ function* handleRedditCallback() {
} catch (error) {
return logErrorAndRedirect(this, 'reddit', error);
}
this.redirect('/create_account');
this.redirect('/enter_email');
return null;
}

Expand Down
30 changes: 30 additions & 0 deletions server/sendEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import sendgrid from 'sendgrid';
import config from '../config';

const sg = sendgrid(config.sendgrid.key);

export default function sendEmail(template, to, params) {
const tmpl_id = config.sendgrid.templates[template];
if (!tmpl_id) throw new Error(`can't find template ${template}`);

const request = sg.emptyRequest({
method: 'POST',
path: '/v3/mail/send',
body: {
template_id: tmpl_id,
personalizations: [
{to: [{email: to}],
substitutions: params},
],
from: {email: config.sendgrid.from}
}
});

sg.API(request)
.then(response => {
console.log(`sent '${template}' email to '${to}'`, response.statusCode);
})
.catch(error => {
console.error(`failed to send '${template}' email to '${to}'`, error);
});
}
2 changes: 2 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useRedirects from './redirects';
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 isBot from 'koa-isbot';
import session from 'koa-session';
import csrf from 'koa-csrf';
Expand Down Expand Up @@ -74,6 +75,7 @@ useRedirects(app);
useOauthLogin(app);
useGeneralApi(app);
useAccountRecoveryApi(app);
useEnterAndConfirmEmailPages(app);
app.use(favicon(path.join(__dirname, '../app/assets/images/favicons/favicon.ico')));
app.use(isBot());

Expand Down
149 changes: 149 additions & 0 deletions server/server_pages/enter_confirm_email.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import koa_router from 'koa-router';
import koa_body from 'koa-body';
import React from 'react';
import { renderToString } from 'react-dom/server';
import models from 'db/models';
import findUser from 'db/utils/find_user';
import config from 'config';
import recordWebEvent from 'server/record_web_event';
import {esc, escAttrs} from 'db/models';
import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from '../utils';
import coBody from 'co-body';
import ServerHTML from '../server-html';
import { Link } from 'react-router';
import Icon from 'app/components/elements/Icon.jsx';
import sendEmail from '../sendEmail';

// alter table identities add confirmation_code varchar(256);
// alter table identities drop index identities_email;
// create index `identities_email` ON `identities` (`email`);
// alter table users drop index users_email;
// create index `users_email` ON `users` (`email`);


const assets = require('../webpack-stats.json');

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

function *confirmEmailHandler() {
const confirmation_code = this.params && this.params.code ? this.params.code : this.request.body.code;
console.log('-- /confirm_email -->', this.session.uid, this.session.user, confirmation_code);
const user_id = this.session.user;
const eid = yield models.Identity.findOne(
{attributes: ['id', 'verified', 'created_at'], where: {user_id, provider: 'email', confirmation_code, verified: false}, order: 'id DESC'}
);
if (!eid) {
this.status = 401;
this.body = 'confirmation code not found';
return;
}
const hours_ago = (Date.now() - eid.created_at) / 1000.0 / 3600.0;
if (hours_ago > 24.0) {
this.status = 401;
this.body = 'confirmation code not found';
return;
}
if (!eid.verified) yield eid.update({verified: true});
this.redirect('/create_account');
}

export default function useEnterAndConfirmEmailPages(app) {
const router = koa_router();
app.use(router.routes());
const koaBody = koa_body();

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; }
const eid = yield models.Identity.findOne(
{attributes: ['email'], where: {user_id, provider: 'email'}, order: 'id DESC'}
);
const body = renderToString(<div className="App">
{header}
<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 : ''} />
</label>
<br />
<input type="submit" className="button" value="CONTINUE" />
</form>
</div>
</div>);
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; }
const email = this.request.body.email;
if (!email) { this.redirect('/enter_email'); return; }
console.log('-- /submit_email -->', this.session.uid, this.session.user, email);

const confirmation_code = Math.random().toString(36).slice(2);
yield models.Identity.create({
provider: 'email',
user_id,
uid: this.session.uid,
email,
verified: false,
confirmation_code
});
sendEmail('confirm_email', email, {confirmation_code});

const body = renderToString(<div className="App">
{header}
<br />
<div className="row">
<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>
</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: [] };
this.body = '<!DOCTYPE html>' + renderToString(<ServerHTML { ...props } />);
});

router.get('/confirm_email/:code', confirmEmailHandler);
router.post('/confirm_email', koaBody, confirmEmailHandler);
}
File renamed without changes.

0 comments on commit 6c31d7f

Please sign in to comment.