diff --git a/src/controllers/category.rs b/src/controllers/category.rs index 6384fcf880c..b7ea29d18b5 100644 --- a/src/controllers/category.rs +++ b/src/controllers/category.rs @@ -12,7 +12,14 @@ use diesel::QueryDsl; use diesel_async::RunQueryDsl; use http::request::Parts; -/// Handles the `GET /categories` route. +/// List all categories. +#[utoipa::path( + get, + path = "/api/v1/categories", + operation_id = "list_categories", + tag = "categories", + responses((status = 200, description = "Successful Response")), +)] pub async fn index(app: AppState, req: Parts) -> AppResult { // FIXME: There are 69 categories, 47 top level. This isn't going to // grow by an OoM. We need a limit for /summary, but we don't need @@ -41,7 +48,14 @@ pub async fn index(app: AppState, req: Parts) -> AppResult { })) } -/// Handles the `GET /categories/:category_id` route. +/// Get category metadata. +#[utoipa::path( + get, + path = "/api/v1/categories/{category}", + operation_id = "get_category", + tag = "categories", + responses((status = 200, description = "Successful Response")), +)] pub async fn show(state: AppState, Path(slug): Path) -> AppResult { let mut conn = state.db_read().await?; @@ -74,7 +88,14 @@ pub async fn show(state: AppState, Path(slug): Path) -> AppResult AppResult { let mut conn = state.db_read().await?; diff --git a/src/controllers/crate_owner_invitation.rs b/src/controllers/crate_owner_invitation.rs index b9b7383faf8..4bfd68d52b7 100644 --- a/src/controllers/crate_owner_invitation.rs +++ b/src/controllers/crate_owner_invitation.rs @@ -23,7 +23,14 @@ use http::request::Parts; use indexmap::IndexMap; use std::collections::{HashMap, HashSet}; -/// Handles the `GET /api/v1/me/crate_owner_invitations` route. +/// List all crate owner invitations for the authenticated user. +#[utoipa::path( + get, + path = "/api/v1/me/crate_owner_invitations", + operation_id = "list_crate_owner_invitations_for_user", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn list(app: AppState, req: Parts) -> AppResult { let mut conn = app.db_read().await?; let auth = AuthCheck::only_cookie().check(&req, &mut conn).await?; @@ -61,7 +68,14 @@ pub async fn list(app: AppState, req: Parts) -> AppResult { })) } -/// Handles the `GET /api/private/crate_owner_invitations` route. +/// List all crate owner invitations for a crate or user. +#[utoipa::path( + get, + path = "/api/private/crate_owner_invitations", + operation_id = "list_crate_owner_invitations", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn private_list(app: AppState, req: Parts) -> AppResult> { let mut conn = app.db_read().await?; let auth = AuthCheck::only_cookie().check(&req, &mut conn).await?; @@ -265,7 +279,14 @@ struct OwnerInvitation { crate_owner_invite: InvitationResponse, } -/// Handles the `PUT /api/v1/me/crate_owner_invitations/:crate_id` route. +/// Accept or decline a crate owner invitation. +#[utoipa::path( + put, + path = "/api/v1/me/crate_owner_invitations/{crate_id}", + operation_id = "handle_crate_owner_invitation", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn handle_invite(state: AppState, req: BytesRequest) -> AppResult { let (parts, body) = req.0.into_parts(); @@ -293,7 +314,14 @@ pub async fn handle_invite(state: AppState, req: BytesRequest) -> AppResult, diff --git a/src/controllers/keyword.rs b/src/controllers/keyword.rs index 6843b72461a..c7c4efbfab3 100644 --- a/src/controllers/keyword.rs +++ b/src/controllers/keyword.rs @@ -15,7 +15,14 @@ pub struct IndexQuery { sort: Option, } -/// Handles the `GET /keywords` route. +/// List all keywords. +#[utoipa::path( + get, + path = "/api/v1/keywords", + operation_id = "list_keywords", + tag = "keywords", + responses((status = 200, description = "Successful Response")), +)] pub async fn index(state: AppState, qp: Query, req: Parts) -> AppResult { use crate::schema::keywords; @@ -42,7 +49,14 @@ pub async fn index(state: AppState, qp: Query, req: Parts) -> AppRes })) } -/// Handles the `GET /keywords/:keyword_id` route. +/// Get keyword metadata. +#[utoipa::path( + get, + path = "/api/v1/keywords/{keyword}", + operation_id = "get_keyword", + tag = "keywords", + responses((status = 200, description = "Successful Response")), +)] pub async fn show(Path(name): Path, state: AppState) -> AppResult { let mut conn = state.db_read().await?; let kw = Keyword::find_by_keyword(&mut conn, &name).await?; diff --git a/src/controllers/krate.rs b/src/controllers/krate.rs index 93ee59d4798..ca1cd37cddb 100644 --- a/src/controllers/krate.rs +++ b/src/controllers/krate.rs @@ -1,4 +1,4 @@ -mod delete; +pub mod delete; pub mod downloads; pub mod follow; pub mod metadata; @@ -6,5 +6,3 @@ pub mod owners; pub mod publish; pub mod search; pub mod versions; - -pub use delete::delete; diff --git a/src/controllers/krate/delete.rs b/src/controllers/krate/delete.rs index 55bd9db4b3c..b08e4642061 100644 --- a/src/controllers/krate/delete.rs +++ b/src/controllers/krate/delete.rs @@ -18,12 +18,22 @@ use http::StatusCode; const DOWNLOADS_PER_MONTH_LIMIT: u64 = 100; const AVAILABLE_AFTER: TimeDelta = TimeDelta::hours(24); -/// Deletes a crate from the database, index and storage. +/// Delete a crate. +/// +/// The crate is immediately deleted from the database, and with a small delay +/// from the git and sparse index, and the crate file storage. /// /// The crate can only be deleted by the owner of the crate, and only if the /// crate has been published for less than 72 hours, or if the crate has a /// single owner, has been downloaded less than 100 times for each month it has /// been published, and is not depended upon by any other crate on crates.io. +#[utoipa::path( + delete, + path = "/api/v1/crates/{name}", + operation_id = "delete_crate", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] pub async fn delete( Path(name): Path, parts: Parts, diff --git a/src/controllers/krate/downloads.rs b/src/controllers/krate/downloads.rs index 3ff2dbba87e..3d731aa0588 100644 --- a/src/controllers/krate/downloads.rs +++ b/src/controllers/krate/downloads.rs @@ -16,7 +16,18 @@ use diesel::prelude::*; use diesel_async::RunQueryDsl; use std::cmp; -/// Handles the `GET /crates/:crate_id/downloads` route. +/// Get the download counts for a crate. +/// +/// This includes the per-day downloads for the last 90 days and for the +/// latest 5 versions plus the sum of the rest. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/downloads", + operation_id = "get_crate_downloads", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] + pub async fn downloads(state: AppState, Path(crate_name): Path) -> AppResult { let mut conn = state.db_read().await?; diff --git a/src/controllers/krate/follow.rs b/src/controllers/krate/follow.rs index d000a10c532..c0e2dc2985b 100644 --- a/src/controllers/krate/follow.rs +++ b/src/controllers/krate/follow.rs @@ -29,7 +29,14 @@ async fn follow_target( Ok(Follow { user_id, crate_id }) } -/// Handles the `PUT /crates/:crate_id/follow` route. +/// Follow a crate. +#[utoipa::path( + put, + path = "/api/v1/crates/{name}/follow", + operation_id = "follow_crate", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] pub async fn follow( app: AppState, Path(crate_name): Path, @@ -47,7 +54,14 @@ pub async fn follow( ok_true() } -/// Handles the `DELETE /crates/:crate_id/follow` route. +/// Unfollow a crate. +#[utoipa::path( + delete, + path = "/api/v1/crates/{name}/follow", + operation_id = "unfollow_crate", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] pub async fn unfollow( app: AppState, Path(crate_name): Path, @@ -61,7 +75,14 @@ pub async fn unfollow( ok_true() } -/// Handles the `GET /crates/:crate_id/following` route. +/// Check if a crate is followed. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/following", + operation_id = "get_following_crate", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] pub async fn following( app: AppState, Path(crate_name): Path, diff --git a/src/controllers/krate/metadata.rs b/src/controllers/krate/metadata.rs index 8be893ba887..f0befa72aa7 100644 --- a/src/controllers/krate/metadata.rs +++ b/src/controllers/krate/metadata.rs @@ -26,12 +26,29 @@ use http::request::Parts; use std::cmp::Reverse; use std::str::FromStr; -/// Handles the `GET /crates/new` special case. +/// Get crate metadata (for the `new` crate). +/// +/// This endpoint works around a small limitation in `axum` and is delegating +/// to the `GET /api/v1/crates/{name}` endpoint internally. +#[utoipa::path( + get, + path = "/api/v1/crates/new", + operation_id = "crates_show_new", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] pub async fn show_new(app: AppState, req: Parts) -> AppResult { show(app, Path("new".to_string()), req).await } -/// Handles the `GET /crates/:crate_id` route. +/// Get crate metadata. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}", + operation_id = "get_crate", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] pub async fn show(app: AppState, Path(name): Path, req: Parts) -> AppResult { let mut conn = app.db_read().await?; @@ -227,7 +244,14 @@ impl FromStr for ShowIncludeMode { } } -/// Handles the `GET /crates/:crate_id/:version/readme` route. +/// Get the readme of a crate version. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/{version}/readme", + operation_id = "get_version_readme", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn readme( app: AppState, Path((crate_name, version)): Path<(String, String)>, @@ -241,7 +265,14 @@ pub async fn readme( } } -/// Handles the `GET /crates/:crate_id/reverse_dependencies` route. +/// List reverse dependencies of a crate. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/reverse_dependencies", + operation_id = "list_reverse_dependencies", + tag = "crates", + responses((status = 200, description = "Successful Response")), +)] pub async fn reverse_dependencies( app: AppState, Path(name): Path, diff --git a/src/controllers/krate/owners.rs b/src/controllers/krate/owners.rs index 05a3451a5a7..6fdf2584f37 100644 --- a/src/controllers/krate/owners.rs +++ b/src/controllers/krate/owners.rs @@ -17,7 +17,14 @@ use http::request::Parts; use http::StatusCode; use secrecy::{ExposeSecret, SecretString}; -/// Handles the `GET /crates/:crate_id/owners` route. +/// List crate owners. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/owners", + operation_id = "list_owners", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn owners(state: AppState, Path(crate_name): Path) -> AppResult { let mut conn = state.db_read().await?; @@ -37,7 +44,14 @@ pub async fn owners(state: AppState, Path(crate_name): Path) -> AppResul Ok(json!({ "users": owners })) } -/// Handles the `GET /crates/:crate_id/owner_team` route. +/// List team owners of a crate. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/owner_team", + operation_id = "get_team_owners", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn owner_team(state: AppState, Path(crate_name): Path) -> AppResult { let mut conn = state.db_read().await?; let krate: Crate = Crate::by_name(&crate_name) @@ -55,7 +69,14 @@ pub async fn owner_team(state: AppState, Path(crate_name): Path) -> AppR Ok(json!({ "teams": owners })) } -/// Handles the `GET /crates/:crate_id/owner_user` route. +/// List user owners of a crate. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/owner_user", + operation_id = "get_user_owners", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn owner_user(state: AppState, Path(crate_name): Path) -> AppResult { let mut conn = state.db_read().await?; @@ -74,7 +95,14 @@ pub async fn owner_user(state: AppState, Path(crate_name): Path) -> AppR Ok(json!({ "users": owners })) } -/// Handles the `PUT /crates/:crate_id/owners` route. +/// Add crate owners. +#[utoipa::path( + put, + path = "/api/v1/crates/{name}/owners", + operation_id = "add_owners", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn add_owners( app: AppState, Path(crate_name): Path, @@ -84,7 +112,14 @@ pub async fn add_owners( modify_owners(app, crate_name, parts, body, true).await } -/// Handles the `DELETE /crates/:crate_id/owners` route. +/// Remove crate owners. +#[utoipa::path( + delete, + path = "/api/v1/crates/{name}/owners", + operation_id = "delete_owners", + tag = "owners", + responses((status = 200, description = "Successful Response")), +)] pub async fn remove_owners( app: AppState, Path(crate_name): Path, diff --git a/src/controllers/krate/publish.rs b/src/controllers/krate/publish.rs index fe938360d23..82a30e6ff00 100644 --- a/src/controllers/krate/publish.rs +++ b/src/controllers/krate/publish.rs @@ -49,9 +49,17 @@ const MISSING_RIGHTS_ERROR_MESSAGE: &str = "this crate exists but you don't seem const MAX_DESCRIPTION_LENGTH: usize = 1000; -/// Handles the `PUT /crates/new` route. +/// Publish a new crate/version. +/// /// Used by `cargo publish` to publish a new crate or to publish a new version of an /// existing crate. +#[utoipa::path( + put, + path = "/api/v1/crates/new", + operation_id = "publish", + tag = "publish", + responses((status = 200, description = "Successful Response")), +)] pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult> { let stream = body.into_data_stream(); let stream = stream.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)); diff --git a/src/controllers/krate/versions.rs b/src/controllers/krate/versions.rs index 43761016122..5dd370a2418 100644 --- a/src/controllers/krate/versions.rs +++ b/src/controllers/krate/versions.rs @@ -20,7 +20,14 @@ use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError use crate::util::RequestUtils; use crate::views::EncodableVersion; -/// Handles the `GET /crates/:crate_id/versions` route. +/// List all versions of a crate. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/versions", + operation_id = "list_crate_versions", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn versions( state: AppState, Path(crate_name): Path, diff --git a/src/controllers/site_metadata.rs b/src/controllers/site_metadata.rs index 910055458e5..7d4515f2d5f 100644 --- a/src/controllers/site_metadata.rs +++ b/src/controllers/site_metadata.rs @@ -2,10 +2,17 @@ use crate::app::AppState; use axum::response::IntoResponse; use axum_extra::json; -/// Returns the JSON representation of the current deployed commit sha. +/// Get crates.io metadata. /// -/// The sha is contained within the `HEROKU_SLUG_COMMIT` environment variable. -/// If `HEROKU_SLUG_COMMIT` is not set, returns `"unknown"`. +/// Returns the current deployed commit SHA1 (or `unknown`), and whether the +/// system is in read-only mode. +#[utoipa::path( + get, + path = "/api/v1/site_metadata", + operation_id = "get_site_metadata", + tag = "other", + responses((status = 200, description = "Successful Response")), +)] pub async fn show_deployed_sha(state: AppState) -> impl IntoResponse { let read_only = state.config.db.are_all_read_only(); diff --git a/src/controllers/summary.rs b/src/controllers/summary.rs index 452cb7d5c87..c803d892422 100644 --- a/src/controllers/summary.rs +++ b/src/controllers/summary.rs @@ -12,7 +12,17 @@ use diesel_async::{AsyncPgConnection, RunQueryDsl}; use futures_util::FutureExt; use std::future::Future; -/// Handles the `GET /summary` route. +/// Get front page data. +/// +/// This endpoint returns a summary of the most important data for the front +/// page of crates.io. +#[utoipa::path( + get, + path = "/api/v1/summary", + operation_id = "get_summary", + tag = "other", + responses((status = 200, description = "Successful Response")), +)] pub async fn summary(state: AppState) -> AppResult { let mut conn = state.db_read().await?; diff --git a/src/controllers/team.rs b/src/controllers/team.rs index b1263ae463d..ccfc482e3b8 100644 --- a/src/controllers/team.rs +++ b/src/controllers/team.rs @@ -8,7 +8,14 @@ use axum_extra::response::ErasedJson; use diesel::prelude::*; use diesel_async::RunQueryDsl; -/// Handles the `GET /teams/:team_id` route. +/// Find team by login. +#[utoipa::path( + get, + path = "/api/v1/teams/{team}", + operation_id = "get_team", + tag = "teams", + responses((status = 200, description = "Successful Response")), +)] pub async fn show_team(state: AppState, Path(name): Path) -> AppResult { use crate::schema::teams::dsl::{login, teams}; diff --git a/src/controllers/token.rs b/src/controllers/token.rs index 8f6a4d2c301..14f0a06626d 100644 --- a/src/controllers/token.rs +++ b/src/controllers/token.rs @@ -35,7 +35,14 @@ impl GetParams { } } -/// Handles the `GET /me/tokens` route. +/// List all API tokens of the authenticated user. +#[utoipa::path( + get, + path = "/api/v1/me/tokens", + operation_id = "list_api_tokens", + tag = "api_tokens", + responses((status = 200, description = "Successful Response")), +)] pub async fn list( app: AppState, Query(params): Query, @@ -76,7 +83,14 @@ pub struct NewApiTokenRequest { api_token: NewApiToken, } -/// Handles the `PUT /me/tokens` route. +/// Create a new API token. +#[utoipa::path( + put, + path = "/api/v1/me/tokens", + operation_id = "create_api_token", + tag = "api_tokens", + responses((status = 200, description = "Successful Response")), +)] pub async fn new( app: AppState, parts: Parts, @@ -165,7 +179,14 @@ pub async fn new( Ok(json!({ "api_token": api_token })) } -/// Handles the `GET /me/tokens/:id` route. +/// Find API token by id. +#[utoipa::path( + get, + path = "/api/v1/me/tokens/{id}", + operation_id = "get_api_token", + tag = "api_tokens", + responses((status = 200, description = "Successful Response")), +)] pub async fn show(app: AppState, Path(id): Path, req: Parts) -> AppResult { let mut conn = app.db_write().await?; let auth = AuthCheck::default().check(&req, &mut conn).await?; @@ -179,7 +200,14 @@ pub async fn show(app: AppState, Path(id): Path, req: Parts) -> AppResult, req: Parts) -> AppResult { let mut conn = app.db_write().await?; let auth = AuthCheck::default().check(&req, &mut conn).await?; @@ -192,7 +220,17 @@ pub async fn revoke(app: AppState, Path(id): Path, req: Parts) -> AppResult Ok(json!({})) } -/// Handles the `DELETE /tokens/current` route. +/// Revoke the current API token. +/// +/// This endpoint revokes the API token that is used to authenticate +/// the request. +#[utoipa::path( + delete, + path = "/api/v1/tokens/current", + operation_id = "revoke_current_api_token", + tag = "api_tokens", + responses((status = 200, description = "Successful Response")), +)] pub async fn revoke_current(app: AppState, req: Parts) -> AppResult { let mut conn = app.db_write().await?; let auth = AuthCheck::default().check(&req, &mut conn).await?; diff --git a/src/controllers/user.rs b/src/controllers/user.rs index 4226f0f3a0e..250a658be14 100644 --- a/src/controllers/user.rs +++ b/src/controllers/user.rs @@ -1,6 +1,6 @@ pub mod me; pub mod other; -mod resend; +pub mod resend; pub mod session; pub mod update; diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index 210db02e4c3..a4c0d3bf00c 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -19,7 +19,14 @@ use crate::util::errors::{bad_request, AppResult}; use crate::util::BytesRequest; use crate::views::{EncodableMe, EncodablePrivateUser, EncodableVersion, OwnedCrate}; -/// Handles the `GET /me` route. +/// Get the currently authenticated user. +#[utoipa::path( + get, + path = "/api/v1/me", + operation_id = "get_authenticated_user", + tag = "users", + responses((status = 200, description = "Successful Response")), +)] pub async fn me(app: AppState, req: Parts) -> AppResult> { let mut conn = app.db_read_prefer_primary().await?; let user_id = AuthCheck::only_cookie() @@ -62,7 +69,14 @@ pub async fn me(app: AppState, req: Parts) -> AppResult> { })) } -/// Handles the `GET /me/updates` route. +/// List versions of crates that the authenticated user follows. +#[utoipa::path( + get, + path = "/api/v1/me/updates", + operation_id = "get_authenticated_user_updates", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn updates(app: AppState, req: Parts) -> AppResult { let mut conn = app.db_read_prefer_primary().await?; let auth = AuthCheck::only_cookie().check(&req, &mut conn).await?; @@ -101,7 +115,14 @@ pub async fn updates(app: AppState, req: Parts) -> AppResult { })) } -/// Handles the `PUT /confirm/:email_token` route +/// Marks the email belonging to the given token as verified. +#[utoipa::path( + put, + path = "/api/v1/confirm/{email_token}", + operation_id = "confirm_user_email", + tag = "users", + responses((status = 200, description = "Successful Response")), +)] pub async fn confirm_user_email(state: AppState, Path(token): Path) -> AppResult { use diesel::update; @@ -119,7 +140,18 @@ pub async fn confirm_user_email(state: AppState, Path(token): Path) -> A ok_true() } -/// Handles `PUT /me/email_notifications` route +/// 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", + operation_id = "update_email_notifications", + tag = "users", + responses((status = 200, description = "Successful Response")), +)] +#[deprecated] pub async fn update_email_notifications(app: AppState, req: BytesRequest) -> AppResult { use diesel::pg::upsert::excluded; diff --git a/src/controllers/user/other.rs b/src/controllers/user/other.rs index 8ba6459abb3..05602a415b2 100644 --- a/src/controllers/user/other.rs +++ b/src/controllers/user/other.rs @@ -12,7 +12,14 @@ use crate::sql::lower; use crate::util::errors::AppResult; use crate::views::EncodablePublicUser; -/// Handles the `GET /users/:user_id` route. +/// Find user by login. +#[utoipa::path( + get, + path = "/api/v1/users/{user}", + operation_id = "get_user", + tag = "users", + responses((status = 200, description = "Successful Response")), +)] pub async fn show(state: AppState, Path(user_name): Path) -> AppResult { let mut conn = state.db_read_prefer_primary().await?; @@ -28,7 +35,17 @@ pub async fn show(state: AppState, Path(user_name): Path) -> AppResult) -> AppResult { let mut conn = state.db_read_prefer_primary().await?; diff --git a/src/controllers/user/resend.rs b/src/controllers/user/resend.rs index d73f47697e9..3a743848a9d 100644 --- a/src/controllers/user/resend.rs +++ b/src/controllers/user/resend.rs @@ -14,7 +14,14 @@ use diesel_async::scoped_futures::ScopedFutureExt; use diesel_async::{AsyncConnection, RunQueryDsl}; use http::request::Parts; -/// Handles `PUT /user/:user_id/resend` route +/// Regenerate and send an email verification token. +#[utoipa::path( + put, + path = "/api/v1/users/{id}/resend", + operation_id = "resend_email_verification", + tag = "users", + responses((status = 200, description = "Successful Response")), +)] pub async fn regenerate_token_and_send( state: AppState, Path(param_user_id): Path, diff --git a/src/controllers/user/session.rs b/src/controllers/user/session.rs index e555573a1e4..84566e701ad 100644 --- a/src/controllers/user/session.rs +++ b/src/controllers/user/session.rs @@ -19,7 +19,7 @@ use crate::util::errors::{bad_request, server_error, AppResult}; use crate::views::EncodableMe; use crates_io_github::GithubUser; -/// Handles the `GET /api/private/session/begin` route. +/// Begin authentication flow. /// /// This route will return an authorization URL for the GitHub OAuth flow including the crates.io /// `client_id` and a randomly generated `state` secret. @@ -34,6 +34,13 @@ use crates_io_github::GithubUser; /// "url": "https://github.com/login/oauth/authorize?client_id=...&state=...&scope=read%3Aorg" /// } /// ``` +#[utoipa::path( + get, + path = "/api/private/session/begin", + operation_id = "begin_session", + tag = "session", + responses((status = 200, description = "Successful Response")), +)] pub async fn begin(app: AppState, session: SessionExtension) -> ErasedJson { let (url, state) = app .github_oauth @@ -54,7 +61,7 @@ pub struct AuthorizeQuery { state: CsrfToken, } -/// Handles the `GET /api/private/session/authorize` route. +/// Complete authentication flow. /// /// This route is called from the GitHub API OAuth flow after the user accepted or rejected /// the data access permissions. It will check the `state` parameter and then call the GitHub API @@ -72,7 +79,6 @@ pub struct AuthorizeQuery { /// /// ```json /// { -/// "api_token": "b84a63c4ea3fcb4ac84", /// "user": { /// "email": "foo@bar.org", /// "name": "Foo Bar", @@ -82,6 +88,13 @@ pub struct AuthorizeQuery { /// } /// } /// ``` +#[utoipa::path( + get, + path = "/api/private/session/authorize", + operation_id = "authorize_session", + tag = "session", + responses((status = 200, description = "Successful Response")), +)] pub async fn authorize( query: AuthorizeQuery, app: AppState, @@ -158,7 +171,14 @@ async fn find_user_by_gh_id(conn: &mut AsyncPgConnection, gh_id: i32) -> QueryRe .optional() } -/// Handles the `DELETE /api/private/session` route. +/// End the current session. +#[utoipa::path( + delete, + path = "/api/private/session", + operation_id = "end_session", + tag = "session", + responses((status = 200, description = "Successful Response")), +)] pub async fn logout(session: SessionExtension) -> Json { session.remove("user_id"); Json(true) diff --git a/src/controllers/user/update.rs b/src/controllers/user/update.rs index 9c14a308e2c..06e934ee7c9 100644 --- a/src/controllers/user/update.rs +++ b/src/controllers/user/update.rs @@ -24,7 +24,18 @@ pub struct User { publish_notifications: Option, } -/// Handles the `PUT /users/:user_id` route. +/// Update user settings. +/// +/// This endpoint allows users to update their email address and publish notifications settings. +/// +/// The `id` parameter needs to match the ID of the currently authenticated user. +#[utoipa::path( + put, + path = "/api/v1/users/{user}", + operation_id = "update_user", + tag = "users", + responses((status = 200, description = "Successful Response")), +)] pub async fn update_user( state: AppState, Path(param_user_id): Path, diff --git a/src/controllers/version/downloads.rs b/src/controllers/version/downloads.rs index 28390e2b908..92841325a5d 100644 --- a/src/controllers/version/downloads.rs +++ b/src/controllers/version/downloads.rs @@ -18,8 +18,16 @@ use diesel::prelude::*; use diesel_async::RunQueryDsl; use http::request::Parts; -/// Handles the `GET /crates/:crate_id/:version/download` route. +/// Download a crate version. +/// /// This returns a URL to the location where the crate is stored. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/{version}/download", + operation_id = "download_version", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn download( app: AppState, Path((crate_name, version)): Path<(String, String)>, @@ -34,7 +42,16 @@ pub async fn download( } } -/// Handles the `GET /crates/:crate_id/:version/downloads` route. +/// Get the download counts for a crate version. +/// +/// This includes the per-day downloads for the last 90 days. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/{version}/downloads", + operation_id = "get_version_downloads", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn downloads( app: AppState, Path((crate_name, version)): Path<(String, String)>, diff --git a/src/controllers/version/metadata.rs b/src/controllers/version/metadata.rs index 61130b80bf0..01f15fa7885 100644 --- a/src/controllers/version/metadata.rs +++ b/src/controllers/version/metadata.rs @@ -40,13 +40,20 @@ pub struct VersionUpdateRequest { version: VersionUpdate, } -/// Handles the `GET /crates/:crate_id/:version/dependencies` route. +/// Get crate version dependencies. /// -/// This information can be obtained directly from the index. +/// 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", + operation_id = "get_version_dependencies", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn dependencies( state: AppState, Path((crate_name, version)): Path<(String, String)>, @@ -71,21 +78,33 @@ pub async fn dependencies( Ok(json!({ "dependencies": deps })) } -/// Handles the `GET /crates/:crate_id/:version/authors` route. +/// 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", + operation_id = "get_version_authors", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] +#[deprecated] pub async fn authors() -> ErasedJson { - // Currently we return the empty list. - // Because the API is not used anymore after RFC https://github.com/rust-lang/rfcs/pull/3052. - json!({ "users": [], "meta": { "names": [] }, }) } -/// Handles the `GET /crates/:crate/:version` route. -/// -/// The frontend doesn't appear to hit this endpoint, but our tests do, and it seems to be a useful -/// API route to have. +/// Get crate version metadata. +#[utoipa::path( + get, + path = "/api/v1/crates/{name}/{version}", + operation_id = "get_version", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn show( state: AppState, Path((crate_name, version)): Path<(String, String)>, @@ -103,9 +122,16 @@ pub async fn show( Ok(json!({ "version": version })) } -/// Handles the `PATCH /crates/:crate/:version` route. +/// Update a crate version. /// -/// This endpoint allows updating the yanked state of a version, including a yank message. +/// This endpoint allows updating the `yanked` state of a version, including a yank message. +#[utoipa::path( + patch, + path = "/api/v1/crates/{name}/{version}", + operation_id = "update_version", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn update( state: AppState, Path((crate_name, version)): Path<(String, String)>, diff --git a/src/controllers/version/yank.rs b/src/controllers/version/yank.rs index c9e18f65074..06b8cac5cb8 100644 --- a/src/controllers/version/yank.rs +++ b/src/controllers/version/yank.rs @@ -10,15 +10,24 @@ use axum::extract::Path; use axum::response::Response; use http::request::Parts; -/// Handles the `DELETE /crates/:crate_id/:version/yank` route. +/// Yank a crate version. +/// /// This does not delete a crate version, it makes the crate /// version accessible only to crates that already have a /// `Cargo.lock` containing this version. /// /// Notes: -/// Crate deletion is not implemented to avoid breaking builds, +/// +/// Version deletion is not implemented to avoid breaking builds, /// and the goal of yanking a crate is to prevent crates /// beginning to depend on the yanked crate version. +#[utoipa::path( + delete, + path = "/api/v1/crates/{name}/{version}/yank", + operation_id = "yank_version", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn yank( app: AppState, Path((crate_name, version)): Path<(String, String)>, @@ -27,7 +36,14 @@ pub async fn yank( modify_yank(crate_name, version, app, req, true).await } -/// Handles the `PUT /crates/:crate_id/:version/unyank` route. +/// Unyank a crate version. +#[utoipa::path( + put, + path = "/api/v1/crates/{name}/{version}/unyank", + operation_id = "unyank_version", + tag = "versions", + responses((status = 200, description = "Successful Response")), +)] pub async fn unyank( app: AppState, Path((crate_name, version)): Path<(String, String)>, diff --git a/src/router.rs b/src/router.rs index 1da715e7103..24758d92405 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,163 +1,75 @@ use axum::response::IntoResponse; -use axum::routing::{delete, get, post, put}; +use axum::routing::{get, post}; use axum::{Json, Router}; use http::{Method, StatusCode}; use utoipa_axum::routes; use crate::app::AppState; -use crate::controllers::user::update_user; use crate::controllers::*; use crate::openapi::BaseOpenApi; use crate::util::errors::not_found; use crate::Env; +#[allow(deprecated)] pub fn build_axum_router(state: AppState) -> Router<()> { let (router, openapi) = BaseOpenApi::router() + // Route used by both `cargo search` and the frontend + .routes(routes!(krate::search::search)) + // Routes used by `cargo` + .routes(routes!(krate::publish::publish, krate::metadata::show_new)) .routes(routes!( - // Route used by both `cargo search` and the frontend - krate::search::search + krate::owners::owners, + krate::owners::add_owners, + krate::owners::remove_owners )) + .routes(routes!(version::yank::yank)) + .routes(routes!(version::yank::unyank)) + .routes(routes!(version::downloads::download)) + // Routes used by the frontend + .routes(routes!(krate::metadata::show, krate::delete::delete)) + .routes(routes!(version::metadata::show, version::metadata::update)) + .routes(routes!(krate::metadata::readme)) + .routes(routes!(version::metadata::dependencies)) + .routes(routes!(version::downloads::downloads)) + .routes(routes!(version::metadata::authors)) + .routes(routes!(krate::downloads::downloads)) + .routes(routes!(krate::versions::versions)) + .routes(routes!(krate::follow::follow, krate::follow::unfollow)) + .routes(routes!(krate::follow::following)) + .routes(routes!(krate::owners::owner_team)) + .routes(routes!(krate::owners::owner_user)) + .routes(routes!(krate::metadata::reverse_dependencies)) + .routes(routes!(keyword::index)) + .routes(routes!(keyword::show)) + .routes(routes!(category::index)) + .routes(routes!(category::show)) + .routes(routes!(category::slugs)) + .routes(routes!(user::other::show, user::update::update_user)) + .routes(routes!(user::other::stats)) + .routes(routes!(team::show_team)) + .routes(routes!(user::me::me)) + .routes(routes!(user::me::updates)) + .routes(routes!(token::list, token::new)) + .routes(routes!(token::show, token::revoke)) + .routes(routes!(token::revoke_current)) + .routes(routes!(crate_owner_invitation::list)) + .routes(routes!(crate_owner_invitation::private_list)) + .routes(routes!(crate_owner_invitation::handle_invite)) + .routes(routes!(crate_owner_invitation::handle_invite_with_token)) + .routes(routes!(user::me::update_email_notifications)) + .routes(routes!(summary::summary)) + .routes(routes!(user::me::confirm_user_email)) + .routes(routes!(user::resend::regenerate_token_and_send)) + .routes(routes!(site_metadata::show_deployed_sha)) + // Session management + .routes(routes!(user::session::begin)) + .routes(routes!(user::session::authorize)) + .routes(routes!(user::session::logout)) .split_for_parts(); let mut router = router - // Routes used by `cargo` - .route( - "/api/v1/crates/new", - put(krate::publish::publish).get(krate::metadata::show_new), - ) - .route( - "/api/v1/crates/:crate_id/owners", - get(krate::owners::owners) - .put(krate::owners::add_owners) - .delete(krate::owners::remove_owners), - ) - .route( - "/api/v1/crates/:crate_id/:version/yank", - delete(version::yank::yank), - ) - .route( - "/api/v1/crates/:crate_id/:version/unyank", - put(version::yank::unyank), - ) - .route( - "/api/v1/crates/:crate_id/:version/download", - get(version::downloads::download), - ) - // Routes used by the frontend - .route( - "/api/v1/crates/:crate_id", - get(krate::metadata::show).delete(krate::delete), - ) - .route( - "/api/v1/crates/:crate_id/:version", - get(version::metadata::show).patch(version::metadata::update), - ) - .route( - "/api/v1/crates/:crate_id/:version/readme", - get(krate::metadata::readme), - ) - .route( - "/api/v1/crates/:crate_id/:version/dependencies", - get(version::metadata::dependencies), - ) - .route( - "/api/v1/crates/:crate_id/:version/downloads", - get(version::downloads::downloads), - ) - .route( - "/api/v1/crates/:crate_id/:version/authors", - get(version::metadata::authors), - ) - .route( - "/api/v1/crates/:crate_id/downloads", - get(krate::downloads::downloads), - ) - .route( - "/api/v1/crates/:crate_id/versions", - get(krate::versions::versions), - ) - .route( - "/api/v1/crates/:crate_id/follow", - put(krate::follow::follow).delete(krate::follow::unfollow), - ) - .route( - "/api/v1/crates/:crate_id/following", - get(krate::follow::following), - ) - .route( - "/api/v1/crates/:crate_id/owner_team", - get(krate::owners::owner_team), - ) - .route( - "/api/v1/crates/:crate_id/owner_user", - get(krate::owners::owner_user), - ) - .route( - "/api/v1/crates/:crate_id/reverse_dependencies", - get(krate::metadata::reverse_dependencies), - ) - .route("/api/v1/keywords", get(keyword::index)) - .route("/api/v1/keywords/:keyword_id", get(keyword::show)) - .route("/api/v1/categories", get(category::index)) - .route("/api/v1/categories/:category_id", get(category::show)) - .route("/api/v1/category_slugs", get(category::slugs)) - .route( - "/api/v1/users/:user_id", - get(user::other::show).put(update_user), - ) - .route("/api/v1/users/:user_id/stats", get(user::other::stats)) - .route("/api/v1/teams/:team_id", get(team::show_team)) - .route("/api/v1/me", get(user::me::me)) - .route("/api/v1/me/updates", get(user::me::updates)) - .route("/api/v1/me/tokens", get(token::list).put(token::new)) - .route( - "/api/v1/me/tokens/:id", - get(token::show).delete(token::revoke), - ) - .route("/api/v1/tokens/current", delete(token::revoke_current)) - .route( - "/api/v1/me/crate_owner_invitations", - get(crate_owner_invitation::list), - ) - .route( - "/api/v1/me/crate_owner_invitations/:crate_id", - put(crate_owner_invitation::handle_invite), - ) - .route( - "/api/v1/me/crate_owner_invitations/accept/:token", - put(crate_owner_invitation::handle_invite_with_token), - ) - .route( - "/api/v1/me/email_notifications", - put(user::me::update_email_notifications), - ) - .route("/api/v1/summary", get(summary::summary)) - .route( - "/api/v1/confirm/:email_token", - put(user::me::confirm_user_email), - ) - .route( - "/api/v1/users/:user_id/resend", - put(user::regenerate_token_and_send), - ) - .route( - "/api/v1/site_metadata", - get(site_metadata::show_deployed_sha), - ) - // Session management - .route("/api/private/session/begin", get(user::session::begin)) - .route( - "/api/private/session/authorize", - get(user::session::authorize), - ) - .route("/api/private/session", delete(user::session::logout)) // Metrics .route("/api/private/metrics/:kind", get(metrics::prometheus)) - // Crate ownership invitations management in the frontend - .route( - "/api/private/crate_owner_invitations", - get(crate_owner_invitation::private_list), - ) // Alerts from GitHub scanning for exposed API tokens .route( "/api/github/secret-scanning/verify", diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index bb539c39650..325db33001b 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -20,6 +20,120 @@ snapshot_kind: text }, "openapi": "3.1.0", "paths": { + "/api/private/crate_owner_invitations": { + "get": { + "operationId": "list_crate_owner_invitations", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List all crate owner invitations for a crate or user.", + "tags": [ + "owners" + ] + } + }, + "/api/private/session": { + "delete": { + "operationId": "end_session", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "End the current session.", + "tags": [ + "session" + ] + } + }, + "/api/private/session/authorize": { + "get": { + "description": "This route is called from the GitHub API OAuth flow after the user accepted or rejected\nthe data access permissions. It will check the `state` parameter and then call the GitHub API\nto exchange the temporary `code` for an API token. The API token is returned together with\nthe corresponding user information.\n\nsee \n\n## Query Parameters\n\n- `code` – temporary code received from the GitHub API **(Required)**\n- `state` – state parameter received from the GitHub API **(Required)**\n\n## Response Body Example\n\n```json\n{\n \"user\": {\n \"email\": \"foo@bar.org\",\n \"name\": \"Foo Bar\",\n \"login\": \"foobar\",\n \"avatar\": \"https://avatars.githubusercontent.com/u/1234\",\n \"url\": null\n }\n}\n```", + "operationId": "authorize_session", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Complete authentication flow.", + "tags": [ + "session" + ] + } + }, + "/api/private/session/begin": { + "get": { + "description": "This route will return an authorization URL for the GitHub OAuth flow including the crates.io\n`client_id` and a randomly generated `state` secret.\n\nsee \n\n## Response Body Example\n\n```json\n{\n \"state\": \"b84a63c4ea3fcb4ac84\",\n \"url\": \"https://github.com/login/oauth/authorize?client_id=...&state=...&scope=read%3Aorg\"\n}\n```", + "operationId": "begin_session", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Begin authentication flow.", + "tags": [ + "session" + ] + } + }, + "/api/v1/categories": { + "get": { + "operationId": "list_categories", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List all categories.", + "tags": [ + "categories" + ] + } + }, + "/api/v1/categories/{category}": { + "get": { + "operationId": "get_category", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get category metadata.", + "tags": [ + "categories" + ] + } + }, + "/api/v1/category_slugs": { + "get": { + "operationId": "list_category_slugs", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List all available category slugs.", + "tags": [ + "categories" + ] + } + }, + "/api/v1/confirm/{email_token}": { + "put": { + "operationId": "confirm_user_email", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Marks the email belonging to the given token as verified.", + "tags": [ + "users" + ] + } + }, "/api/v1/crates": { "get": { "description": "Called in a variety of scenarios in the front end, including:\n- Alphabetical listing of crates\n- List of crates under a specific owner\n- Listing a user's followed crates", @@ -34,6 +148,622 @@ snapshot_kind: text "crates" ] } + }, + "/api/v1/crates/new": { + "get": { + "description": "This endpoint works around a small limitation in `axum` and is delegating\nto the `GET /api/v1/crates/{name}` endpoint internally.", + "operationId": "crates_show_new", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get crate metadata (for the `new` crate).", + "tags": [ + "crates" + ] + }, + "put": { + "description": "Used by `cargo publish` to publish a new crate or to publish a new version of an\nexisting crate.", + "operationId": "publish", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Publish a new crate/version.", + "tags": [ + "publish" + ] + } + }, + "/api/v1/crates/{name}": { + "delete": { + "description": "The crate is immediately deleted from the database, and with a small delay\nfrom the git and sparse index, and the crate file storage.\n\nThe crate can only be deleted by the owner of the crate, and only if the\ncrate has been published for less than 72 hours, or if the crate has a\nsingle owner, has been downloaded less than 100 times for each month it has\nbeen published, and is not depended upon by any other crate on crates.io.", + "operationId": "delete_crate", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Delete a crate.", + "tags": [ + "crates" + ] + }, + "get": { + "operationId": "get_crate", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get crate metadata.", + "tags": [ + "crates" + ] + } + }, + "/api/v1/crates/{name}/downloads": { + "get": { + "description": "This includes the per-day downloads for the last 90 days and for the\nlatest 5 versions plus the sum of the rest.", + "operationId": "get_crate_downloads", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get the download counts for a crate.", + "tags": [ + "crates" + ] + } + }, + "/api/v1/crates/{name}/follow": { + "delete": { + "operationId": "unfollow_crate", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Unfollow a crate.", + "tags": [ + "crates" + ] + }, + "put": { + "operationId": "follow_crate", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Follow a crate.", + "tags": [ + "crates" + ] + } + }, + "/api/v1/crates/{name}/following": { + "get": { + "operationId": "get_following_crate", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Check if a crate is followed.", + "tags": [ + "crates" + ] + } + }, + "/api/v1/crates/{name}/owner_team": { + "get": { + "operationId": "get_team_owners", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List team owners of a crate.", + "tags": [ + "owners" + ] + } + }, + "/api/v1/crates/{name}/owner_user": { + "get": { + "operationId": "get_user_owners", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List user owners of a crate.", + "tags": [ + "owners" + ] + } + }, + "/api/v1/crates/{name}/owners": { + "delete": { + "operationId": "delete_owners", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Remove crate owners.", + "tags": [ + "owners" + ] + }, + "get": { + "operationId": "list_owners", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List crate owners.", + "tags": [ + "owners" + ] + }, + "put": { + "operationId": "add_owners", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Add crate owners.", + "tags": [ + "owners" + ] + } + }, + "/api/v1/crates/{name}/reverse_dependencies": { + "get": { + "operationId": "list_reverse_dependencies", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List reverse dependencies of a crate.", + "tags": [ + "crates" + ] + } + }, + "/api/v1/crates/{name}/versions": { + "get": { + "operationId": "list_crate_versions", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List all versions of a crate.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}": { + "get": { + "operationId": "get_version", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get crate version metadata.", + "tags": [ + "versions" + ] + }, + "patch": { + "description": "This endpoint allows updating the `yanked` state of a version, including a yank message.", + "operationId": "update_version", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Update a crate version.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}/authors": { + "get": { + "deprecated": true, + "description": "This endpoint was deprecated by [RFC #3052](https://github.com/rust-lang/rfcs/pull/3052)\nand returns an empty list for backwards compatibility reasons.", + "operationId": "get_version_authors", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get crate version authors.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}/dependencies": { + "get": { + "description": "This information can also be obtained directly from the index.\n\nIn addition to returning cached data from the index, this returns\nfields for `id`, `version_id`, and `downloads` (which appears to always\nbe 0)", + "operationId": "get_version_dependencies", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get crate version dependencies.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}/download": { + "get": { + "description": "This returns a URL to the location where the crate is stored.", + "operationId": "download_version", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Download a crate version.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}/downloads": { + "get": { + "description": "This includes the per-day downloads for the last 90 days.", + "operationId": "get_version_downloads", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get the download counts for a crate version.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}/readme": { + "get": { + "operationId": "get_version_readme", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get the readme of a crate version.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}/unyank": { + "put": { + "operationId": "unyank_version", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Unyank a crate version.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/crates/{name}/{version}/yank": { + "delete": { + "description": "This does not delete a crate version, it makes the crate\nversion accessible only to crates that already have a\n`Cargo.lock` containing this version.\n\nNotes:\n\nVersion deletion is not implemented to avoid breaking builds,\nand the goal of yanking a crate is to prevent crates\nbeginning to depend on the yanked crate version.", + "operationId": "yank_version", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Yank a crate version.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/keywords": { + "get": { + "operationId": "list_keywords", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List all keywords.", + "tags": [ + "keywords" + ] + } + }, + "/api/v1/keywords/{keyword}": { + "get": { + "operationId": "get_keyword", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get keyword metadata.", + "tags": [ + "keywords" + ] + } + }, + "/api/v1/me": { + "get": { + "operationId": "get_authenticated_user", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get the currently authenticated user.", + "tags": [ + "users" + ] + } + }, + "/api/v1/me/crate_owner_invitations": { + "get": { + "operationId": "list_crate_owner_invitations_for_user", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List all crate owner invitations for the authenticated user.", + "tags": [ + "owners" + ] + } + }, + "/api/v1/me/crate_owner_invitations/accept/{token}": { + "put": { + "operationId": "accept_crate_owner_invitation_with_token", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Accept a crate owner invitation with a token.", + "tags": [ + "owners" + ] + } + }, + "/api/v1/me/crate_owner_invitations/{crate_id}": { + "put": { + "operationId": "handle_crate_owner_invitation", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Accept or decline a crate owner invitation.", + "tags": [ + "owners" + ] + } + }, + "/api/v1/me/email_notifications": { + "put": { + "deprecated": true, + "description": "This endpoint was implemented for an experimental feature that was never\nfully implemented. It is now deprecated and will be removed in the future.", + "operationId": "update_email_notifications", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Update email notification settings for the authenticated user.", + "tags": [ + "users" + ] + } + }, + "/api/v1/me/tokens": { + "get": { + "operationId": "list_api_tokens", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List all API tokens of the authenticated user.", + "tags": [ + "api_tokens" + ] + }, + "put": { + "operationId": "create_api_token", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Create a new API token.", + "tags": [ + "api_tokens" + ] + } + }, + "/api/v1/me/tokens/{id}": { + "delete": { + "operationId": "revoke_api_token", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Revoke API token.", + "tags": [ + "api_tokens" + ] + }, + "get": { + "operationId": "get_api_token", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Find API token by id.", + "tags": [ + "api_tokens" + ] + } + }, + "/api/v1/me/updates": { + "get": { + "operationId": "get_authenticated_user_updates", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "List versions of crates that the authenticated user follows.", + "tags": [ + "versions" + ] + } + }, + "/api/v1/site_metadata": { + "get": { + "description": "Returns the current deployed commit SHA1 (or `unknown`), and whether the\nsystem is in read-only mode.", + "operationId": "get_site_metadata", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get crates.io metadata.", + "tags": [ + "other" + ] + } + }, + "/api/v1/summary": { + "get": { + "description": "This endpoint returns a summary of the most important data for the front\npage of crates.io.", + "operationId": "get_summary", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get front page data.", + "tags": [ + "other" + ] + } + }, + "/api/v1/teams/{team}": { + "get": { + "operationId": "get_team", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Find team by login.", + "tags": [ + "teams" + ] + } + }, + "/api/v1/tokens/current": { + "delete": { + "description": "This endpoint revokes the API token that is used to authenticate\nthe request.", + "operationId": "revoke_current_api_token", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Revoke the current API token.", + "tags": [ + "api_tokens" + ] + } + }, + "/api/v1/users/{id}/resend": { + "put": { + "operationId": "resend_email_verification", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Regenerate and send an email verification token.", + "tags": [ + "users" + ] + } + }, + "/api/v1/users/{id}/stats": { + "get": { + "description": "This currently only returns the total number of downloads for crates owned\nby the user.", + "operationId": "get_user_stats", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Get user stats.", + "tags": [ + "users" + ] + } + }, + "/api/v1/users/{user}": { + "get": { + "operationId": "get_user", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Find user by login.", + "tags": [ + "users" + ] + }, + "put": { + "description": "This endpoint allows users to update their email address and publish notifications settings.\n\nThe `id` parameter needs to match the ID of the currently authenticated user.", + "operationId": "update_user", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Update user settings.", + "tags": [ + "users" + ] + } } }, "servers": [ diff --git a/src/tests/blocked_routes.rs b/src/tests/blocked_routes.rs index 7c1e31efddb..bca35c93607 100644 --- a/src/tests/blocked_routes.rs +++ b/src/tests/blocked_routes.rs @@ -32,7 +32,7 @@ async fn test_blocked_download_route() { config.blocked_routes.clear(); config .blocked_routes - .insert("/api/v1/crates/:crate_id/:version/download".into()); + .insert("/api/v1/crates/:name/:version/download".into()); }) .with_user() .await;