From 4b136d872b0d17a7604144d2850942335ebfd6ec Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:34:13 +0100 Subject: [PATCH 1/8] controllers/krate/metadata: Move `list_reverse_dependencies()` to dedicated module --- src/controllers/krate.rs | 1 + src/controllers/krate/metadata.rs | 69 ++--------------------------- src/controllers/krate/rev_deps.rs | 72 +++++++++++++++++++++++++++++++ src/router.rs | 2 +- 4 files changed, 77 insertions(+), 67 deletions(-) create mode 100644 src/controllers/krate/rev_deps.rs diff --git a/src/controllers/krate.rs b/src/controllers/krate.rs index cfdfadee18..6735169e22 100644 --- a/src/controllers/krate.rs +++ b/src/controllers/krate.rs @@ -12,6 +12,7 @@ pub mod follow; pub mod metadata; pub mod owners; pub mod publish; +pub mod rev_deps; pub mod search; pub mod versions; diff --git a/src/controllers/krate/metadata.rs b/src/controllers/krate/metadata.rs index 6321b6e609..f7d2ed5e1c 100644 --- a/src/controllers/krate/metadata.rs +++ b/src/controllers/krate/metadata.rs @@ -5,19 +5,16 @@ //! `Cargo.toml` file. use crate::app::AppState; -use crate::controllers::helpers::pagination::PaginationOptions; use crate::controllers::krate::CratePath; use crate::controllers::version::CrateVersionPath; use crate::models::{ - Category, Crate, CrateCategory, CrateKeyword, CrateName, Keyword, RecentCrateDownloads, User, - Version, VersionOwnerAction, + Category, Crate, CrateCategory, CrateKeyword, Keyword, RecentCrateDownloads, User, Version, + VersionOwnerAction, }; use crate::schema::*; use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError}; use crate::util::{redirect, RequestUtils}; -use crate::views::{ - EncodableCategory, EncodableCrate, EncodableDependency, EncodableKeyword, EncodableVersion, -}; +use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword, EncodableVersion}; use axum::response::{IntoResponse, Response}; use axum_extra::json; use axum_extra::response::ErasedJson; @@ -261,63 +258,3 @@ pub async fn get_version_readme(app: AppState, path: CrateVersionPath, req: Part redirect(redirect_url) } } - -/// List reverse dependencies of a crate. -#[utoipa::path( - get, - path = "/api/v1/crates/{name}/reverse_dependencies", - params(CratePath), - tag = "crates", - responses((status = 200, description = "Successful Response")), -)] -pub async fn list_reverse_dependencies( - app: AppState, - path: CratePath, - req: Parts, -) -> AppResult { - let mut conn = app.db_read().await?; - - let pagination_options = PaginationOptions::builder().gather(&req)?; - - let krate = path.load_crate(&mut conn).await?; - - let (rev_deps, total) = krate - .reverse_dependencies(&mut conn, pagination_options) - .await?; - - let rev_deps: Vec<_> = rev_deps - .into_iter() - .map(|dep| EncodableDependency::from_reverse_dep(dep, &krate.name)) - .collect(); - - let version_ids: Vec = rev_deps.iter().map(|dep| dep.version_id).collect(); - - let versions_and_publishers: Vec<(Version, CrateName, Option)> = versions::table - .filter(versions::id.eq_any(version_ids)) - .inner_join(crates::table) - .left_outer_join(users::table) - .select(<(Version, CrateName, Option)>::as_select()) - .load(&mut conn) - .await?; - - let versions = versions_and_publishers - .iter() - .map(|(v, ..)| v) - .collect::>(); - - let actions = VersionOwnerAction::for_versions(&mut conn, &versions).await?; - - let versions = versions_and_publishers - .into_iter() - .zip(actions) - .map(|((version, krate_name, published_by), actions)| { - EncodableVersion::from(version, &krate_name.name, published_by, actions) - }) - .collect::>(); - - Ok(json!({ - "dependencies": rev_deps, - "versions": versions, - "meta": { "total": total }, - })) -} diff --git a/src/controllers/krate/rev_deps.rs b/src/controllers/krate/rev_deps.rs new file mode 100644 index 0000000000..b4ee3e3a88 --- /dev/null +++ b/src/controllers/krate/rev_deps.rs @@ -0,0 +1,72 @@ +use crate::app::AppState; +use crate::controllers::helpers::pagination::PaginationOptions; +use crate::controllers::krate::CratePath; +use crate::models::{CrateName, User, Version, VersionOwnerAction}; +use crate::util::errors::AppResult; +use crate::views::{EncodableDependency, EncodableVersion}; +use axum_extra::json; +use axum_extra::response::ErasedJson; +use crates_io_database::schema::{crates, users, versions}; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use http::request::Parts; + +/// List reverse dependencies of a crate. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/reverse_dependencies", + params(CratePath), + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] +pub async fn list_reverse_dependencies( + app: AppState, + path: CratePath, + req: Parts, +) -> AppResult { + let mut conn = app.db_read().await?; + + let pagination_options = PaginationOptions::builder().gather(&req)?; + + let krate = path.load_crate(&mut conn).await?; + + let (rev_deps, total) = krate + .reverse_dependencies(&mut conn, pagination_options) + .await?; + + let rev_deps: Vec<_> = rev_deps + .into_iter() + .map(|dep| EncodableDependency::from_reverse_dep(dep, &krate.name)) + .collect(); + + let version_ids: Vec = rev_deps.iter().map(|dep| dep.version_id).collect(); + + let versions_and_publishers: Vec<(Version, CrateName, Option)> = versions::table + .filter(versions::id.eq_any(version_ids)) + .inner_join(crates::table) + .left_outer_join(users::table) + .select(<(Version, CrateName, Option)>::as_select()) + .load(&mut conn) + .await?; + + let versions = versions_and_publishers + .iter() + .map(|(v, ..)| v) + .collect::>(); + + let actions = VersionOwnerAction::for_versions(&mut conn, &versions).await?; + + let versions = versions_and_publishers + .into_iter() + .zip(actions) + .map(|((version, krate_name, published_by), actions)| { + EncodableVersion::from(version, &krate_name.name, published_by, actions) + }) + .collect::>(); + + Ok(json!({ + "dependencies": rev_deps, + "versions": versions, + "meta": { "total": total }, + })) +} diff --git a/src/router.rs b/src/router.rs index be8a480a36..c371c948cb 100644 --- a/src/router.rs +++ b/src/router.rs @@ -50,7 +50,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> { .routes(routes!(krate::follow::get_following_crate)) .routes(routes!(krate::owners::get_team_owners)) .routes(routes!(krate::owners::get_user_owners)) - .routes(routes!(krate::metadata::list_reverse_dependencies)) + .routes(routes!(krate::rev_deps::list_reverse_dependencies)) .routes(routes!(keyword::list_keywords)) .routes(routes!(keyword::find_keyword)) .routes(routes!(category::list_categories)) From 31a17e2b3ebeaa4c4e6da588e078da619ce37685 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:36:50 +0100 Subject: [PATCH 2/8] controllers/krate/metadata: Move `get_version_readme()` to dedicated module --- src/controllers/krate/metadata.rs | 21 +-------------------- src/controllers/version.rs | 1 + src/controllers/version/readme.rs | 23 +++++++++++++++++++++++ src/router.rs | 2 +- 4 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 src/controllers/version/readme.rs diff --git a/src/controllers/krate/metadata.rs b/src/controllers/krate/metadata.rs index f7d2ed5e1c..0038b549ca 100644 --- a/src/controllers/krate/metadata.rs +++ b/src/controllers/krate/metadata.rs @@ -6,16 +6,14 @@ use crate::app::AppState; use crate::controllers::krate::CratePath; -use crate::controllers::version::CrateVersionPath; use crate::models::{ Category, Crate, CrateCategory, CrateKeyword, Keyword, RecentCrateDownloads, User, Version, VersionOwnerAction, }; use crate::schema::*; use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError}; -use crate::util::{redirect, RequestUtils}; +use crate::util::RequestUtils; use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword, EncodableVersion}; -use axum::response::{IntoResponse, Response}; use axum_extra::json; use axum_extra::response::ErasedJson; use diesel::prelude::*; @@ -241,20 +239,3 @@ impl FromStr for ShowIncludeMode { Ok(mode) } } - -/// Get the readme of a crate version. -#[utoipa::path( - get, - path = "/api/v1/crates/{name}/{version}/readme", - params(CrateVersionPath), - tag = "versions", - responses((status = 200, description = "Successful Response")), -)] -pub async fn get_version_readme(app: AppState, path: CrateVersionPath, req: Parts) -> Response { - let redirect_url = app.storage.readme_location(&path.name, &path.version); - if req.wants_json() { - json!({ "url": redirect_url }).into_response() - } else { - redirect(redirect_url) - } -} diff --git a/src/controllers/version.rs b/src/controllers/version.rs index 78921fb37b..84208eb804 100644 --- a/src/controllers/version.rs +++ b/src/controllers/version.rs @@ -1,5 +1,6 @@ pub mod downloads; pub mod metadata; +pub mod readme; pub mod yank; use axum::extract::{FromRequestParts, Path}; diff --git a/src/controllers/version/readme.rs b/src/controllers/version/readme.rs new file mode 100644 index 0000000000..53df1cd84e --- /dev/null +++ b/src/controllers/version/readme.rs @@ -0,0 +1,23 @@ +use crate::app::AppState; +use crate::controllers::version::CrateVersionPath; +use crate::util::{redirect, RequestUtils}; +use axum::response::{IntoResponse, Response}; +use axum_extra::json; +use http::request::Parts; + +/// Get the readme of a crate version. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/{version}/readme", + params(CrateVersionPath), + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] +pub async fn get_version_readme(app: AppState, path: CrateVersionPath, req: Parts) -> Response { + let redirect_url = app.storage.readme_location(&path.name, &path.version); + if req.wants_json() { + json!({ "url": redirect_url }).into_response() + } else { + redirect(redirect_url) + } +} diff --git a/src/router.rs b/src/router.rs index c371c948cb..f214d7e6b5 100644 --- a/src/router.rs +++ b/src/router.rs @@ -37,7 +37,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> { version::metadata::find_version, version::metadata::update_version )) - .routes(routes!(krate::metadata::get_version_readme)) + .routes(routes!(version::readme::get_version_readme)) .routes(routes!(version::metadata::get_version_dependencies)) .routes(routes!(version::downloads::get_version_downloads)) .routes(routes!(version::metadata::get_version_authors)) From 6ef828b6462850256eda9736d032f09dd61a081c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:38:36 +0100 Subject: [PATCH 3/8] controllers/user/me: Move `update_email_notifications()` to dedicated module --- src/controllers/user.rs | 1 + src/controllers/user/email_notifications.rs | 89 +++++++++++++++++++++ src/controllers/user/me.rs | 78 ------------------ src/router.rs | 4 +- 4 files changed, 93 insertions(+), 79 deletions(-) create mode 100644 src/controllers/user/email_notifications.rs diff --git a/src/controllers/user.rs b/src/controllers/user.rs index 280f14999c..26315b3ab0 100644 --- a/src/controllers/user.rs +++ b/src/controllers/user.rs @@ -1,3 +1,4 @@ +pub mod email_notifications; pub mod me; pub mod other; pub mod resend; diff --git a/src/controllers/user/email_notifications.rs b/src/controllers/user/email_notifications.rs new file mode 100644 index 0000000000..6814f37b45 --- /dev/null +++ b/src/controllers/user/email_notifications.rs @@ -0,0 +1,89 @@ +use crate::app::AppState; +use crate::auth::AuthCheck; +use crate::controllers::helpers::ok_true; +use crate::models::{CrateOwner, OwnerKind}; +use crate::schema::crate_owners; +use crate::util::errors::AppResult; +use axum::response::Response; +use axum::Json; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use http::request::Parts; +use std::collections::HashMap; + +#[derive(Deserialize)] +pub struct CrateEmailNotifications { + id: i32, + email_notifications: bool, +} + +/// Update email notification settings for the authenticated user. +/// +/// This endpoint was implemented for an experimental feature that was never +/// fully implemented. It is now deprecated and will be removed in the future. +#[utoipa::path( + put, + path = "/api/v1/me/email_notifications", + tag = "users", + responses((status = 200, description = "Successful Response")), +)] +#[deprecated] +pub async fn update_email_notifications( + app: AppState, + parts: Parts, + Json(updates): Json>, +) -> AppResult { + use diesel::pg::upsert::excluded; + + let updates: HashMap = updates + .iter() + .map(|c| (c.id, c.email_notifications)) + .collect(); + + let mut conn = app.db_write().await?; + let user_id = AuthCheck::default() + .check(&parts, &mut conn) + .await? + .user_id(); + + // Build inserts from existing crates belonging to the current user + let to_insert = CrateOwner::by_owner_kind(OwnerKind::User) + .filter(crate_owners::owner_id.eq(user_id)) + .select(( + crate_owners::crate_id, + crate_owners::owner_id, + crate_owners::owner_kind, + crate_owners::email_notifications, + )) + .load(&mut conn) + .await? + .into_iter() + // Remove records whose `email_notifications` will not change from their current value + .map( + |(c_id, o_id, o_kind, e_notifications): (i32, i32, i32, bool)| { + let current_e_notifications = *updates.get(&c_id).unwrap_or(&e_notifications); + ( + crate_owners::crate_id.eq(c_id), + crate_owners::owner_id.eq(o_id), + crate_owners::owner_kind.eq(o_kind), + crate_owners::email_notifications.eq(current_e_notifications), + ) + }, + ) + .collect::>(); + + // Upsert crate owners; this should only actually execute updates + diesel::insert_into(crate_owners::table) + .values(&to_insert) + .on_conflict(( + crate_owners::crate_id, + crate_owners::owner_id, + crate_owners::owner_kind, + )) + .do_update() + .set(crate_owners::email_notifications.eq(excluded(crate_owners::email_notifications))) + .execute(&mut conn) + .await?; + + ok_true() +} diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index 8c3caf9fa0..1e42872431 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -7,7 +7,6 @@ use axum_extra::response::ErasedJson; use diesel::prelude::*; use diesel_async::RunQueryDsl; use http::request::Parts; -use std::collections::HashMap; use crate::app::AppState; use crate::controllers::helpers::pagination::{Paginated, PaginationOptions}; @@ -138,80 +137,3 @@ pub async fn confirm_user_email(state: AppState, Path(token): Path) -> A ok_true() } - -#[derive(Deserialize)] -pub struct CrateEmailNotifications { - id: i32, - email_notifications: bool, -} - -/// Update email notification settings for the authenticated user. -/// -/// This endpoint was implemented for an experimental feature that was never -/// fully implemented. It is now deprecated and will be removed in the future. -#[utoipa::path( - put, - path = "/api/v1/me/email_notifications", - tag = "users", - responses((status = 200, description = "Successful Response")), -)] -#[deprecated] -pub async fn update_email_notifications( - app: AppState, - parts: Parts, - Json(updates): Json>, -) -> AppResult { - use diesel::pg::upsert::excluded; - - let updates: HashMap = updates - .iter() - .map(|c| (c.id, c.email_notifications)) - .collect(); - - let mut conn = app.db_write().await?; - let user_id = AuthCheck::default() - .check(&parts, &mut conn) - .await? - .user_id(); - - // Build inserts from existing crates belonging to the current user - let to_insert = CrateOwner::by_owner_kind(OwnerKind::User) - .filter(crate_owners::owner_id.eq(user_id)) - .select(( - crate_owners::crate_id, - crate_owners::owner_id, - crate_owners::owner_kind, - crate_owners::email_notifications, - )) - .load(&mut conn) - .await? - .into_iter() - // Remove records whose `email_notifications` will not change from their current value - .map( - |(c_id, o_id, o_kind, e_notifications): (i32, i32, i32, bool)| { - let current_e_notifications = *updates.get(&c_id).unwrap_or(&e_notifications); - ( - crate_owners::crate_id.eq(c_id), - crate_owners::owner_id.eq(o_id), - crate_owners::owner_kind.eq(o_kind), - crate_owners::email_notifications.eq(current_e_notifications), - ) - }, - ) - .collect::>(); - - // Upsert crate owners; this should only actually execute updates - diesel::insert_into(crate_owners::table) - .values(&to_insert) - .on_conflict(( - crate_owners::crate_id, - crate_owners::owner_id, - crate_owners::owner_kind, - )) - .do_update() - .set(crate_owners::email_notifications.eq(excluded(crate_owners::email_notifications))) - .execute(&mut conn) - .await?; - - ok_true() -} diff --git a/src/router.rs b/src/router.rs index f214d7e6b5..ad2386725b 100644 --- a/src/router.rs +++ b/src/router.rs @@ -76,7 +76,9 @@ pub fn build_axum_router(state: AppState) -> Router<()> { .routes(routes!( crate_owner_invitation::accept_crate_owner_invitation_with_token )) - .routes(routes!(user::me::update_email_notifications)) + .routes(routes!( + user::email_notifications::update_email_notifications + )) .routes(routes!(summary::get_summary)) .routes(routes!(user::me::confirm_user_email)) .routes(routes!(user::resend::resend_email_verification)) From 3514423ed2524424448b9e7e4902084dc3c63e21 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:40:30 +0100 Subject: [PATCH 4/8] controllers/user/me: Move `confirm_user_email()` to dedicated module --- src/controllers/user.rs | 4 +-- .../user/{resend.rs => email_verification.rs} | 25 ++++++++++++++ src/controllers/user/me.rs | 33 ++----------------- ...il_verification__tests__happy_path-2.snap} | 2 +- src/router.rs | 4 +-- 5 files changed, 32 insertions(+), 36 deletions(-) rename src/controllers/user/{resend.rs => email_verification.rs} (82%) rename src/controllers/user/snapshots/{crates_io__controllers__user__resend__tests__happy_path-2.snap => crates_io__controllers__user__email_verification__tests__happy_path-2.snap} (88%) diff --git a/src/controllers/user.rs b/src/controllers/user.rs index 26315b3ab0..b605fe0106 100644 --- a/src/controllers/user.rs +++ b/src/controllers/user.rs @@ -1,9 +1,9 @@ pub mod email_notifications; +pub mod email_verification; pub mod me; pub mod other; -pub mod resend; pub mod session; pub mod update; -pub use resend::resend_email_verification; +pub use email_verification::resend_email_verification; pub use update::update_user; diff --git a/src/controllers/user/resend.rs b/src/controllers/user/email_verification.rs similarity index 82% rename from src/controllers/user/resend.rs rename to src/controllers/user/email_verification.rs index ef5084daf7..145a8e580a 100644 --- a/src/controllers/user/resend.rs +++ b/src/controllers/user/email_verification.rs @@ -14,6 +14,31 @@ use diesel_async::scoped_futures::ScopedFutureExt; use diesel_async::{AsyncConnection, RunQueryDsl}; use http::request::Parts; +/// Marks the email belonging to the given token as verified. +#[utoipa::path( + put, + path = "/api/v1/confirm/{email_token}", + params( + ("email_token" = String, Path, description = "Secret verification token sent to the user's email address"), + ), + tag = "users", + responses((status = 200, description = "Successful Response")), +)] +pub async fn confirm_user_email(state: AppState, Path(token): Path) -> AppResult { + let mut conn = state.db_write().await?; + + let updated_rows = diesel::update(emails::table.filter(emails::token.eq(&token))) + .set(emails::verified.eq(true)) + .execute(&mut conn) + .await?; + + if updated_rows == 0 { + return Err(bad_request("Email belonging to token not found.")); + } + + ok_true() +} + /// Regenerate and send an email verification token. #[utoipa::path( put, diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index 1e42872431..a8484633bb 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -1,6 +1,4 @@ use crate::auth::AuthCheck; -use axum::extract::Path; -use axum::response::Response; use axum::Json; use axum_extra::json; use axum_extra::response::ErasedJson; @@ -10,11 +8,11 @@ use http::request::Parts; use crate::app::AppState; use crate::controllers::helpers::pagination::{Paginated, PaginationOptions}; -use crate::controllers::helpers::{ok_true, Paginate}; +use crate::controllers::helpers::Paginate; use crate::models::krate::CrateName; use crate::models::{CrateOwner, Follow, OwnerKind, User, Version, VersionOwnerAction}; use crate::schema::{crate_owners, crates, emails, follows, users, versions}; -use crate::util::errors::{bad_request, AppResult}; +use crate::util::errors::AppResult; use crate::views::{EncodableMe, EncodablePrivateUser, EncodableVersion, OwnedCrate}; /// Get the currently authenticated user. @@ -110,30 +108,3 @@ pub async fn get_authenticated_user_updates(app: AppState, req: Parts) -> AppRes "meta": { "more": more }, })) } - -/// Marks the email belonging to the given token as verified. -#[utoipa::path( - put, - path = "/api/v1/confirm/{email_token}", - params( - ("email_token" = String, Path, description = "Secret verification token sent to the user's email address"), - ), - tag = "users", - responses((status = 200, description = "Successful Response")), -)] -pub async fn confirm_user_email(state: AppState, Path(token): Path) -> AppResult { - use diesel::update; - - let mut conn = state.db_write().await?; - - let updated_rows = update(emails::table.filter(emails::token.eq(&token))) - .set(emails::verified.eq(true)) - .execute(&mut conn) - .await?; - - if updated_rows == 0 { - return Err(bad_request("Email belonging to token not found.")); - } - - ok_true() -} diff --git a/src/controllers/user/snapshots/crates_io__controllers__user__resend__tests__happy_path-2.snap b/src/controllers/user/snapshots/crates_io__controllers__user__email_verification__tests__happy_path-2.snap similarity index 88% rename from src/controllers/user/snapshots/crates_io__controllers__user__resend__tests__happy_path-2.snap rename to src/controllers/user/snapshots/crates_io__controllers__user__email_verification__tests__happy_path-2.snap index cab488718a..c225ad1d01 100644 --- a/src/controllers/user/snapshots/crates_io__controllers__user__resend__tests__happy_path-2.snap +++ b/src/controllers/user/snapshots/crates_io__controllers__user__email_verification__tests__happy_path-2.snap @@ -1,5 +1,5 @@ --- -source: src/controllers/user/resend.rs +source: src/controllers/user/email_verification.rs expression: app.emails_snapshot().await snapshot_kind: text --- diff --git a/src/router.rs b/src/router.rs index ad2386725b..7465301daf 100644 --- a/src/router.rs +++ b/src/router.rs @@ -80,8 +80,8 @@ pub fn build_axum_router(state: AppState) -> Router<()> { user::email_notifications::update_email_notifications )) .routes(routes!(summary::get_summary)) - .routes(routes!(user::me::confirm_user_email)) - .routes(routes!(user::resend::resend_email_verification)) + .routes(routes!(user::email_verification::confirm_user_email)) + .routes(routes!(user::email_verification::resend_email_verification)) .routes(routes!(site_metadata::get_site_metadata)) // Session management .routes(routes!(user::session::begin_session)) From 3bc7e27c0c0faafbd12e0a1c954f199eae570b9c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:41:58 +0100 Subject: [PATCH 5/8] controllers/user/session: Convert into top-level controller module --- src/controllers.rs | 1 + src/controllers/{user => }/session.rs | 2 +- src/controllers/user.rs | 1 - src/router.rs | 6 +++--- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/controllers/{user => }/session.rs (99%) diff --git a/src/controllers.rs b/src/controllers.rs index 435d6dfbb0..5563b2eace 100644 --- a/src/controllers.rs +++ b/src/controllers.rs @@ -8,6 +8,7 @@ pub mod github; pub mod keyword; pub mod krate; pub mod metrics; +pub mod session; pub mod site_metadata; pub mod summary; pub mod team; diff --git a/src/controllers/user/session.rs b/src/controllers/session.rs similarity index 99% rename from src/controllers/user/session.rs rename to src/controllers/session.rs index 565707895e..8889e86984 100644 --- a/src/controllers/user/session.rs +++ b/src/controllers/session.rs @@ -128,7 +128,7 @@ pub async fn authorize_session( // Log in by setting a cookie and the middleware authentication session.insert("user_id".to_string(), user.id.to_string()); - super::me::get_authenticated_user(app, req).await + super::user::me::get_authenticated_user(app, req).await } async fn save_user_to_database( diff --git a/src/controllers/user.rs b/src/controllers/user.rs index b605fe0106..4a604d1607 100644 --- a/src/controllers/user.rs +++ b/src/controllers/user.rs @@ -2,7 +2,6 @@ pub mod email_notifications; pub mod email_verification; pub mod me; pub mod other; -pub mod session; pub mod update; pub use email_verification::resend_email_verification; diff --git a/src/router.rs b/src/router.rs index 7465301daf..ddcbdb76cb 100644 --- a/src/router.rs +++ b/src/router.rs @@ -84,9 +84,9 @@ pub fn build_axum_router(state: AppState) -> Router<()> { .routes(routes!(user::email_verification::resend_email_verification)) .routes(routes!(site_metadata::get_site_metadata)) // Session management - .routes(routes!(user::session::begin_session)) - .routes(routes!(user::session::authorize_session)) - .routes(routes!(user::session::end_session)) + .routes(routes!(session::begin_session)) + .routes(routes!(session::authorize_session)) + .routes(routes!(session::end_session)) .split_for_parts(); let mut router = router From 3bb07a7c719e3dfe7d95e02c07450803b405a6d9 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:45:17 +0100 Subject: [PATCH 6/8] controllers/version/metadata: Move `get_version_dependencies()` to dedicated module --- src/controllers/version.rs | 1 + src/controllers/version/dependencies.rs | 44 +++++++++++++++++++++++++ src/controllers/version/metadata.rs | 39 ++-------------------- src/router.rs | 2 +- 4 files changed, 48 insertions(+), 38 deletions(-) create mode 100644 src/controllers/version/dependencies.rs diff --git a/src/controllers/version.rs b/src/controllers/version.rs index 84208eb804..33f25433ee 100644 --- a/src/controllers/version.rs +++ b/src/controllers/version.rs @@ -1,3 +1,4 @@ +pub mod dependencies; pub mod downloads; pub mod metadata; pub mod readme; diff --git a/src/controllers/version/dependencies.rs b/src/controllers/version/dependencies.rs new file mode 100644 index 0000000000..82299f1575 --- /dev/null +++ b/src/controllers/version/dependencies.rs @@ -0,0 +1,44 @@ +use super::CrateVersionPath; +use crate::app::AppState; +use crate::models::Dependency; +use crate::util::errors::AppResult; +use crate::views::EncodableDependency; +use axum_extra::json; +use axum_extra::response::ErasedJson; +use crates_io_database::schema::{crates, dependencies}; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; + +/// Get crate version dependencies. +/// +/// This information can also be obtained directly from the index. +/// +/// In addition to returning cached data from the index, this returns +/// fields for `id`, `version_id`, and `downloads` (which appears to always +/// be 0) +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/{version}/dependencies", + params(CrateVersionPath), + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] +pub async fn get_version_dependencies( + state: AppState, + path: CrateVersionPath, +) -> AppResult { + let mut conn = state.db_read().await?; + let version = path.load_version(&mut conn).await?; + + let deps = Dependency::belonging_to(&version) + .inner_join(crates::table) + .select((Dependency::as_select(), crates::name)) + .order((dependencies::optional, crates::name)) + .load::<(Dependency, String)>(&mut conn) + .await? + .into_iter() + .map(|(dep, crate_name)| EncodableDependency::from_dep(dep, &crate_name)) + .collect::>(); + + Ok(json!({ "dependencies": deps })) +} diff --git a/src/controllers/version/metadata.rs b/src/controllers/version/metadata.rs index 9cd541ec8a..471c298238 100644 --- a/src/controllers/version/metadata.rs +++ b/src/controllers/version/metadata.rs @@ -7,7 +7,6 @@ use axum::Json; use axum_extra::json; use axum_extra::response::ErasedJson; -use crates_io_database::schema::{crates, dependencies}; use crates_io_worker::BackgroundJob; use diesel::prelude::*; use diesel_async::{AsyncPgConnection, RunQueryDsl}; @@ -19,12 +18,12 @@ use crate::app::AppState; use crate::auth::{AuthCheck, Authentication}; use crate::models::token::EndpointScope; use crate::models::{ - Crate, Dependency, NewVersionOwnerAction, Rights, Version, VersionAction, VersionOwnerAction, + Crate, NewVersionOwnerAction, Rights, Version, VersionAction, VersionOwnerAction, }; use crate::rate_limiter::LimitedAction; use crate::schema::versions; use crate::util::errors::{bad_request, custom, AppResult}; -use crate::views::{EncodableDependency, EncodableVersion}; +use crate::views::EncodableVersion; use crate::worker::jobs::{SyncToGitIndex, SyncToSparseIndex, UpdateDefaultVersion}; use super::CrateVersionPath; @@ -39,40 +38,6 @@ pub struct VersionUpdateRequest { version: VersionUpdate, } -/// Get crate version dependencies. -/// -/// This information can also be obtained directly from the index. -/// -/// In addition to returning cached data from the index, this returns -/// fields for `id`, `version_id`, and `downloads` (which appears to always -/// be 0) -#[utoipa::path( - get, - path = "/api/v1/crates/{name}/{version}/dependencies", - params(CrateVersionPath), - tag = "versions", - responses((status = 200, description = "Successful Response")), -)] -pub async fn get_version_dependencies( - state: AppState, - path: CrateVersionPath, -) -> AppResult { - let mut conn = state.db_read().await?; - let version = path.load_version(&mut conn).await?; - - let deps = Dependency::belonging_to(&version) - .inner_join(crates::table) - .select((Dependency::as_select(), crates::name)) - .order((dependencies::optional, crates::name)) - .load::<(Dependency, String)>(&mut conn) - .await? - .into_iter() - .map(|(dep, crate_name)| EncodableDependency::from_dep(dep, &crate_name)) - .collect::>(); - - Ok(json!({ "dependencies": deps })) -} - /// Get crate version authors. /// /// This endpoint was deprecated by [RFC #3052](https://github.com/rust-lang/rfcs/pull/3052) diff --git a/src/router.rs b/src/router.rs index ddcbdb76cb..f28f2e2344 100644 --- a/src/router.rs +++ b/src/router.rs @@ -38,7 +38,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> { version::metadata::update_version )) .routes(routes!(version::readme::get_version_readme)) - .routes(routes!(version::metadata::get_version_dependencies)) + .routes(routes!(version::dependencies::get_version_dependencies)) .routes(routes!(version::downloads::get_version_downloads)) .routes(routes!(version::metadata::get_version_authors)) .routes(routes!(krate::downloads::get_crate_downloads)) From 09881fa9d99586db7d5465ac5db209d94e9d3b40 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:46:54 +0100 Subject: [PATCH 7/8] controllers/version/metadata: Move `get_version_authors()` to dedicated module --- src/controllers/version.rs | 1 + src/controllers/version/authors.rs | 22 ++++++++++++++++++++++ src/controllers/version/metadata.rs | 19 ------------------- src/router.rs | 2 +- 4 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 src/controllers/version/authors.rs diff --git a/src/controllers/version.rs b/src/controllers/version.rs index 33f25433ee..ad946a3eed 100644 --- a/src/controllers/version.rs +++ b/src/controllers/version.rs @@ -1,3 +1,4 @@ +pub mod authors; pub mod dependencies; pub mod downloads; pub mod metadata; diff --git a/src/controllers/version/authors.rs b/src/controllers/version/authors.rs new file mode 100644 index 0000000000..85fee9efda --- /dev/null +++ b/src/controllers/version/authors.rs @@ -0,0 +1,22 @@ +use crate::controllers::version::CrateVersionPath; +use axum_extra::json; +use axum_extra::response::ErasedJson; + +/// Get crate version authors. +/// +/// This endpoint was deprecated by [RFC #3052](https://github.com/rust-lang/rfcs/pull/3052) +/// and returns an empty list for backwards compatibility reasons. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/{version}/authors", + params(CrateVersionPath), + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] +#[deprecated] +pub async fn get_version_authors() -> ErasedJson { + json!({ + "users": [], + "meta": { "names": [] }, + }) +} diff --git a/src/controllers/version/metadata.rs b/src/controllers/version/metadata.rs index 471c298238..cd116f15ac 100644 --- a/src/controllers/version/metadata.rs +++ b/src/controllers/version/metadata.rs @@ -38,25 +38,6 @@ pub struct VersionUpdateRequest { version: VersionUpdate, } -/// Get crate version authors. -/// -/// This endpoint was deprecated by [RFC #3052](https://github.com/rust-lang/rfcs/pull/3052) -/// and returns an empty list for backwards compatibility reasons. -#[utoipa::path( - get, - path = "/api/v1/crates/{name}/{version}/authors", - params(CrateVersionPath), - tag = "versions", - responses((status = 200, description = "Successful Response")), -)] -#[deprecated] -pub async fn get_version_authors() -> ErasedJson { - json!({ - "users": [], - "meta": { "names": [] }, - }) -} - /// Get crate version metadata. #[utoipa::path( get, diff --git a/src/router.rs b/src/router.rs index f28f2e2344..5eccd6c9a4 100644 --- a/src/router.rs +++ b/src/router.rs @@ -40,7 +40,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> { .routes(routes!(version::readme::get_version_readme)) .routes(routes!(version::dependencies::get_version_dependencies)) .routes(routes!(version::downloads::get_version_downloads)) - .routes(routes!(version::metadata::get_version_authors)) + .routes(routes!(version::authors::get_version_authors)) .routes(routes!(krate::downloads::get_crate_downloads)) .routes(routes!(krate::versions::list_versions)) .routes(routes!( From dc70376c3955d7bbbf31f31d794104b1fe7fa77d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 17 Dec 2024 17:49:08 +0100 Subject: [PATCH 8/8] controllers/version/metadata: Move `update_version()` to dedicated module --- src/controllers/version.rs | 1 + src/controllers/version/metadata.rs | 180 +-------------------------- src/controllers/version/update.rs | 183 ++++++++++++++++++++++++++++ src/controllers/version/yank.rs | 2 +- src/router.rs | 2 +- 5 files changed, 188 insertions(+), 180 deletions(-) create mode 100644 src/controllers/version/update.rs diff --git a/src/controllers/version.rs b/src/controllers/version.rs index ad946a3eed..2c1f1dca68 100644 --- a/src/controllers/version.rs +++ b/src/controllers/version.rs @@ -3,6 +3,7 @@ pub mod dependencies; pub mod downloads; pub mod metadata; pub mod readme; +pub mod update; pub mod yank; use axum::extract::{FromRequestParts, Path}; diff --git a/src/controllers/version/metadata.rs b/src/controllers/version/metadata.rs index cd116f15ac..f83bee7ddb 100644 --- a/src/controllers/version/metadata.rs +++ b/src/controllers/version/metadata.rs @@ -4,40 +4,16 @@ //! index or cached metadata which was extracted (client side) from the //! `Cargo.toml` file. -use axum::Json; use axum_extra::json; use axum_extra::response::ErasedJson; -use crates_io_worker::BackgroundJob; -use diesel::prelude::*; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; -use http::request::Parts; -use http::StatusCode; -use serde::Deserialize; use crate::app::AppState; -use crate::auth::{AuthCheck, Authentication}; -use crate::models::token::EndpointScope; -use crate::models::{ - Crate, NewVersionOwnerAction, Rights, Version, VersionAction, VersionOwnerAction, -}; -use crate::rate_limiter::LimitedAction; -use crate::schema::versions; -use crate::util::errors::{bad_request, custom, AppResult}; +use crate::models::VersionOwnerAction; +use crate::util::errors::AppResult; use crate::views::EncodableVersion; -use crate::worker::jobs::{SyncToGitIndex, SyncToSparseIndex, UpdateDefaultVersion}; use super::CrateVersionPath; -#[derive(Deserialize)] -pub struct VersionUpdate { - yanked: Option, - yank_message: Option, -} -#[derive(Deserialize)] -pub struct VersionUpdateRequest { - version: VersionUpdate, -} - /// Get crate version metadata. #[utoipa::path( get, @@ -55,155 +31,3 @@ pub async fn find_version(state: AppState, path: CrateVersionPath) -> AppResult< let version = EncodableVersion::from(version, &krate.name, published_by, actions); Ok(json!({ "version": version })) } - -/// Update a crate version. -/// -/// This endpoint allows updating the `yanked` state of a version, including a yank message. -#[utoipa::path( - patch, - path = "/api/v1/crates/{name}/{version}", - params(CrateVersionPath), - tag = "versions", - responses((status = 200, description = "Successful Response")), -)] -pub async fn update_version( - state: AppState, - path: CrateVersionPath, - req: Parts, - Json(update_request): Json, -) -> AppResult { - let mut conn = state.db_write().await?; - let (mut version, krate) = path.load_version_and_crate(&mut conn).await?; - validate_yank_update(&update_request.version, &version)?; - let auth = authenticate(&req, &mut conn, &krate.name).await?; - - state - .rate_limiter - .check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, &mut conn) - .await?; - - perform_version_yank_update( - &state, - &mut conn, - &mut version, - &krate, - &auth, - update_request.version.yanked, - update_request.version.yank_message, - ) - .await?; - - let published_by = version.published_by(&mut conn).await?; - let actions = VersionOwnerAction::by_version(&mut conn, &version).await?; - let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions); - Ok(json!({ "version": updated_version })) -} - -fn validate_yank_update(update_data: &VersionUpdate, version: &Version) -> AppResult<()> { - if update_data.yank_message.is_some() { - if matches!(update_data.yanked, Some(false)) { - return Err(bad_request("Cannot set yank message when unyanking")); - } - - if update_data.yanked.is_none() && !version.yanked { - return Err(bad_request( - "Cannot update yank message for a version that is not yanked", - )); - } - } - - Ok(()) -} - -pub async fn authenticate( - req: &Parts, - conn: &mut AsyncPgConnection, - name: &str, -) -> AppResult { - AuthCheck::default() - .with_endpoint_scope(EndpointScope::Yank) - .for_crate(name) - .check(req, conn) - .await -} - -pub async fn perform_version_yank_update( - state: &AppState, - conn: &mut AsyncPgConnection, - version: &mut Version, - krate: &Crate, - auth: &Authentication, - yanked: Option, - yank_message: Option, -) -> AppResult<()> { - let api_token_id = auth.api_token_id(); - let user = auth.user(); - let owners = krate.owners(conn).await?; - - let yanked = yanked.unwrap_or(version.yanked); - - if user.rights(state, &owners).await? < Rights::Publish { - if user.is_admin { - let action = if yanked { "yanking" } else { "unyanking" }; - warn!( - "Admin {} is {action} {}@{}", - user.gh_login, krate.name, version.num - ); - } else { - return Err(custom( - StatusCode::FORBIDDEN, - "must already be an owner to yank or unyank", - )); - } - } - - // Check if the yanked state or yank message has changed and update if necessary - let updated_cnt = diesel::update( - versions::table.find(version.id).filter( - versions::yanked - .is_distinct_from(yanked) - .or(versions::yank_message.is_distinct_from(&yank_message)), - ), - ) - .set(( - versions::yanked.eq(yanked), - versions::yank_message.eq(&yank_message), - )) - .execute(conn) - .await?; - - // If no rows were updated, return early - if updated_cnt == 0 { - return Ok(()); - } - - // Apply the update to the version - version.yanked = yanked; - version.yank_message = yank_message; - - let action = if yanked { - VersionAction::Yank - } else { - VersionAction::Unyank - }; - NewVersionOwnerAction::builder() - .version_id(version.id) - .user_id(user.id) - .maybe_api_token_id(api_token_id) - .action(action) - .build() - .insert(conn) - .await?; - - let git_index_job = SyncToGitIndex::new(&krate.name); - let sparse_index_job = SyncToSparseIndex::new(&krate.name); - let update_default_version_job = UpdateDefaultVersion::new(krate.id); - - tokio::try_join!( - git_index_job.enqueue(conn), - sparse_index_job.enqueue(conn), - update_default_version_job.enqueue(conn), - )?; - - Ok(()) -} diff --git a/src/controllers/version/update.rs b/src/controllers/version/update.rs new file mode 100644 index 0000000000..89371469a8 --- /dev/null +++ b/src/controllers/version/update.rs @@ -0,0 +1,183 @@ +use super::CrateVersionPath; +use crate::app::AppState; +use crate::auth::{AuthCheck, Authentication}; +use crate::models::token::EndpointScope; +use crate::models::{ + Crate, NewVersionOwnerAction, Rights, Version, VersionAction, VersionOwnerAction, +}; +use crate::rate_limiter::LimitedAction; +use crate::schema::versions; +use crate::util::errors::{bad_request, custom, AppResult}; +use crate::views::EncodableVersion; +use crate::worker::jobs::{SyncToGitIndex, SyncToSparseIndex, UpdateDefaultVersion}; +use axum::Json; +use axum_extra::json; +use axum_extra::response::ErasedJson; +use crates_io_worker::BackgroundJob; +use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use http::request::Parts; +use http::StatusCode; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct VersionUpdate { + yanked: Option, + yank_message: Option, +} +#[derive(Deserialize)] +pub struct VersionUpdateRequest { + version: VersionUpdate, +} + +/// Update a crate version. +/// +/// This endpoint allows updating the `yanked` state of a version, including a yank message. +#[utoipa::path( + patch, + path = "/api/v1/crates/{name}/{version}", + params(CrateVersionPath), + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] +pub async fn update_version( + state: AppState, + path: CrateVersionPath, + req: Parts, + Json(update_request): Json, +) -> AppResult { + let mut conn = state.db_write().await?; + let (mut version, krate) = path.load_version_and_crate(&mut conn).await?; + validate_yank_update(&update_request.version, &version)?; + let auth = authenticate(&req, &mut conn, &krate.name).await?; + + state + .rate_limiter + .check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, &mut conn) + .await?; + + perform_version_yank_update( + &state, + &mut conn, + &mut version, + &krate, + &auth, + update_request.version.yanked, + update_request.version.yank_message, + ) + .await?; + + let published_by = version.published_by(&mut conn).await?; + let actions = VersionOwnerAction::by_version(&mut conn, &version).await?; + let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions); + Ok(json!({ "version": updated_version })) +} + +fn validate_yank_update(update_data: &VersionUpdate, version: &Version) -> AppResult<()> { + if update_data.yank_message.is_some() { + if matches!(update_data.yanked, Some(false)) { + return Err(bad_request("Cannot set yank message when unyanking")); + } + + if update_data.yanked.is_none() && !version.yanked { + return Err(bad_request( + "Cannot update yank message for a version that is not yanked", + )); + } + } + + Ok(()) +} + +pub async fn authenticate( + req: &Parts, + conn: &mut AsyncPgConnection, + name: &str, +) -> AppResult { + AuthCheck::default() + .with_endpoint_scope(EndpointScope::Yank) + .for_crate(name) + .check(req, conn) + .await +} + +pub async fn perform_version_yank_update( + state: &AppState, + conn: &mut AsyncPgConnection, + version: &mut Version, + krate: &Crate, + auth: &Authentication, + yanked: Option, + yank_message: Option, +) -> AppResult<()> { + let api_token_id = auth.api_token_id(); + let user = auth.user(); + let owners = krate.owners(conn).await?; + + let yanked = yanked.unwrap_or(version.yanked); + + if user.rights(state, &owners).await? < Rights::Publish { + if user.is_admin { + let action = if yanked { "yanking" } else { "unyanking" }; + warn!( + "Admin {} is {action} {}@{}", + user.gh_login, krate.name, version.num + ); + } else { + return Err(custom( + StatusCode::FORBIDDEN, + "must already be an owner to yank or unyank", + )); + } + } + + // Check if the yanked state or yank message has changed and update if necessary + let updated_cnt = diesel::update( + versions::table.find(version.id).filter( + versions::yanked + .is_distinct_from(yanked) + .or(versions::yank_message.is_distinct_from(&yank_message)), + ), + ) + .set(( + versions::yanked.eq(yanked), + versions::yank_message.eq(&yank_message), + )) + .execute(conn) + .await?; + + // If no rows were updated, return early + if updated_cnt == 0 { + return Ok(()); + } + + // Apply the update to the version + version.yanked = yanked; + version.yank_message = yank_message; + + let action = if yanked { + VersionAction::Yank + } else { + VersionAction::Unyank + }; + NewVersionOwnerAction::builder() + .version_id(version.id) + .user_id(user.id) + .maybe_api_token_id(api_token_id) + .action(action) + .build() + .insert(conn) + .await?; + + let git_index_job = SyncToGitIndex::new(&krate.name); + let sparse_index_job = SyncToSparseIndex::new(&krate.name); + let update_default_version_job = UpdateDefaultVersion::new(krate.id); + + tokio::try_join!( + git_index_job.enqueue(conn), + sparse_index_job.enqueue(conn), + update_default_version_job.enqueue(conn), + )?; + + Ok(()) +} diff --git a/src/controllers/version/yank.rs b/src/controllers/version/yank.rs index 4e69ef458b..6410bbcfcd 100644 --- a/src/controllers/version/yank.rs +++ b/src/controllers/version/yank.rs @@ -1,6 +1,6 @@ //! Endpoints for yanking and unyanking specific versions of crates -use super::metadata::{authenticate, perform_version_yank_update}; +use super::update::{authenticate, perform_version_yank_update}; use super::CrateVersionPath; use crate::app::AppState; use crate::controllers::helpers::ok_true; diff --git a/src/router.rs b/src/router.rs index 5eccd6c9a4..50776d671d 100644 --- a/src/router.rs +++ b/src/router.rs @@ -35,7 +35,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> { )) .routes(routes!( version::metadata::find_version, - version::metadata::update_version + version::update::update_version )) .routes(routes!(version::readme::get_version_readme)) .routes(routes!(version::dependencies::get_version_dependencies))