Skip to content

Commit 08eab88

Browse files
Merge #1069
1069: Email polish r=carols10cents Just cleaning up a few small things before deploying email verification :)
2 parents d0edb9b + 9fcc3b0 commit 08eab88

File tree

6 files changed

+137
-30
lines changed

6 files changed

+137
-30
lines changed

app/components/email-input.js

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ export default Component.extend({
2626
}),
2727
isError: false,
2828
emailError: '',
29+
disableResend: false,
30+
resendButtonText: computed('disableResend', 'user.email_verification_sent', function() {
31+
if (this.get('disableResend')) {
32+
return 'Sent!';
33+
} else if (this.get('user.email_verification_sent')) {
34+
return 'Resend';
35+
} else {
36+
return 'Send verification email';
37+
}
38+
}),
2939

3040
actions: {
3141
editEmail() {
@@ -55,7 +65,11 @@ export default Component.extend({
5565

5666
user.set('email', userEmail);
5767
user.save()
58-
.then(() => this.set('serverError', null))
68+
.then(() => {
69+
this.set('serverError', null);
70+
this.set('user.email_verification_sent', true);
71+
this.set('user.email_verified', false);
72+
})
5973
.catch(err => {
6074
let msg;
6175
if (err.errors && err.errors[0] && err.errors[0].detail) {
@@ -70,6 +84,7 @@ export default Component.extend({
7084

7185
this.set('isEditing', false);
7286
this.set('notValidEmail', false);
87+
this.set('disableResend', false);
7388
},
7489

7590
cancelEdit() {
@@ -80,25 +95,19 @@ export default Component.extend({
8095
resendEmail() {
8196
let user = this.get('user');
8297

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-
});
98+
this.get('ajax').raw(`/api/v1/users/${user.id}/resend`, {
99+
method: 'PUT'
100+
})
101+
.then(() => this.set('disableResend', true))
102+
.catch((error) => {
103+
if (error.payload) {
104+
this.set('isError', true);
105+
this.set('emailError', `Error in resending message: ${error.payload.errors[0].detail}`);
106+
} else {
107+
this.set('isError', true);
108+
this.set('emailError', 'Unknown error in resending message');
109+
}
110+
});
102111
}
103112
}
104113
});

app/models/user.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import DS from 'ember-data';
33
export default DS.Model.extend({
44
email: DS.attr('string'),
55
email_verified: DS.attr('boolean'),
6+
email_verification_sent: DS.attr('boolean'),
67
name: DS.attr('string'),
78
login: DS.attr('string'),
89
avatar: DS.attr('string'),

app/styles/me.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,24 @@
8686
}
8787
.email {
8888
@include flex(20);
89+
.verified {
90+
color: green;
91+
font-weight: bold;
92+
}
8993
}
9094
.actions {
9195
@include display-flex;
9296
@include align-items(center);
9397
img { margin-left: 10px }
9498
}
9599
.email-form {
100+
@include flex(10);
96101
display: inline-flex;
102+
justify-content: space-between;
103+
flex-wrap: wrap;
104+
input.ember-text-field {
105+
width: 400px;
106+
}
97107
}
98108
.space-right {
99109
margin-right: 10px;

app/templates/components/email-input.hbs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@
3030
<dt>Email</dt>
3131
</div>
3232
<div class='email'>
33-
<dd class='no-space-left'>{{ user.email }}</dd>
33+
<dd class='no-space-left'>
34+
{{ user.email }}
35+
{{#if user.email_verified}}
36+
<span class='verified'>Verified!</span>
37+
{{/if}}
38+
</dd>
3439
</div>
3540
<div class='actions'>
3641
<button class='small yellow-button space-left' {{action 'editEmail'}}>Edit</button>
@@ -39,10 +44,13 @@
3944
{{#if emailNotVerified }}
4045
<div class='row'>
4146
<div class='label'>
47+
{{#if user.email_verification_sent}}
48+
<p class='small-text'>We have sent a verification email to your address.</p>
49+
{{/if}}
4250
<p class='small-text'>Your email has not yet been verified.</p>
4351
</div>
4452
<div class='actions'>
45-
<button class='small yellow-button space-left' {{action 'resendEmail'}}>Resend</button>
53+
<button class='small yellow-button space-left' {{action 'resendEmail'}} disabled={{disableResend}}>{{resendButtonText}}</button>
4654
</div>
4755
</div>
4856
{{/if}}

src/tests/user.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use conduit::{Handler, Method};
44

55
use cargo_registry::token::ApiToken;
66
use cargo_registry::krate::EncodableCrate;
7-
use cargo_registry::user::{User, NewUser, EncodablePrivateUser, EncodablePublicUser, Email, Token};
7+
use cargo_registry::user::{User, NewUser, EncodablePrivateUser, EncodablePublicUser, Email,
8+
NewEmail, Token};
89
use cargo_registry::version::EncodableVersion;
910

1011
use diesel::prelude::*;
@@ -417,6 +418,8 @@ fn test_email_get_and_put() {
417418
let r = ::json::<R>(&mut response);
418419
assert_eq!(r.user.email.unwrap(), "[email protected]");
419420
assert_eq!(r.user.login, "mango");
421+
assert!(!r.user.email_verified);
422+
assert!(r.user.email_verification_sent);
420423
}
421424

422425
/* Given a crates.io user, check to make sure that the user
@@ -607,6 +610,8 @@ fn test_insert_into_email_table_with_email_change() {
607610
let r = ::json::<R>(&mut response);
608611
assert_eq!(r.user.email.unwrap(), "[email protected]");
609612
assert_eq!(r.user.login, "potato");
613+
assert!(!r.user.email_verified);
614+
assert!(r.user.email_verification_sent);
610615

611616
let body = r#"{"user":{"email":"[email protected]","name":"potato","login":"potato","avatar":"https://avatars0.githubusercontent.com","url":"https://github.com/potato","kind":null}}"#;
612617
let mut response = ok_resp!(
@@ -638,6 +643,8 @@ fn test_insert_into_email_table_with_email_change() {
638643
));
639644
let r = ::json::<R>(&mut response);
640645
assert_eq!(r.user.email.unwrap(), "[email protected]");
646+
assert!(!r.user.email_verified);
647+
assert!(r.user.email_verification_sent);
641648
assert_eq!(r.user.login, "potato");
642649
}
643650

@@ -703,4 +710,55 @@ fn test_confirm_user_email() {
703710
assert_eq!(r.user.email.unwrap(), "[email protected]");
704711
assert_eq!(r.user.login, "potato");
705712
assert!(r.user.email_verified);
713+
assert!(r.user.email_verification_sent);
714+
}
715+
716+
/* Given a user who existed before we added email confirmation,
717+
test that `email_verification_sent` is false so that we don't
718+
make the user think we've sent an email when we haven't.
719+
*/
720+
#[test]
721+
fn test_existing_user_email() {
722+
use cargo_registry::schema::{emails, users};
723+
use diesel::insert;
724+
725+
#[derive(Deserialize)]
726+
struct R {
727+
user: EncodablePrivateUser,
728+
}
729+
730+
let (_b, app, middle) = ::app();
731+
let mut req = ::req(app.clone(), Method::Get, "/me");
732+
{
733+
let conn = app.diesel_database.get().unwrap();
734+
let user = ::new_user("potahto");
735+
736+
// Deliberately not using User::create_or_update since that
737+
// will try to send a verification email; we want to simulate
738+
// a user who already had an email before we added verification.
739+
let user = insert(&user)
740+
.into(users::table)
741+
.get_result::<User>(&*conn)
742+
.unwrap();
743+
744+
println!("{:?}", user);
745+
746+
let email = NewEmail {
747+
user_id: user.id,
748+
email: String::from("[email protected]"),
749+
verified: false,
750+
};
751+
752+
insert(&email).into(emails::table).execute(&*conn).unwrap();
753+
754+
::sign_in_as(&mut req, &user);
755+
}
756+
757+
let mut response = ok_resp!(middle.call(
758+
req.with_path("/api/v1/me").with_method(Method::Get),
759+
));
760+
let r = ::json::<R>(&mut response);
761+
assert_eq!(r.user.email.unwrap(), "[email protected]");
762+
assert!(!r.user.email_verified);
763+
assert!(!r.user.email_verification_sent);
706764
}

src/user/mod.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ pub struct EncodablePrivateUser {
207207
pub login: String,
208208
pub email: Option<String>,
209209
pub email_verified: bool,
210+
pub email_verification_sent: bool,
210211
pub name: Option<String>,
211212
pub avatar: Option<String>,
212213
pub url: Option<String>,
@@ -240,7 +241,11 @@ impl User {
240241
}
241242

242243
/// Converts this `User` model into an `EncodablePrivateUser` for JSON serialization.
243-
pub fn encodable_private(self, email_verified: bool) -> EncodablePrivateUser {
244+
pub fn encodable_private(
245+
self,
246+
email_verified: bool,
247+
email_verification_sent: bool,
248+
) -> EncodablePrivateUser {
244249
let User {
245250
id,
246251
email,
@@ -254,6 +259,7 @@ impl User {
254259
id: id,
255260
email: email,
256261
email_verified,
262+
email_verification_sent,
257263
avatar: gh_avatar,
258264
login: gh_login,
259265
name: name,
@@ -415,16 +421,31 @@ pub fn me(req: &mut Request) -> CargoResult<Response> {
415421

416422
use self::users::dsl::{users, id};
417423
use self::emails::dsl::{emails, user_id};
424+
use diesel::select;
425+
use diesel::expression::dsl::exists;
418426

419427
let u_id = req.user()?.id;
420428
let conn = req.db_conn()?;
421429

422430
let user_info = users.filter(id.eq(u_id)).first::<User>(&*conn)?;
423431
let email_result = emails.filter(user_id.eq(u_id)).first::<Email>(&*conn);
424432

425-
let (email, verified): (Option<String>, bool) = match email_result {
426-
Ok(response) => (Some(response.email), response.verified),
427-
Err(_) => (None, false),
433+
let (email, verified, verification_sent): (Option<String>, bool, bool) = match email_result {
434+
Ok(email_record) => {
435+
let verification_sent = if email_record.verified {
436+
true
437+
} else {
438+
select(exists(Token::belonging_to(&email_record)))
439+
.get_result(&*conn)
440+
.unwrap_or(false)
441+
};
442+
(
443+
Some(email_record.email),
444+
email_record.verified,
445+
verification_sent,
446+
)
447+
}
448+
Err(_) => (None, false, false),
428449
};
429450

430451
let user = User {
@@ -436,7 +457,9 @@ pub fn me(req: &mut Request) -> CargoResult<Response> {
436457
struct R {
437458
user: EncodablePrivateUser,
438459
}
439-
Ok(req.json(&R { user: user.encodable_private(verified) }))
460+
Ok(req.json(&R {
461+
user: user.encodable_private(verified, verification_sent),
462+
}))
440463
}
441464

442465
/// Handles the `GET /users/:user_id` route.
@@ -692,8 +715,6 @@ pub fn regenerate_token_and_send(req: &mut Request) -> CargoResult<Response> {
692715
use time;
693716
use self::tokens::dsl::email_id;
694717

695-
let mut body = String::new();
696-
req.body().read_to_string(&mut body)?;
697718
let user = req.user()?;
698719
let name = &req.params()["user_id"].parse::<i32>().ok().unwrap();
699720
let conn = req.db_conn()?;

0 commit comments

Comments
 (0)