Skip to content

Drop Email column (merge and deploy the fix for #1888 first!) #1891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 2, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion migrations/20140924113530_dumped_migration_1/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ CREATE TABLE users (
email VARCHAR NOT NULL UNIQUE,
gh_access_token VARCHAR NOT NULL,
api_token VARCHAR NOT NULL
);
);

2 changes: 1 addition & 1 deletion migrations/20161115111957_dumped_migration_119/up.sql
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CREATE FUNCTION update_categories_crates_cnt() RETURNS trigger AS $$ BEGIN IF (TG_OP = 'INSERT') THEN UPDATE categories SET crates_cnt = crates_cnt + 1 WHERE id = NEW.category_id; return NEW; ELSIF (TG_OP = 'DELETE') THEN UPDATE categories SET crates_cnt = crates_cnt - 1 WHERE id = OLD.category_id; return OLD; END IF; END $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_categories_crates_cnt BEFORE INSERT OR DELETE ON crates_categories FOR EACH ROW EXECUTE PROCEDURE update_categories_crates_cnt(); CREATE TRIGGER touch_crate_on_modify_categories AFTER INSERT OR DELETE ON crates_categories FOR EACH ROW EXECUTE PROCEDURE touch_crate();
CREATE FUNCTION update_categories_crates_cnt() RETURNS trigger AS $$ BEGIN IF (TG_OP = 'INSERT') THEN UPDATE categories SET crates_cnt = crates_cnt + 1 WHERE id = NEW.category_id; return NEW; ELSIF (TG_OP = 'DELETE') THEN UPDATE categories SET crates_cnt = crates_cnt - 1 WHERE id = OLD.category_id; return OLD; END IF; END $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_categories_crates_cnt BEFORE INSERT OR DELETE ON crates_categories FOR EACH ROW EXECUTE PROCEDURE update_categories_crates_cnt(); CREATE TRIGGER touch_crate_on_modify_categories AFTER INSERT OR DELETE ON crates_categories FOR EACH ROW EXECUTE PROCEDURE touch_crate();
1 change: 1 addition & 0 deletions migrations/2019-11-11-162609_drop_email_from_user/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- Not reversible
2 changes: 2 additions & 0 deletions migrations/2019-11-11-162609_drop_email_from_user/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Your SQL goes here
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this comment :)

ALTER TABLE users DROP COLUMN email CASCADE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super picky, but can you remove the extra space between TABLE and users?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

11 changes: 3 additions & 8 deletions src/controllers/user/me.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ pub fn me(req: &mut dyn Request) -> CargoResult<Response> {

let verified = verified.unwrap_or(false);
let verification_sent = verified || verification_sent;
let user = User { email, ..user };
let user = User { ..user };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line can be completely removed now, actually! I think user = User { ..user } is doing a no-op.


Ok(req.json(&EncodableMe {
user: user.encodable_private(verified, verification_sent),
user: user.encodable_private(email, verified, verification_sent)?,
}))
}

