Skip to content

Commit 78e23d7

Browse files
Merge #1045
1045: Sending an email to a user to confirm their email address r=carols10cents This PR addresses issue #808, allow editing your own email address. Following the discussion on the issue, this implements the second of three parts, the ability to send a confirmation email to a user for verification. For a full explanation of the issue, see [this comment](#808 (comment)) I left which should fully explain the changes made, as well as parts 1 and 3. In short, I added two tables to the database: one for the user's email, the other for a confirmation token associated with the email. Once the user clicks the link sent, the associated token is deleted from the token table and `email_verified` is set to `true` in the email table. It is indicated to the user if they have not verified their email on the account settings page, a message and email resend button being displayed. Sending emails requires Mailgun to be added to the crates.io Heroku account. This has applications for issue #924 in that we should now be able to send an email to a user, if they have added their email. I know there was a lot of discussion on that issue regarding the ability to send users an email to be added as an owner.
2 parents c7daabf + 26254f4 commit 78e23d7

24 files changed

+1004
-31
lines changed

.env.sample

+11
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,14 @@ export GIT_REPO_CHECKOUT=./tmp/index-co
3838
# to the address `http://localhost:4200/authorize/github`.
3939
export GH_CLIENT_ID=
4040
export GH_CLIENT_SECRET=
41+
42+
# Credentials for configuring Mailgun. You can leave these commented out
43+
# if you are not interested in actually sending emails. If left empty,
44+
# a mock email will be sent to a file in your local '/tmp/' directory.
45+
# If interested in setting up Mailgun to send emails, you will have
46+
# to create an account with Mailgun and modify these manually.
47+
# If running a crates mirror on heroku, you can instead add the Mailgun
48+
# app to your instance and shouldn't have to mess with these.
49+
# export MAILGUN_SMTP_LOGIN=
50+
# export MAILGUN_SMTP_PASSWORD=
51+
# export MAILGUN_SMTP_SERVER=

Cargo.lock

+153
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ comrak = { version = "0.1.9", default-features = false }
5959
ammonia = "0.7.0"
6060
docopt = "0.8.1"
6161
itertools = "0.6.0"
62+
lettre = "0.6"
6263

6364
conduit = "0.8"
6465
conduit-conditional-get = "0.8"

app/components/email-input.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
import Component from '@ember/component';
22
import { empty } from '@ember/object/computed';
3+
import { computed } from '@ember/object';
4+
import { inject as service } from '@ember/service';
35

