Skip to content

Commit 5d10e3c

Browse files
authored
Merge pull request #10568 from Turbo87/new-user-cleanup
Simplify `NewUser` struct
2 parents 0ba3ecc + 99f92f3 commit 5d10e3c

File tree

13 files changed

+229
-211
lines changed

13 files changed

+229
-211
lines changed

src/controllers/session.rs

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ use axum::Json;
33
use axum_extra::json;
44
use axum_extra::response::ErasedJson;
55
use diesel::prelude::*;
6-
use diesel_async::{AsyncPgConnection, RunQueryDsl};
6+
use diesel_async::scoped_futures::ScopedFutureExt;
7+
use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
78
use http::request::Parts;
89
use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse};
910

1011
use crate::app::AppState;
12+
use crate::controllers::user::update::UserConfirmEmail;
1113
use crate::email::Emails;
1214
use crate::middleware::log_request::RequestLogExt;
13-
use crate::models::{NewUser, User};
15+
use crate::models::{NewEmail, NewUser, User};
1416
use crate::schema::users;
1517
use crate::util::diesel::is_read_only_error;
1618
use crate::util::errors::{bad_request, server_error, AppResult};
@@ -134,36 +136,70 @@ pub async fn authorize_session(
134136
super::user::me::get_authenticated_user(app, req).await
135137
}
136138