Expand Down Expand Up @@ -91,8 +91,7 @@ pub fn updates(req: &mut dyn Request) -> CargoResult<Response> {
/// Handles the `PUT /user/:user_id` route.
pub fn update_user(req: &mut dyn Request) -> CargoResult<Response> {
use self::emails::user_id;
use self::users::dsl::{email, gh_login, users};
use diesel::{insert_into, update};
use diesel::insert_into;

let mut body = String::new();
req.body().read_to_string(&mut body)?;
Expand Down Expand Up @@ -130,10 +129,6 @@ pub fn update_user(req: &mut dyn Request) -> CargoResult<Response> {
}

conn.transaction::<_, Box<dyn CargoError>, _>(|| {
update(users.filter(gh_login.eq(&user.gh_login)))
.set(email.eq(user_email))
.execute(&*conn)?;

let new_email = NewEmail {
user_id: user.id,
email: user_email,
Expand Down
3 changes: 1 addition & 2 deletions src/controllers/user/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,11 @@ impl GithubUser {
NewUser::new(
self.id,
&self.login,
self.email.as_ref().map(|s| &s[..]),
self.name.as_ref().map(|s| &s[..]),
self.avatar_url.as_ref().map(|s| &s[..]),
access_token,
)
.create_or_update(conn)
.create_or_update(self.email.as_ref().map(|s| &s[..]), conn)
.map_err(Into::into)
.or_else(|e: Box<dyn CargoError>| {
// If we're in read only mode, we can't update their details
Expand Down
38 changes: 27 additions & 11 deletions src/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,19 @@ use crate::views::{EncodablePrivateUser, EncodablePublicUser};
#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, AsChangeset, Associations)]
pub struct User {
pub id: i32,
pub email: Option<String>,
pub gh_access_token: String,
pub gh_login: String,
pub name: Option<String>,
pub gh_avatar: Option<String>,
pub gh_id: i32,
}

/// Represents a new user record insertible to the `users` table
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small fix: can you change insertible to insertable in this comment please?

#[derive(Insertable, Debug, Default)]
#[table_name = "users"]
pub struct NewUser<'a> {
pub gh_id: i32,
pub gh_login: &'a str,
pub email: Option<&'a str>,
pub name: Option<&'a str>,
pub gh_avatar: Option<&'a str>,
pub gh_access_token: Cow<'a, str>,
Expand All @@ -36,23 +35,26 @@ impl<'a> NewUser<'a> {
pub fn new(
gh_id: i32,
gh_login: &'a str,
email: Option<&'a str>,
name: Option<&'a str>,
gh_avatar: Option<&'a str>,
gh_access_token: &'a str,
) -> Self {
NewUser {
gh_id,
gh_login,
email,
name,
gh_avatar,
gh_access_token: Cow::Borrowed(gh_access_token),
}
}

/// Inserts the user into the database, or updates an existing one.
pub fn create_or_update(&self, conn: &PgConnection) -> QueryResult<User> {
pub fn create_or_update(
&self,
email: Option<&'a str>,
conn: &PgConnection,
) -> QueryResult<User> {
use crate::schema::emails::columns::user_id;
use crate::schema::users::dsl::*;
use diesel::dsl::sql;
use diesel::insert_into;
Expand Down Expand Up @@ -81,15 +83,17 @@ impl<'a> NewUser<'a> {
.get_result::<User>(conn)?;

// To send the user an account verification email...
if let Some(user_email) = user.email.as_ref() {
if let Some(user_email) = email {
let new_email = NewEmail {
user_id: user.id,
email: user_email,
};

let token = insert_into(emails::table)
.values(&new_email)
.on_conflict_do_nothing()
.on_conflict(user_id)
.do_update()
.set(&new_email)
.returning(emails::token)
.get_result::<String>(conn)
.optional()?;
Expand Down Expand Up @@ -168,6 +172,8 @@ impl User {
Ok(best)
}

/// Queries the database for the verified emails
/// belonging to a given user
pub fn verified_email(&self, conn: &PgConnection) -> CargoResult<Option<String>> {
Ok(Email::belonging_to(self)
.select(emails::email)
Expand All @@ -179,19 +185,21 @@ impl User {
/// Converts this `User` model into an `EncodablePrivateUser` for JSON serialization.
pub fn encodable_private(
self,
email: Option<String>,

email_verified: bool,
email_verification_sent: bool,
) -> EncodablePrivateUser {
) -> CargoResult<EncodablePrivateUser> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this function needed to be changed to return a Result? It doesn't look like it's necessary to me because I don't see anywhere returning an Err variant. Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't necessary. I was previously reading this email variable from the database with an Err

if let Some(user_email) = email {

variant result before realizing that was already "done" and the email could simply be passed to the src/models/users/encodable_private function as an argument.
I definitely forgot to undo that modification to the return type - will do so now

let User {
id,
email,
name,
gh_login,
gh_avatar,
..
} = self;
let url = format!("https://github.com/{}", gh_login);
EncodablePrivateUser {

Ok(EncodablePrivateUser {
id,
email,
email_verified,
Expand All @@ -200,7 +208,15 @@ impl User {
login: gh_login,
name,
url: Some(url),
}
})
}

/// Queries for the email belonging to a particular user
pub fn email(&self, conn: &PgConnection) -> CargoResult<Option<String>> {
Ok(Email::belonging_to(self)
.select(emails::email)
.first::<String>(&*conn)
.optional()?)
}

/// Converts this`User` model into an `EncodablePublicUser` for JSON serialization.
Expand Down
2 changes: 1 addition & 1 deletion src/publish_rate_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ mod tests {
gh_login,
..NewUser::default()
}
.create_or_update(conn)?;
.create_or_update(None, conn)?;
Ok(user.id)
}

Expand Down
6 changes: 0 additions & 6 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,12 +745,6 @@ table! {
///
/// (Automatically generated by Diesel.)
id -> Int4,
/// The `email` column of the `users` table.
///
/// Its SQL type is `Nullable<Varchar>`.
///
/// (Automatically generated by Diesel.)
email -> Nullable<Varchar>,
/// The `gh_access_token` column of the `users` table.
///
/// Its SQL type is `Varchar`.
Expand Down
1 change: 0 additions & 1 deletion src/tasks/dump_db/dump-db.toml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ id in (
)"""
[users.columns]
id = "public"
email = "private"
gh_access_token = "private"
gh_login = "public"
name = "public"
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/update_downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ mod test {
}

fn user(conn: &PgConnection) -> User {
NewUser::new(2, "login", None, None, None, "access_token")
.create_or_update(conn)
NewUser::new(2, "login", None, None, "access_token")
.create_or_update(None, conn)
.unwrap()
}

Expand Down
1 change: 0 additions & 1 deletion src/tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ fn new_user(login: &str) -> NewUser<'_> {
NewUser {
gh_id: NEXT_GH_ID.fetch_add(1, Ordering::SeqCst) as i32,
gh_login: login,
email: None,
name: None,
gh_avatar: None,
gh_access_token: Cow::Borrowed("some random token"),
Expand Down
2 changes: 1 addition & 1 deletion src/tests/krate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ fn index() {
assert_eq!(json.meta.total, 0);

let krate = app.db(|conn| {
let u = new_user("foo").create_or_update(conn).unwrap();
let u = new_user("foo").create_or_update(None, conn).unwrap();
CrateBuilder::new("fooindex", u.id).expect_build(conn)
});

Expand Down
2 changes: 1 addition & 1 deletion src/tests/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ fn token_gives_access_to_me() {
anon.get(url).assert_forbidden();

let json: UserShowPrivateResponse = token.get(url).good();
assert_eq!(json.user.email, user.as_model().email);
assert_eq!(json.user.name, user.as_model().name);
}

#[test]
Expand Down
34 changes: 18 additions & 16 deletions src/tests/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ fn me() {
let user = app.db_new_user("foo");
let json = user.show_me();

assert_eq!(json.user.email, user.as_model().email);
assert_eq!(json.user.name, user.as_model().name);
}

#[test]
Expand Down Expand Up @@ -141,21 +141,19 @@ fn show_latest_user_case_insensitively() {
t!(NewUser::new(
1,
"foobar",
Some("[email protected]"),
Some("I was first then deleted my github account"),
None,
"bar"
)
.create_or_update(conn));
.create_or_update(None, conn));
t!(NewUser::new(
2,
"FOOBAR",
Some("[email protected]"),
Some("I was second, I took the foobar username on github"),
None,
"bar"
)
.create_or_update(conn));
.create_or_update(None, conn));
});

let json: UserShowPublicResponse = anon.get("api/v1/users/fOObAr").good();
Expand Down Expand Up @@ -324,7 +322,7 @@ fn updating_existing_user_doesnt_change_api_token() {

let user = app.db(|conn| {
// Reuse gh_id but use new gh_login and gh_access_token
t!(NewUser::new(gh_id, "bar", None, None, None, "bar_token").create_or_update(conn));
t!(NewUser::new(gh_id, "bar", None, None, "bar_token").create_or_update(None, conn));

// Use the original API token to find the now updated user
t!(User::find_by_api_token(conn, token))
Expand Down Expand Up @@ -353,7 +351,7 @@ fn github_without_email_does_not_overwrite_email() {
// Don't use app.db_new_user because it adds a verified email.
let user_without_github_email = app.db(|conn| {
let u = new_user("arbitrary_username");
let u = u.create_or_update(conn).unwrap();
let u = u.create_or_update(None, conn).unwrap();
MockCookieUser::new(&app, u)
});
let user_without_github_email_model = user_without_github_email.as_model();
Expand All @@ -373,7 +371,7 @@ fn github_without_email_does_not_overwrite_email() {
// new_user uses a None email; the rest of the fields are arbitrary
..new_user("arbitrary_username")
};
let u = u.create_or_update(conn).unwrap();
let u = u.create_or_update(None, conn).unwrap();
MockCookieUser::new(&app, u)
});

Expand All @@ -386,9 +384,16 @@ fn github_without_email_does_not_overwrite_email() {
*/
#[test]
fn github_with_email_does_not_overwrite_email() {
use cargo_registry::schema::emails;

let (app, _, user) = TestApp::init().with_user();
let model = user.as_model();
let original_email = &model.email;
let original_email = app.db(|conn| {
Email::belonging_to(model)
.select(emails::email)
.first::<String>(&*conn)
.unwrap()
});

let new_github_email = "[email protected]";

Expand All @@ -397,16 +402,15 @@ fn github_with_email_does_not_overwrite_email() {
let u = NewUser {
// Use the same github ID to link to the existing account
gh_id: model.gh_id,
email: Some(new_github_email),
// the rest of the fields are arbitrary
..new_user("arbitrary_username")
};
let u = u.create_or_update(conn).unwrap();
let u = u.create_or_update(Some(new_github_email), conn).unwrap();
MockCookieUser::new(&app, u)
});

let json = user_with_different_email_in_github.show_me();
assert_eq!(json.user.email, *original_email);
assert_eq!(json.user.email, Some(original_email));
}

/* Given a crates.io user, check that the user's email can be
Expand Down Expand Up @@ -512,10 +516,9 @@ fn test_confirm_user_email() {
// email directly into the database and we want to test the verification flow here.
let user = app.db(|conn| {
let u = NewUser {
email: Some("[email protected]"),
..new_user("arbitrary_username")
};
let u = u.create_or_update(conn).unwrap();
let u = u.create_or_update(None, conn).unwrap();
MockCookieUser::new(&app, u)
});
let user_model = user.as_model();
Expand Down Expand Up @@ -551,10 +554,9 @@ fn test_existing_user_email() {
// email directly into the database and we want to test the verification flow here.
let user = app.db(|conn| {
let u = NewUser {
email: Some("[email protected]"),
..new_user("arbitrary_username")
};
let u = u.create_or_update(conn).unwrap();
let u = u.create_or_update(None, conn).unwrap();
update(Email::belonging_to(&u))
// Users created before we added verification will have
// `NULL` in the `token_generated_at` column.
Expand Down
7 changes: 4 additions & 3 deletions src/tests/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,10 @@ impl TestApp {
use diesel::prelude::*;

let user = self.db(|conn| {
let mut user = crate::new_user(username).create_or_update(conn).unwrap();
let email = "[email protected]";
user.email = Some(email.to_string());
let email = "[email protected]";
let user = crate::new_user(username)
.create_or_update(Some(email), conn)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, so the unique constraint that's causing the tests to fail is that here in the test setup you're creating a new user with an email set, then lines 142-149 right after this the test setup is inserting the email into the table directly, which violates the constraint we have that each user may only have one email.

To fix this, you can pass None to create_or_update here and the next part will take care of inserting the email (and setting it to verified, which is important for the tests).

.unwrap();
diesel::insert_into(emails::table)
.values((
emails::user_id.eq(user.id),
Expand Down
Loading