diff --git a/src/controllers/category.rs b/src/controllers/category.rs index 6384fcf880..b7ea29d18b 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 b9b7383faf..4bfd68d52b 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 6843b72461..c7c4efbfab 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 93ee59d479..ca1cd37cdd 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 55bd9db4b3..b08e464206 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 3ff2dbba87..3d731aa058 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 d000a10c53..c0e2dc2985 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 8be893ba88..f0befa72aa 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 05a3451a5a..6fdf2584f3 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 fe938360d2..82a30e6ff0 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 4376101612..5dd370a241 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 910055458e..7d4515f2d5 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 452cb7d5c8..c803d89242 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 b1263ae463..ccfc482e3b 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 8f6a4d2c30..14f0a06626 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 4226f0f3a0..250a658be1 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 210db02e4c..a4c0d3bf00 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 8ba6459abb..05602a415b 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 d73f47697e..3a743848a9 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 e555573a1e..84566e701a 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 9c14a308e2..06e934ee7c 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 28390e2b90..92841325a5 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 61130b80bf..01f15fa788 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 c9e18f6507..06b8cac5cb 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 1da715e710..24758d9240 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 bb539c3965..325db33001 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 7c1e31efdd..bca35c9360 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;