Skip to content

Commit 8cd0ec5

Browse files
committed
controllers/krate/owners: Convert remaining endpoints to async/await
1 parent 05e3976 commit 8cd0ec5

File tree

11 files changed

+187
-172
lines changed

11 files changed

+187
-172
lines changed

src/controllers/krate/owners.rs

Lines changed: 116 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
use crate::models::{krate::NewOwnerInvite, token::EndpointScope};
44
use crate::models::{Crate, Owner, Rights, Team, User};
5-
use crate::tasks::spawn_blocking;
65
use crate::util::diesel::prelude::*;
76
use crate::util::errors::{bad_request, crate_not_found, custom, AppResult};
87
use crate::views::EncodableOwner;
@@ -12,11 +11,11 @@ use axum::extract::Path;
1211
use axum::Json;
1312
use axum_extra::json;
1413
use axum_extra::response::ErasedJson;
15-
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
14+
use diesel_async::scoped_futures::ScopedFutureExt;
15+
use diesel_async::AsyncConnection;
1616
use http::request::Parts;
1717
use http::StatusCode;
1818
use secrecy::{ExposeSecret, SecretString};
19-
use tokio::runtime::Handle;
2019

2120
/// Handles the `GET /crates/:crate_id/owners` route.
2221
pub async fn owners(state: AppState, Path(crate_name): Path<String>) -> AppResult<ErasedJson> {
@@ -62,25 +61,23 @@ pub async fn owner_team(state: AppState, Path(crate_name): Path<String>) -> AppR
6261

6362
/// Handles the `GET /crates/:crate_id/owner_user` route.
6463
pub async fn owner_user(state: AppState, Path(crate_name): Path<String>) -> AppResult<ErasedJson> {
65-
let conn = state.db_read().await?;
66-
spawn_blocking(move || {
67-
use diesel::RunQueryDsl;
64+
use diesel_async::RunQueryDsl;
6865

69-
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
66+
let mut conn = state.db_read().await?;
7067

71-
let krate: Crate = Crate::by_name(&crate_name)
72-
.first(conn)
73-
.optional()?
74-
.ok_or_else(|| crate_not_found(&crate_name))?;
68+
let krate: Crate = Crate::by_name(&crate_name)
69+
.first(&mut conn)
70+
.await
71+
.optional()?
72+
.ok_or_else(|| crate_not_found(&crate_name))?;
7573

76-
let owners = User::owning(&krate, conn)?
77-
.into_iter()
78-
.map(Owner::into)
79-
.collect::<Vec<EncodableOwner>>();
74+
let owners = User::owning(&krate, &mut conn)
75+
.await?
76+
.into_iter()
77+
.map(Owner::into)
78+
.collect::<Vec<EncodableOwner>>();
8079

81-
Ok(json!({ "users": owners }))
82-
})
83-
.await?
80+
Ok(json!({ "users": owners }))
8481
}
8582

8683
/// Handles the `PUT /crates/:crate_id/owners` route.
@@ -116,6 +113,8 @@ async fn modify_owners(
116113
body: ChangeOwnersRequest,
117114
add: bool,
118115
) -> AppResult<ErasedJson> {
116+
use diesel_async::RunQueryDsl;
117+
119118
let logins = body.owners;
120119

121120
// Bound the number of invites processed per request to limit the cost of
@@ -132,121 +131,124 @@ async fn modify_owners(
132131
.for_crate(&crate_name)
133132
.check(&parts, &mut conn)
134133
.await?;
135-
spawn_blocking(move || {
136-
use diesel::RunQueryDsl;
137-
138-
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
139-
140-
let user = auth.user();
141-
142-
// The set of emails to send out after invite processing is complete and
143-
// the database transaction has committed.
144-
let mut emails = Vec::with_capacity(logins.len());
145-
146-
let comma_sep_msg = conn.transaction(|conn| {
147-
let krate: Crate = Crate::by_name(&crate_name)
148-
.first(conn)
149-
.optional()?
150-
.ok_or_else(|| crate_not_found(&crate_name))?;
151-
152-
let owners = krate.owners(conn)?;
153134

154-
match Handle::current().block_on(user.rights(&app, &owners))? {
155-
Rights::Full => {}
156-
// Yes!
157-
Rights::Publish => {
158-
return Err(custom(
159-
StatusCode::FORBIDDEN,
160-
"team members don't have permission to modify owners",
161-
));
162-
}
163-
Rights::None => {
164-
return Err(custom(
165-
StatusCode::FORBIDDEN,
166-
"only owners have permission to modify owners",
167-
));
135+
let user = auth.user();
136+
137+
let (comma_sep_msg, emails) = conn
138+
.transaction(|conn| {
139+
let app = app.clone();
140+
async move {
141+
let krate: Crate = Crate::by_name(&crate_name)
142+
.first(conn)
143+
.await
144+
.optional()?
145+
.ok_or_else(|| crate_not_found(&crate_name))?;
146+
147+
let owners = krate.async_owners(conn).await?;
148+
149+
match user.rights(&app, &owners).await? {
150+
Rights::Full => {}
151+
// Yes!
152+
Rights::Publish => {
153+
return Err(custom(
154+
StatusCode::FORBIDDEN,
155+
"team members don't have permission to modify owners",
156+
));
157+
}
158+
Rights::None => {
159+
return Err(custom(
160+
StatusCode::FORBIDDEN,
161+
"only owners have permission to modify owners",
162+
));
163+
}
168164
}
169-
}
170165

171-
let comma_sep_msg = if add {
172-
let mut msgs = Vec::with_capacity(logins.len());
173-
for login in &logins {
174-
let login_test =
175-
|owner: &Owner| owner.login().to_lowercase() == *login.to_lowercase();
176-
if owners.iter().any(login_test) {
177-
return Err(bad_request(format_args!("`{login}` is already an owner")));
178-
}
166+
// The set of emails to send out after invite processing is complete and
167+
// the database transaction has committed.
168+
let mut emails = Vec::with_capacity(logins.len());
169+
170+
let comma_sep_msg = if add {
171+
let mut msgs = Vec::with_capacity(logins.len());
172+
for login in &logins {
173+
let login_test =
174+
|owner: &Owner| owner.login().to_lowercase() == *login.to_lowercase();
175+
if owners.iter().any(login_test) {
176+
return Err(bad_request(format_args!("`{login}` is already an owner")));
177+
}
179178

180-
match krate.owner_add(&app, conn, user, login) {
181-
// A user was successfully invited, and they must accept
182-
// the invite, and a best-effort attempt should be made
183-
// to email them the invite token for one-click
184-
// acceptance.
185-
Ok(NewOwnerInvite::User(invitee, token)) => {
186-
msgs.push(format!(
187-
"user {} has been invited to be an owner of crate {}",
188-
invitee.gh_login, krate.name,
189-
));
190-
191-
if let Some(recipient) = invitee.verified_email(conn).ok().flatten() {
192-
emails.push(OwnerInviteEmail {
193-
recipient_email_address: recipient,
194-
inviter: user.gh_login.clone(),
195-
domain: app.emails.domain.clone(),
196-
crate_name: krate.name.clone(),
197-
token,
198-
});
179+
match krate.owner_add(&app, conn, user, login).await {
180+
// A user was successfully invited, and they must accept
181+
// the invite, and a best-effort attempt should be made
182+
// to email them the invite token for one-click
183+
// acceptance.
184+
Ok(NewOwnerInvite::User(invitee, token)) => {
185+
msgs.push(format!(
186+
"user {} has been invited to be an owner of crate {}",
187+
invitee.gh_login, krate.name,
188+
));
189+
190+
if let Some(recipient) =
191+
invitee.async_verified_email(conn).await.ok().flatten()
192+
{
193+
emails.push(OwnerInviteEmail {
194+
recipient_email_address: recipient,
195+
inviter: user.gh_login.clone(),
196+
domain: app.emails.domain.clone(),
197+
crate_name: krate.name.clone(),
198+
token,
199+
});
200+
}
199201
}
200-
}
201202

202-
// A team was successfully invited. They are immediately
203-
// added, and do not have an invite token.
204-
Ok(NewOwnerInvite::Team(team)) => msgs.push(format!(
205-
"team {} has been added as an owner of crate {}",
206-
team.login, krate.name
207-
)),
203+
// A team was successfully invited. They are immediately
204+
// added, and do not have an invite token.
205+
Ok(NewOwnerInvite::Team(team)) => msgs.push(format!(
206+
"team {} has been added as an owner of crate {}",
207+
team.login, krate.name
208+
)),
208209

209-
// This user has a pending invite.
210-
Err(OwnerAddError::AlreadyInvited(user)) => msgs.push(format!(
210+
// This user has a pending invite.
211+
Err(OwnerAddError::AlreadyInvited(user)) => msgs.push(format!(
211212
"user {} already has a pending invitation to be an owner of crate {}",
212213
user.gh_login, krate.name
213214
)),
214215

215-
// An opaque error occurred.
216-
Err(OwnerAddError::AppError(e)) => return Err(e),
216+
// An opaque error occurred.
217+
Err(OwnerAddError::AppError(e)) => return Err(e),
218+
}
217219
}
218-
}
219-
msgs.join(",")
220-
} else {
221-
for login in &logins {
222-
krate.owner_remove(conn, login)?;
223-
}
224-
if User::owning(&krate, conn)?.is_empty() {
225-
return Err(bad_request(
226-
"cannot remove all individual owners of a crate. \
220+
msgs.join(",")
221+
} else {
222+
for login in &logins {
223+
krate.owner_remove(conn, login).await?;
224+
}
225+
if User::owning(&krate, conn).await?.is_empty() {
226+
return Err(bad_request(
227+
"cannot remove all individual owners of a crate. \
227228
Team member don't have permission to modify owners, so \
228229
at least one individual owner is required.",
229-
));
230-
}
231-
"owners successfully removed".to_owned()
232-
};
230+
));
231+
}
232+
"owners successfully removed".to_owned()
233+
};
233234

234-
Ok(comma_sep_msg)
235-
})?;
235+
Ok((comma_sep_msg, emails))
236+
}
237+
.scope_boxed()
238+
})
239+
.await?;
236240

237-
// Send the accumulated invite emails now the database state has
238-
// committed.
239-
for email in emails {
240-
let addr = email.recipient_email_address().to_string();
241+
// Send the accumulated invite emails now the database state has
242+
// committed.
243+
for email in emails {
244+
let addr = email.recipient_email_address().to_string();
241245

242-
if let Err(e) = app.emails.send(&addr, email) {
243-
warn!("Failed to send co-owner invite email: {e}");
244-
}
246+
if let Err(e) = app.emails.async_send(&addr, email).await {
247+
warn!("Failed to send co-owner invite email: {e}");
245248
}
249+
}
246250

247-
Ok(json!({ "msg": comma_sep_msg, "ok": true }))
248-
})
249-
.await?
251+
Ok(json!({ "msg": comma_sep_msg, "ok": true }))
250252
}
251253

252254
pub struct OwnerInviteEmail {

src/models/crate_owner_invitation.rs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use chrono::{NaiveDateTime, Utc};
2-
use diesel_async::AsyncPgConnection;
2+
use diesel_async::scoped_futures::ScopedFutureExt;
3+
use diesel_async::{AsyncConnection, AsyncPgConnection};
34
use http::StatusCode;
45
use secrecy::SecretString;
56

67
use crate::config;
78
use crate::models::{CrateOwner, OwnerKind};
89
use crate::schema::{crate_owner_invitations, crate_owners, crates};
910
use crate::util::diesel::prelude::*;
10-
use crate::util::diesel::Conn;
1111
use crate::util::errors::{custom, AppResult};
1212

1313
#[derive(Debug)]
@@ -30,14 +30,14 @@ pub struct CrateOwnerInvitation {
3030
}
3131

3232
impl CrateOwnerInvitation {
33-
pub fn create(
33+
pub async fn create(
3434
invited_user_id: i32,
3535
invited_by_user_id: i32,
3636
crate_id: i32,
37-
conn: &mut impl Conn,
37+
conn: &mut AsyncPgConnection,
3838
config: &config::Server,
3939
) -> QueryResult<NewCrateOwnerInvitationOutcome> {
40-
use diesel::RunQueryDsl;
40+
use diesel_async::RunQueryDsl;
4141

4242
#[derive(Insertable, Clone, Copy, Debug)]
4343
#[diesel(table_name = crate_owner_invitations, check_for_backend(diesel::pg::Pg))]
@@ -50,22 +50,27 @@ impl CrateOwnerInvitation {
5050
// Before actually creating the invite, check if an expired invitation already exists
5151
// and delete it from the database. This allows obtaining a new invite if the old one
5252
// expired, instead of returning "already exists".
53-
conn.transaction(|conn| -> QueryResult<()> {
54-
// This does a SELECT FOR UPDATE + DELETE instead of a DELETE with a WHERE clause to
55-
// use the model's `is_expired` method, centralizing our expiration checking logic.
56-
let existing: Option<CrateOwnerInvitation> = crate_owner_invitations::table
57-
.find((invited_user_id, crate_id))
58-
.for_update()
59-
.first(conn)
60-
.optional()?;
61-
62-
if let Some(existing) = existing {
63-
if existing.is_expired(config) {
64-
diesel::delete(&existing).execute(conn)?;
53+
conn.transaction(|conn| {
54+
async move {
55+
// This does a SELECT FOR UPDATE + DELETE instead of a DELETE with a WHERE clause to
56+
// use the model's `is_expired` method, centralizing our expiration checking logic.
57+
let existing: Option<CrateOwnerInvitation> = crate_owner_invitations::table
58+
.find((invited_user_id, crate_id))
59+
.for_update()
60+
.first(conn)
61+
.await
62+
.optional()?;
63+
64+
if let Some(existing) = existing {
65+
if existing.is_expired(config) {
66+
diesel::delete(&existing).execute(conn).await?;
67+
}
6568
}
69+
QueryResult::Ok(())
6670
}
67-
Ok(())
68-
})?;
71+
.scope_boxed()
72+
})
73+
.await?;
6974

7075
let res: Option<CrateOwnerInvitation> = diesel::insert_into(crate_owner_invitations::table)
7176
.values(&NewRecord {
@@ -78,6 +83,7 @@ impl CrateOwnerInvitation {
7883
// deleted before doing this INSERT.
7984
.on_conflict_do_nothing()
8085
.get_result(conn)
86+
.await
8187
.optional()?;
8288

8389
Ok(match res {

0 commit comments

Comments
 (0)