|
2 | 2 |
|
3 | 3 | use crate::controllers::krate::CratePath;
|
4 | 4 | use crate::models::krate::OwnerRemoveError;
|
5 |
| -use crate::models::{krate::NewOwnerInvite, token::EndpointScope}; |
| 5 | +use crate::models::{ |
| 6 | + krate::NewOwnerInvite, token::EndpointScope, CrateOwner, NewCrateOwnerInvitation, |
| 7 | + NewCrateOwnerInvitationOutcome, OwnerKind, |
| 8 | +}; |
6 | 9 | use crate::models::{Crate, Owner, Rights, Team, User};
|
7 | 10 | use crate::util::errors::{bad_request, crate_not_found, custom, AppResult, BoxedAppError};
|
8 | 11 | use crate::views::EncodableOwner;
|
9 |
| -use crate::{app::AppState, models::krate::OwnerAddError}; |
| 12 | +use crate::{app::AppState, App}; |
10 | 13 | use crate::{auth::AuthCheck, email::Email};
|
11 | 14 | use axum::Json;
|
12 | 15 | use axum_extra::json;
|
13 | 16 | use axum_extra::response::ErasedJson;
|
| 17 | +use chrono::Utc; |
| 18 | +use crates_io_database::schema::crate_owners; |
14 | 19 | use diesel::prelude::*;
|
15 | 20 | use diesel_async::scoped_futures::ScopedFutureExt;
|
16 |
| -use diesel_async::{AsyncConnection, RunQueryDsl}; |
| 21 | +use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl}; |
17 | 22 | use http::request::Parts;
|
18 | 23 | use http::StatusCode;
|
19 | 24 | use secrecy::{ExposeSecret, SecretString};
|
| 25 | +use thiserror::Error; |
20 | 26 |
|
21 | 27 | /// List crate owners.
|
22 | 28 | #[utoipa::path(
|
@@ -200,7 +206,7 @@ async fn modify_owners(
|
200 | 206 | return Err(bad_request(format_args!("`{login}` is already an owner")));
|
201 | 207 | }
|
202 | 208 |
|
203 |
| - match krate.owner_add(&app, conn, user, login).await { |
| 209 | + match add_owner(&app, conn, user, &krate, login).await { |
204 | 210 | // A user was successfully invited, and they must accept
|
205 | 211 | // the invite, and a best-effort attempt should be made
|
206 | 212 | // to email them the invite token for one-click
|
@@ -275,6 +281,85 @@ async fn modify_owners(
|
275 | 281 | Ok(json!({ "msg": comma_sep_msg, "ok": true }))
|
276 | 282 | }
|
277 | 283 |
|
| 284 | +/// Invite `login` as an owner of this crate, returning the created |
| 285 | +/// [`NewOwnerInvite`]. |
| 286 | +async fn add_owner( |
| 287 | + app: &App, |
| 288 | + conn: &mut AsyncPgConnection, |
| 289 | + req_user: &User, |
| 290 | + krate: &Crate, |
| 291 | + login: &str, |
| 292 | +) -> Result<NewOwnerInvite, OwnerAddError> { |
| 293 | + use diesel::insert_into; |
| 294 | + |
| 295 | + let owner = Owner::find_or_create_by_login(app, conn, req_user, login).await?; |
| 296 | + match owner { |
| 297 | + // Users are invited and must accept before being added |
| 298 | + Owner::User(user) => { |
| 299 | + let expires_at = Utc::now() + app.config.ownership_invitations_expiration; |
| 300 | + let invite = NewCrateOwnerInvitation { |
| 301 | + invited_user_id: user.id, |
| 302 | + invited_by_user_id: req_user.id, |
| 303 | + crate_id: krate.id, |
| 304 | + expires_at, |
| 305 | + }; |
| 306 | + |
| 307 | + let creation_ret = invite.create(conn).await.map_err(BoxedAppError::from)?; |
| 308 | + |
| 309 | + match creation_ret { |
| 310 | + NewCrateOwnerInvitationOutcome::InviteCreated { plaintext_token } => { |
| 311 | + Ok(NewOwnerInvite::User(user, plaintext_token)) |
| 312 | + } |
| 313 | + NewCrateOwnerInvitationOutcome::AlreadyExists => { |
| 314 | + Err(OwnerAddError::AlreadyInvited(Box::new(user))) |
| 315 | + } |
| 316 | + } |
| 317 | + } |
| 318 | + // Teams are added as owners immediately |
| 319 | + Owner::Team(team) => { |
| 320 | + insert_into(crate_owners::table) |
| 321 | + .values(&CrateOwner { |
| 322 | + crate_id: krate.id, |
| 323 | + owner_id: team.id, |
| 324 | + created_by: req_user.id, |
| 325 | + owner_kind: OwnerKind::Team, |
| 326 | + email_notifications: true, |
| 327 | + }) |
| 328 | + .on_conflict(crate_owners::table.primary_key()) |
| 329 | + .do_update() |
| 330 | + .set(crate_owners::deleted.eq(false)) |
| 331 | + .execute(conn) |
| 332 | + .await |
| 333 | + .map_err(BoxedAppError::from)?; |
| 334 | + |
| 335 | + Ok(NewOwnerInvite::Team(team)) |
| 336 | + } |
| 337 | + } |
| 338 | +} |
| 339 | + |
| 340 | +/// Error results from a [`add_owner()`] model call. |
| 341 | +#[derive(Debug, Error)] |
| 342 | +enum OwnerAddError { |
| 343 | + /// An opaque [`BoxedAppError`]. |
| 344 | + #[error("{0}")] // AppError does not impl Error |
| 345 | + AppError(BoxedAppError), |
| 346 | + |
| 347 | + /// The requested invitee already has a pending invite. |
| 348 | + /// |
| 349 | + /// Note: Teams are always immediately added, so they cannot have a pending |
| 350 | + /// invite to cause this error. |
| 351 | + #[error("user already has pending invite")] |
| 352 | + AlreadyInvited(Box<User>), |
| 353 | +} |
| 354 | + |
| 355 | +/// A [`BoxedAppError`] does not impl [`std::error::Error`] so it needs a manual |
| 356 | +/// [`From`] impl. |
| 357 | +impl From<BoxedAppError> for OwnerAddError { |
| 358 | + fn from(value: BoxedAppError) -> Self { |
| 359 | + Self::AppError(value) |
| 360 | + } |
| 361 | +} |
| 362 | + |
278 | 363 | impl From<OwnerRemoveError> for BoxedAppError {
|
279 | 364 | fn from(error: OwnerRemoveError) -> Self {
|
280 | 365 | match error {
|
|
0 commit comments