diff --git a/src/controllers.rs b/src/controllers.rs index 435d6dfbb05..5563b2eaced 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/krate.rs b/src/controllers/krate.rs index cfdfadee18b..6735169e22a 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 6321b6e609d..0038b549caf 100644 --- a/src/controllers/krate/metadata.rs +++ b/src/controllers/krate/metadata.rs @@ -5,20 +5,15 @@ //! `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 axum::response::{IntoResponse, Response}; +use crate::util::RequestUtils; +use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword, EncodableVersion}; use axum_extra::json; use axum_extra::response::ErasedJson; use diesel::prelude::*; @@ -244,80 +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) - } -} - -/// 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 00000000000..b4ee3e3a880 --- /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/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 565707895e3..8889e86984f 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 280f14999c0..4a604d16077 100644 --- a/src/controllers/user.rs +++ b/src/controllers/user.rs @@ -1,8 +1,8 @@ +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/email_notifications.rs b/src/controllers/user/email_notifications.rs new file mode 100644 index 00000000000..6814f37b458 --- /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/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 ef5084daf72..145a8e580af 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 8c3caf9fa07..a8484633bba 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -1,21 +1,18 @@ use crate::auth::AuthCheck; -use axum::extract::Path; -use axum::response::Response; use axum::Json; use axum_extra::json; 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}; -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. @@ -111,107 +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() -} - -#[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/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 cab488718a7..c225ad1d01c 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/controllers/version.rs b/src/controllers/version.rs index 78921fb37b3..2c1f1dca68a 100644 --- a/src/controllers/version.rs +++ b/src/controllers/version.rs @@ -1,5 +1,9 @@ +pub mod authors; +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/authors.rs b/src/controllers/version/authors.rs new file mode 100644 index 00000000000..85fee9efda2 --- /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/dependencies.rs b/src/controllers/version/dependencies.rs new file mode 100644 index 00000000000..82299f1575c --- /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 9cd541ec8a8..f83bee7ddb5 100644 --- a/src/controllers/version/metadata.rs +++ b/src/controllers/version/metadata.rs @@ -4,94 +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_database::schema::{crates, dependencies}; -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, Dependency, 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::worker::jobs::{SyncToGitIndex, SyncToSparseIndex, UpdateDefaultVersion}; +use crate::models::VersionOwnerAction; +use crate::util::errors::AppResult; +use crate::views::EncodableVersion; use super::CrateVersionPath; -#[derive(Deserialize)] -pub struct VersionUpdate { - yanked: Option, - yank_message: Option, -} -#[derive(Deserialize)] -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) -/// 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, @@ -109,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/readme.rs b/src/controllers/version/readme.rs new file mode 100644 index 00000000000..53df1cd84ee --- /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/controllers/version/update.rs b/src/controllers/version/update.rs new file mode 100644 index 00000000000..89371469a80 --- /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 4e69ef458bb..6410bbcfcdc 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 be8a480a363..50776d671de 100644 --- a/src/router.rs +++ b/src/router.rs @@ -35,12 +35,12 @@ pub fn build_axum_router(state: AppState) -> Router<()> { )) .routes(routes!( version::metadata::find_version, - version::metadata::update_version + version::update::update_version )) - .routes(routes!(krate::metadata::get_version_readme)) - .routes(routes!(version::metadata::get_version_dependencies)) + .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!( @@ -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)) @@ -76,15 +76,17 @@ 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)) + .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)) - .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