137-
async fn save_user_to_database(
139+
pub async fn save_user_to_database(
138140
user: &GithubUser,
139141
access_token: &str,
140142
emails: &Emails,
141143
conn: &mut AsyncPgConnection,
142-
) -> AppResult<User> {
143-
let new_user = NewUser::new(
144-
user.id,
145-
&user.login,
146-
user.name.as_deref(),
147-
user.avatar_url.as_deref(),
148-
access_token,
149-
);
150-
151-
match new_user
152-
.create_or_update(user.email.as_deref(), emails, conn)
153-
.await
154-
{
144+
) -> QueryResult<User> {
145+
let new_user = NewUser::builder()
146+
.gh_id(user.id)
147+
.gh_login(&user.login)
148+
.maybe_name(user.name.as_deref())
149+
.maybe_gh_avatar(user.avatar_url.as_deref())
150+
.gh_access_token(access_token)
151+
.build();
152+
153+
match create_or_update_user(&new_user, user.email.as_deref(), emails, conn).await {
155154
Ok(user) => Ok(user),
156155
Err(error) if is_read_only_error(&error) => {
157156
// If we're in read only mode, we can't update their details
158157
// just look for an existing user
159-
find_user_by_gh_id(conn, user.id)
160-
.await?
161-
.ok_or_else(|| error.into())
158+
find_user_by_gh_id(conn, user.id).await?.ok_or(error)
162159
}
163-
Err(error) => Err(error.into()),
160+
Err(error) => Err(error),
164161
}
165162
}
166163

164+
/// Inserts the user into the database, or updates an existing one.
165+
///
166+
/// This method also inserts the email address into the `emails` table
167+
/// and sends a confirmation email to the user.
168+
async fn create_or_update_user(
169+
new_user: &NewUser<'_>,
170+
email: Option<&str>,
171+
emails: &Emails,
172+
conn: &mut AsyncPgConnection,
173+
) -> QueryResult<User> {
174+
conn.transaction(|conn| {
175+
async move {
176+
let user = new_user.insert_or_update(conn).await?;
177+
178+
// To send the user an account verification email
179+
if let Some(user_email) = email {
180+
let new_email = NewEmail::builder()
181+
.user_id(user.id)
182+
.email(user_email)
183+
.build();
184+
185+
if let Some(token) = new_email.insert_if_missing(conn).await? {
186+
// Swallows any error. Some users might insert an invalid email address here.
187+
let email = UserConfirmEmail {
188+
user_name: &user.gh_login,
189+
domain: &emails.domain,
190+
token,
191+
};
192+
let _ = emails.send(user_email, email).await;
193+
}
194+
}
195+
196+
Ok(user)
197+
}
198+
.scope_boxed()
199+
})
200+
.await
201+
}
202+
167203
async fn find_user_by_gh_id(conn: &mut AsyncPgConnection, gh_id: i32) -> QueryResult<Option<User>> {
168204
users::table
169205
.filter(users::gh_id.eq(gh_id))

src/models/user.rs

Lines changed: 35 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
use bon::Builder;
12
use chrono::NaiveDateTime;
3+
use diesel::dsl::sql;
24
use diesel::prelude::*;
3-
use diesel_async::scoped_futures::ScopedFutureExt;
4-
use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
5+
use diesel::sql_types::Integer;
6+
use diesel::upsert::excluded;
7+
use diesel_async::{AsyncPgConnection, RunQueryDsl};
58

69
use crate::app::App;
7-
use crate::controllers::user::update::UserConfirmEmail;
8-
use crate::email::Emails;
910
use crate::util::errors::AppResult;
1011

11-
use crate::models::{Crate, CrateOwner, Email, NewEmail, Owner, OwnerKind, Rights};
12+
use crate::models::{Crate, CrateOwner, Email, Owner, OwnerKind, Rights};
1213
use crate::schema::{crate_owners, emails, users};
1314
use crates_io_diesel_helpers::lower;
1415

@@ -106,7 +107,7 @@ impl User {
106107
}
107108

108109
/// Represents a new user record insertable to the `users` table
109-
#[derive(Insertable, Debug, Default)]
110+
#[derive(Insertable, Debug, Builder)]
110111
#[diesel(table_name = users, check_for_backend(diesel::pg::Pg))]
111112
pub struct NewUser<'a> {
112113
pub gh_id: i32,
@@ -116,80 +117,36 @@ pub struct NewUser<'a> {
116117
pub gh_access_token: &'a str,
117118
}
118119

119-
impl<'a> NewUser<'a> {
120-
pub fn new(
121-
gh_id: i32,
122-
gh_login: &'a str,
123-
name: Option<&'a str>,
124-
gh_avatar: Option<&'a str>,
125-
gh_access_token: &'a str,
126-
) -> Self {
127-
NewUser {
128-
gh_id,
129-
gh_login,
130-
name,
131-
gh_avatar,
132-
gh_access_token,
133-
}
120+
impl NewUser<'_> {
121+
/// Inserts the user into the database, or fails if the user already exists.
122+
pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult<User> {
123+
diesel::insert_into(users::table)
124+
.values(self)
125+
.get_result(conn)
126+
.await
134127
}
135128

136129
/// Inserts the user into the database, or updates an existing one.
137-
pub async fn create_or_update(
138-
&self,
139-
email: Option<&'a str>,
140-
emails: &Emails,
141-
conn: &mut AsyncPgConnection,
142-
) -> QueryResult<User> {
143-
use diesel::dsl::sql;
144-
use diesel::insert_into;
145-
use diesel::pg::upsert::excluded;
146-
use diesel::sql_types::Integer;
147-
148-
conn.transaction(|conn| {
149-
async move {
150-
let user: User = insert_into(users::table)
151-
.values(self)
152-
// We need the `WHERE gh_id > 0` condition here because `gh_id` set
153-
// to `-1` indicates that we were unable to find a GitHub ID for
154-
// the associated GitHub login at the time that we backfilled
155-
// GitHub IDs. Therefore, there are multiple records in production
156-
// that have a `gh_id` of `-1` so we need to exclude those when
157-
// considering uniqueness of `gh_id` values. The `> 0` condition isn't
158-
// necessary for most fields in the database to be used as a conflict
159-
// target :)
160-
.on_conflict(sql::<Integer>("(gh_id) WHERE gh_id > 0"))
161-
.do_update()
162-
.set((
163-
users::gh_login.eq(excluded(users::gh_login)),
164-
users::name.eq(excluded(users::name)),
165-
users::gh_avatar.eq(excluded(users::gh_avatar)),
166-
users::gh_access_token.eq(excluded(users::gh_access_token)),
167-
))
168-
.get_result(conn)
169-
.await?;
170-
171-
// To send the user an account verification email
172-
if let Some(user_email) = email {
173-
let new_email = NewEmail::builder()
174-
.user_id(user.id)
175-
.email(user_email)
176-
.build();
177-
178-
if let Some(token) = new_email.insert_if_missing(conn).await? {
179-
// Swallows any error. Some users might insert an invalid email address here.
180-
let email = UserConfirmEmail {
181-
user_name: &user.gh_login,
182-
domain: &emails.domain,
183-
token,
184-
};
185-
let _ = emails.send(user_email, email).await;
186-
}
187-
}
188-
189-
Ok(user)
190-
}
191-
.scope_boxed()
192-
})
193-
.await
130+
pub async fn insert_or_update(&self, conn: &mut AsyncPgConnection) -> QueryResult<User> {
131+
diesel::insert_into(users::table)
132+
.values(self)
133+
// We need the `WHERE gh_id > 0` condition here because `gh_id` set
134+
// to `-1` indicates that we were unable to find a GitHub ID for
135+
// the associated GitHub login at the time that we backfilled
136+
// GitHub IDs. Therefore, there are multiple records in production
137+
// that have a `gh_id` of `-1` so we need to exclude those when
138+
// considering uniqueness of `gh_id` values. The `> 0` condition isn't
139+
// necessary for most fields in the database to be used as a conflict
140+
// target :)
141+
.on_conflict(sql::<Integer>("(gh_id) WHERE gh_id > 0"))
142+
.do_update()
143+
.set((
144+
users::gh_login.eq(excluded(users::gh_login)),
145+
users::name.eq(excluded(users::name)),
146+
users::gh_avatar.eq(excluded(users::gh_avatar)),
147+
users::gh_access_token.eq(excluded(users::gh_access_token)),
148+
))
149+
.get_result(conn)
150+
.await
194151
}
195152
}

src/rate_limiter.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ struct Bucket {
183183
#[cfg(test)]
184184
mod tests {
185185
use super::*;
186-
use crate::schema::users;
187186
use crates_io_test_db::TestDatabase;
188187

189188
#[tokio::test]
@@ -702,16 +701,14 @@ mod tests {
702701
async fn new_user(conn: &mut AsyncPgConnection, gh_login: &str) -> QueryResult<i32> {
703702
use crate::models::NewUser;
704703

705-
let user = NewUser {
706-
gh_login,
707-
..NewUser::default()
708-
};
709-
710-
diesel::insert_into(users::table)
711-
.values(user)
712-
.returning(users::id)
713-
.get_result(conn)
704+
NewUser::builder()
705+
.gh_id(0)
706+
.gh_login(gh_login)
707+
.gh_access_token("some random token")
708+
.build()
709+
.insert(conn)
714710
.await
711+
.map(|user| user.id)
715712
}
716713

717714
async fn new_user_bucket(

src/tests/mod.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,11 @@ pub struct OwnerResp {
9393
}
9494

9595
fn new_user(login: &str) -> NewUser<'_> {
96-
NewUser {
97-
gh_id: next_gh_id(),
98-
gh_login: login,
99-
name: None,
100-
gh_avatar: None,
101-
gh_access_token: "some random token",
102-
}
96+
NewUser::builder()
97+
.gh_id(next_gh_id())
98+
.gh_login(login)
99+
.gh_access_token("some random token")
100+
.build()
103101
}
104102

105103
fn new_team(login: &str) -> NewTeam<'_> {

src/tests/owners.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use crate::views::{
88
EncodableCrateOwnerInvitationV1, EncodableOwner, EncodablePublicUser, InvitationResponse,
99
};
1010

11-
use crate::schema::users;
1211
use chrono::Utc;
1312
use diesel::prelude::*;
1413
use diesel_async::RunQueryDsl;
@@ -770,17 +769,12 @@ async fn inactive_users_dont_get_invitations() {
770769
let invited_gh_login = "user_bar";
771770
let krate_name = "inactive_test";
772771

773-
let user = NewUser {
774-
gh_id: -1,
775-
gh_login: invited_gh_login,
776-
name: None,
777-
gh_avatar: None,
778-
gh_access_token: "some random token",
779-
};
780-
781-
diesel::insert_into(users::table)
782-
.values(user)
783-
.execute(&mut conn)
772+
NewUser::builder()
773+
.gh_id(-1)
774+
.gh_login(invited_gh_login)
775+
.gh_access_token("some random token")
776+
.build()
777+
.insert(&mut conn)
784778
.await
785779
.unwrap();
786780

src/tests/routes/crates/list.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::models::Category;
2-
use crate::schema::{crates, users};
2+
use crate::schema::crates;
33
use crate::tests::builders::{CrateBuilder, VersionBuilder};
44
use crate::tests::util::{RequestHelper, TestApp};
55
use crate::tests::{new_category, new_user};
@@ -22,11 +22,7 @@ async fn index() -> anyhow::Result<()> {
2222
assert_eq!(json.meta.total, 0);
2323
}
2424

25-
let user_id = insert_into(users::table)
26-
.values(new_user("foo"))
27-
.returning(users::id)
28-
.get_result(&mut conn)
29-
.await?;
25+
let user_id = new_user("foo").insert(&mut conn).await?.id;
3026

3127
let krate = CrateBuilder::new("fooindex", user_id)
3228
.expect_build(&mut conn)

src/tests/routes/me/email_notifications.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::schema::{crate_owners, users};
1+
use crate::schema::crate_owners;
22
use crate::tests::builders::CrateBuilder;
33
use crate::tests::new_user;
44
use crate::tests::util::{RequestHelper, TestApp};
@@ -115,12 +115,11 @@ async fn test_update_email_notifications_not_owned() {
115115
let (app, _, user) = TestApp::init().with_user().await;
116116
let mut conn = app.db_conn().await;
117117

118-
let user_id = diesel::insert_into(users::table)
119-
.values(new_user("arbitrary_username"))
120-
.returning(users::id)
121-
.get_result(&mut conn)
118+
let user_id = new_user("arbitrary_username")
119+
.insert(&mut conn)
122120
.await
123-
.unwrap();
121+
.unwrap()
122+
.id;
124123

125124
let not_my_crate = CrateBuilder::new("test_package", user_id)
126125
.expect_build(&mut conn)

0 commit comments

Comments
 (0)