46
export default Component.extend({
7+
ajax: service(),
8+
flashMessages: service(),
9+
510
type: '',
611
value: '',
712
isEditing: false,
813
user: null,
914
disableSave: empty('user.email'),
1015
notValidEmail: false,
1116
prevEmail: '',
12-
emailIsNull: true,
17+
emailIsNull: computed('user.email', function() {
18+
let email = this.get('user.email');
19+
return (email == null);
20+
}),
21+
emailNotVerified: computed('user.email', 'user.email_verified', function() {
22+
let email = this.get('user.email');
23+
let verified = this.get('user.email_verified');
24+
25+
return (email != null && !verified);
26+
}),
27+
isError: false,
28+
emailError: '',
1329

1430
actions: {
1531
editEmail() {
@@ -48,6 +64,8 @@ export default Component.extend({
4864
msg = 'An unknown error occurred while saving this email.';
4965
}
5066
this.set('serverError', msg);
67+
this.set('isError', true);
68+
this.set('emailError', `Error in saving email: ${msg}`);
5169
});
5270

5371
this.set('isEditing', false);
@@ -57,6 +75,30 @@ export default Component.extend({
5775
cancelEdit() {
5876
this.set('isEditing', false);
5977
this.set('value', this.get('prevEmail'));
78+
},
79+
80+
resendEmail() {
81+
let user = this.get('user');
82+
83+
this.get('ajax').raw(`/api/v1/users/${user.id}/resend`, { method: 'PUT',
84+
user: {
85+
avatar: user.avatar,
86+
email: user.email,
87+
email_verified: user.email_verified,
88+
kind: user.kind,
89+
login: user.login,
90+
name: user.name,
91+
url: user.url
92+
}
93+
}).catch((error) => {
94+
if (error.payload) {
95+
this.set('isError', true);
96+
this.set('emailError', `Error in resending message: ${error.payload.errors[0].detail}`);
97+
} else {
98+
this.set('isError', true);
99+
this.set('emailError', 'Unknown error in resending message');
100+
}
101+
});
60102
}
61103
}
62104
});

app/models/user.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DS from 'ember-data';
22

33
export default DS.Model.extend({
44
email: DS.attr('string'),
5+
email_verified: DS.attr('boolean'),
56
name: DS.attr('string'),
67
login: DS.attr('string'),
78
avatar: DS.attr('string'),

app/router.js

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Router.map(function() {
4545
this.route('catchAll', { path: '*path' });
4646
this.route('team', { path: '/teams/:team_id' });
4747
this.route('policies');
48+
this.route('confirm', { path: '/confirm/:email_token' });
4849
});
4950

5051
export default Router;

app/routes/confirm.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Ember from 'ember';
2+
import { inject as service } from '@ember/service';
3+
4+
export default Ember.Route.extend({
5+
flashMessages: service(),
6+
ajax: service(),
7+
8+
model(params) {
9+
return this.get('ajax').raw(`/api/v1/confirm/${params.email_token}`, { method: 'PUT', data: {} })
10+
.then(() => {
11+
/* We need this block to reload the user model from the database,
12+
without which if we haven't submitted another GET /me after
13+
clicking the link and before checking their account info page,
14+
the user will still see that their email has not yet been
15+
validated and could potentially be confused, resend the email,
16+
and set up a situation where their email has been verified but
17+
they have an unverified token sitting in the DB.
18+
19+
Suggestions of a more ideomatic way to fix/test this are welcome!
20+
*/
21+
if (this.session.get('isLoggedIn')) {
22+
this.get('ajax').request('/api/v1/me').then((response) => {
23+
this.session.set('currentUser', this.store.push(this.store.normalize('user', response.user)));
24+
});
25+
}
26+
})
27+
.catch((error) => {
28+
if (error.payload) {
29+
this.get('flashMessages').queue(`Error in email confirmation: ${error.payload.errors[0].detail}`);
30+
return this.replaceWith('index');
31+
} else {
32+
this.get('flashMessages').queue(`Unknown error in email confirmation`);
33+
return this.replaceWith('index');
34+
}
35+
});
36+
}
37+
});

app/styles/me.scss

+41
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,47 @@
6565
}
6666
}
6767

68+
#me-email {
69+
border-bottom: 5px solid $gray-border;
70+
padding-bottom: 20px;
71+
margin-bottom: 20px;
72+
@include display-flex;
73+
@include flex-direction(column);
74+
.row {
75+
width: 100%;
76+
border: 1px solid #d5d3cb;
77+
border-bottom-width: 0px;
78+
&:last-child { border-bottom-width: 1px; }
79+
padding: 10px 20px;
80+
@include display-flex;
81+
@include align-items(center);
82+
.label {
83+
@include flex(1);
84+
margin-right: 0.4em;
85+
font-weight: bold;
86+
}
87+
.email {
88+
@include flex(20);
89+
}
90+
.actions {
91+
@include display-flex;
92+
@include align-items(center);
93+
img { margin-left: 10px }
94+
}
95+
.email-form {
96+
display: inline-flex;
97+
}
98+
.space-right {
99+
margin-right: 10px;
100+
}
101+
}
102+
.friendly-message {
103+
width: 95%;
104+
margin-left: auto;
105+
margin-right: auto;
106+
}
107+
}
108+
68109
#me-api {
69110
@media only screen and (max-width: 350px) {
70111
.api { display: none; }
+31-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
{{#if emailIsNull }}
2+
<div class='friendly-message'>
3+
<p class='small-text'> Please add your email address. We will only use
4+
it to contact you about your account. We promise we'll never share it!
5+
</p>
6+
</div>
7+
{{/if}}
8+
19
{{#if isEditing }}
210
<div class='row'>
311
<div class='label'>
412
<dt>Email</dt>
513
</div>
6-
<form {{action 'saveEmail' on='submit'}}>
7-
{{input type=type value=value placeholder='Email' class='form-control space-bottom'}}
14+
<form class='email-form' {{action 'saveEmail' on='submit'}}>
15+
{{input type=type value=value placeholder='Email' class='form-control space-right'}}
816
{{#if notValidEmail }}
9-
<p class='small-text error'>Invalid email format. Please try again.</p>
10-
{{/if}}
11-
{{#if emailIsNull }}
12-
<p class='small-text'> Please add your email address. We will only use
13-
it to contact you about your account. We promise we'll never share it!
14-
</p>
17+
<div class='error'>
18+
<p class='small-text error'>Whoops, that email format is invalid</p>
19+
</div>
1520
{{/if}}
1621
<div class='actions'>
1722
<button type='submit' class='small yellow-button space-right' disabled={{disableSave}}>Save</button>
@@ -20,7 +25,7 @@
2025
</form>
2126
</div>
2227
{{else}}
23-
<div class='row align-center'>
28+
<div class='row'>
2429
<div class='label'>
2530
<dt>Email</dt>
2631
</div>
@@ -31,4 +36,21 @@
3136
<button class='small yellow-button space-left' {{action 'editEmail'}}>Edit</button>
3237
</div>
3338
</div>
39+
{{#if emailNotVerified }}
40+
<div class='row'>
41+
<div class='label'>
42+
<p class='small-text'>Your email has not yet been verified.</p>
43+
</div>
44+
<div class='actions'>
45+
<button class='small yellow-button space-left' {{action 'resendEmail'}}>Resend</button>
46+
</div>
47+
</div>
48+
{{/if}}
49+
{{#if isError}}
50+
<div class='row'>
51+
<div class='label'>
52+
<p class='small-text'>{{emailError}}</p>
53+
</div>
54+
</div>
55+
{{/if}}
3456
{{/if}}

app/templates/confirm.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Thank you for confirming your email! :)</h1>

app/templates/error.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<h1>Something Went Wrong!</h1>
22
<h5>{{model.message}}</h5>
33
<pre>
4-
{{model.stack}}
4+
{{model.stack}}
55
</pre>

app/templates/me/index.hbs

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
<dd>{{ model.user.name }}</dd>
1717
<dt>GitHub Account</dt>
1818
<dd>{{ model.user.login }}</dd>
19-
{{email-input type='email' value=model.user.email user=model.user}}
2019
</dl>
2120
</div>
2221
</div>
2322

23+
<div id='me-email'>
24+
<h2>User Email</h2>
25+
{{email-input type='email' value=model.user.email user=model.user}}
26+
</div>
27+
2428
<div id='me-api'>
2529
<div class='me-subheading'>
2630
<h2>API Access</h2>

docs/CONTRIBUTING.md

+29
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,35 @@ yarn run start:local
338338
339339
And then you should be able to visit http://localhost:4200!
340340
341+
##### Using Mailgun to Send Emails
342+
343+
We currently have email functionality enabled for confirming a user's email
344+
address. In development, the sending of emails is simulated by a file
345+
representing the email being created in your local `/tmp/` directory. If
346+
you want to test sending real emails, you will have to either set the
347+
Mailgun environment variables in `.env` manually or run your app instance
348+
on Heroku and add the Mailgun app.
349+
350+
To set the environment variables manually, create an account and configure
351+
Mailgun. [These quick start instructions]
352+
(http://mailgun-documentation.readthedocs.io/en/latest/quickstart.html)
353+
might be helpful. Once you get the environment variables for the app, you
354+
will have to add them to the bottom of the `.env` file. You will need to
355+
fill in the `MAILGUN_SMTP_LOGIN`, `MAILGUN_SMTP_PASSWORD`, and
356+
`MAILGUN_SMTP_SERVER` fields.
357+
358+
If using Heroku, you should be able to add the app to your instance on your
359+
dashboard. When your code is pushed and run on Heroku, the environment
360+
variables should be detected and you should not have to set anything
361+
manually.
362+
363+
In either case, you should be able to check in your Mailgun account to see
364+
if emails are being detected and sent. Relevant information should be under
365+
the 'logs' tab on your Mailgun dashboard. To access, if the variables were
366+
set up manually, log in to your account. If the variables were set through
367+
Heroku, you should be able to click on the Mailgun icon in your Heroku
368+
dashboard, which should take you to your Mailgun dashboard.
369+
341370
#### Running the backend tests
342371
343372
In your `.env` file, set `TEST_DATABASE_URL` to a value that's the same as
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- This file should undo anything in `up.sql`
2+
DROP table tokens;
3+
DROP table emails;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Your SQL goes here
2+
CREATE table emails (
3+
id SERIAL PRIMARY KEY,
4+
user_id INTEGER NOT NULL UNIQUE,
5+
email VARCHAR NOT NULL,
6+
verified BOOLEAN DEFAULT false NOT NULL
7+
);
8+
9+
CREATE table tokens (
10+
id SERIAL PRIMARY KEY,
11+
email_id INTEGER NOT NULL UNIQUE REFERENCES emails,
12+
token VARCHAR NOT NULL,
13+
created_at TIMESTAMP NOT NULL DEFAULT now()
14+
);
15+
16+
INSERT INTO emails (user_id, email)
17+
SELECT id, email FROM users WHERE email IS NOT NULL;

0 commit comments

Comments
 (0)