Add utoipa info for v2 routes (#5775)

* wip: add v2 docs, routes to config, paths

* fix up path prefixes

* fix leading slashes

* fix slash route

* fix more slashes

* wip: full utopification of v2

* convert last few v2 routes to utoipa
This commit is contained in:
aecsocket
2026-04-15 13:25:35 +00:00
committed by GitHub
parent baee34b0b6
commit f12bd7b4b8
28 changed files with 1979 additions and 211 deletions
+19 -3
View File
@@ -17,15 +17,15 @@ use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::sync::Arc;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
web::scope("admin")
utoipa_actix_web::scope("/admin")
.service(count_download)
.service(force_reindex),
);
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct DownloadBody {
pub url: String,
pub project_id: ProjectId,
@@ -36,6 +36,14 @@ pub struct DownloadBody {
}
// This is an internal route, cannot be used without key
#[utoipa::path(
patch,
operation_id = "countDownload",
responses(
(status = 204, description = "Download counted successfully"),
(status = 400, description = "Invalid input")
)
)]
#[patch("/_count-download", guard = "admin_key_guard")]
#[allow(clippy::too_many_arguments)]
pub async fn count_download(
@@ -150,6 +158,14 @@ pub async fn count_download(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path(
post,
operation_id = "forceReindex",
responses(
(status = 204, description = "Search index rebuilt successfully"),
(status = 401, description = "Unauthorized")
)
)]
#[post("/_force_reindex", guard = "admin_key_guard")]
pub async fn force_reindex(
pool: web::Data<PgPool>,
+1 -1
View File
@@ -44,7 +44,7 @@ use tracing::warn;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("billing")
web::scope("/billing")
.service(products)
.service(subscriptions)
.service(user_customer)
@@ -37,7 +37,7 @@ pub mod rescan;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("delphi")
web::scope("/delphi")
.service(ingest_report)
.service(_run)
.service(version)
+170 -30
View File
@@ -22,7 +22,7 @@ use crate::util::error::Context;
use crate::util::ext::get_image_ext;
use crate::util::img::upload_image_optimized;
use crate::util::validate::validation_errors_to_string;
use actix_web::web::{Data, Query, ServiceConfig, scope};
use actix_web::web::{Data, Query};
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
@@ -43,9 +43,9 @@ use tracing::info;
use validator::Validate;
use zxcvbn::Score;
pub fn config(cfg: &mut ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
scope("auth")
utoipa_actix_web::scope("/auth")
.service(init)
.service(auth_callback)
.service(delete_auth_provider)
@@ -1041,7 +1041,7 @@ impl AuthProvider {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct AuthorizationInit {
pub url: String,
#[serde(default)]
@@ -1051,7 +1051,7 @@ pub struct AuthorizationInit {
/// this will be set to the user's auth token from the frontend.
pub auth_token: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Authorization {
pub code: String,
pub state: String,
@@ -1059,7 +1059,15 @@ pub struct Authorization {
// Init link takes us to GitHub API and calls back to callback endpoint with a code and state
// http://localhost:8000/auth/init?url=https://modrinth.com
#[get("init")]
#[utoipa::path(
get,
operation_id = "authInit",
responses(
(status = 307, description = "Redirect to OAuth provider"),
(status = 400, description = "Invalid input")
)
)]
#[get("/init")]
pub async fn init(
req: HttpRequest,
Query(info): Query<AuthorizationInit>, // callback url
@@ -1140,7 +1148,15 @@ pub async fn init(
.json(serde_json::json!({ "url": url })))
}
#[get("callback")]
#[utoipa::path(
get,
operation_id = "authCallback",
responses(
(status = 307, description = "Redirect with auth code"),
(status = 401, description = "Authentication failed")
)
)]
#[get("/callback")]
pub async fn auth_callback(
req: HttpRequest,
Query(query): Query<HashMap<String, String>>,
@@ -1336,12 +1352,22 @@ pub async fn auth_callback(
Ok(res?)
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct DeleteAuthProvider {
pub provider: AuthProvider,
}
#[delete("provider")]
#[utoipa::path(
delete,
operation_id = "deleteAuthProvider",
responses(
(status = 204, description = "Auth provider removed"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["USER_AUTH_WRITE"]))
)]
#[delete("/provider")]
pub async fn delete_auth_provider(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1425,7 +1451,7 @@ pub async fn check_sendy_subscription(
Ok(response.trim() == "Subscribed")
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct NewAccount {
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))]
pub username: String,
@@ -1437,7 +1463,15 @@ pub struct NewAccount {
pub sign_up_newsletter: Option<bool>,
}
#[post("create")]
#[utoipa::path(
post,
operation_id = "createAccountPassword",
responses(
(status = 200, description = "Account created"),
(status = 400, description = "Invalid input")
)
)]
#[post("/create")]
pub async fn create_account_with_password(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1566,7 +1600,7 @@ pub async fn create_account_with_password(
Ok(HttpResponse::Ok().json(res))
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct Login {
#[serde(rename = "username")]
pub username_or_email: String,
@@ -1574,7 +1608,15 @@ pub struct Login {
pub challenge: String,
}
#[post("login")]
#[utoipa::path(
post,
operation_id = "loginPassword",
responses(
(status = 200, description = "Login successful"),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login")]
pub async fn login_password(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1639,7 +1681,7 @@ pub async fn login_password(
}
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct Login2FA {
pub code: String,
pub flow: String,
@@ -1724,7 +1766,15 @@ async fn validate_2fa_code(
}
}
#[post("login/2fa")]
#[utoipa::path(
post,
operation_id = "login2fa",
responses(
(status = 200, description = "2FA login successful"),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login/2fa")]
pub async fn login_2fa(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1773,7 +1823,16 @@ pub async fn login_2fa(
}
}
#[post("2fa/get_secret")]
#[utoipa::path(
post,
operation_id = "begin2faFlow",
responses(
(status = 200, description = "2FA secret generated"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa/get_secret")]
pub async fn begin_2fa_flow(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1812,7 +1871,16 @@ pub async fn begin_2fa_flow(
}
}
#[post("2fa")]
#[utoipa::path(
post,
operation_id = "finish2faFlow",
responses(
(status = 200, description = "2FA enabled"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa")]
pub async fn finish_2fa_flow(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1930,12 +1998,21 @@ pub async fn finish_2fa_flow(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct Remove2FA {
pub code: String,
}
#[delete("2fa")]
#[utoipa::path(
delete,
operation_id = "remove2fa",
responses(
(status = 204, description = "2FA removed"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[delete("/2fa")]
pub async fn remove_2fa(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2016,14 +2093,22 @@ pub async fn remove_2fa(
Ok(HttpResponse::NoContent().finish())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ResetPassword {
#[serde(rename = "username")]
pub username_or_email: String,
pub challenge: String,
}
#[post("password/reset")]
#[utoipa::path(
post,
operation_id = "resetPasswordBegin",
responses(
(status = 204, description = "Password reset email sent"),
(status = 400, description = "Invalid input")
)
)]
#[post("/password/reset")]
pub async fn reset_password_begin(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2111,14 +2196,24 @@ pub async fn reset_password_begin(
Ok(HttpResponse::Ok().finish())
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct ChangePassword {
pub flow: Option<String>,
pub old_password: Option<String>,
pub new_password: Option<String>,
}
#[patch("password")]
#[utoipa::path(
patch,
operation_id = "changePassword",
responses(
(status = 204, description = "Password changed"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[patch("/password")]
pub async fn change_password(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2265,13 +2360,23 @@ pub async fn change_password(
Ok(HttpResponse::Ok().finish())
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct SetEmail {
#[validate(email)]
pub email: String,
}
#[patch("email")]
#[utoipa::path(
patch,
operation_id = "setEmail",
responses(
(status = 204, description = "Email set"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[patch("/email")]
pub async fn set_email(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2380,7 +2485,16 @@ pub async fn set_email(
Ok(HttpResponse::Ok().finish())
}
#[post("email/resend_verify")]
#[utoipa::path(
post,
operation_id = "resendVerifyEmail",
responses(
(status = 204, description = "Verification email resent"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/email/resend_verify")]
pub async fn resend_verify_email(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2438,12 +2552,20 @@ pub async fn resend_verify_email(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct VerifyEmail {
pub flow: String,
}
#[post("email/verify")]
#[utoipa::path(
post,
operation_id = "verifyEmail",
responses(
(status = 204, description = "Email verified"),
(status = 400, description = "Invalid input")
)
)]
#[post("/email/verify")]
pub async fn verify_email(
pool: Data<PgPool>,
redis: Data<RedisPool>,
@@ -2498,7 +2620,16 @@ pub async fn verify_email(
}
}
#[post("email/subscribe")]
#[utoipa::path(
post,
operation_id = "subscribeNewsletter",
responses(
(status = 204, description = "Newsletter subscription toggled"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/email/subscribe")]
pub async fn subscribe_newsletter(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2535,7 +2666,16 @@ pub async fn subscribe_newsletter(
Ok(HttpResponse::NoContent().finish())
}
#[get("email/subscribe")]
#[utoipa::path(
get,
operation_id = "getNewsletterSubscriptionStatus",
responses(
(status = 200, description = "Subscription status"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[get("/email/subscribe")]
pub async fn get_newsletter_subscription_status(
req: HttpRequest,
pool: Data<PgPool>,
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::routes::ApiError;
use actix_web::{HttpRequest, HttpResponse, post, web};
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("gdpr").service(export));
cfg.service(web::scope("/gdpr").service(export));
}
#[post("/export")]
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::routes::ApiError;
use crate::util::guards::medal_key_guard;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("medal").service(verify).service(redeem));
cfg.service(web::scope("/medal").service(verify).service(redeem));
}
#[derive(Deserialize)]
+37 -5
View File
@@ -22,13 +22,45 @@ use crate::util::cors::default_cors;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
actix_web::web::scope("_internal")
actix_web::web::scope("/_internal")
.wrap(default_cors())
.configure(admin::config)
.configure(|cfg| {
cfg.service(
actix_web::web::scope("/admin")
.service(admin::count_download)
.service(admin::force_reindex),
);
cfg.service(
actix_web::web::scope("/session")
.service(session::list)
.service(session::delete)
.service(session::refresh),
);
cfg.service(
actix_web::web::scope("/auth")
.service(flows::init)
.service(flows::auth_callback)
.service(flows::delete_auth_provider)
.service(flows::create_account_with_password)
.service(flows::login_password)
.service(flows::login_2fa)
.service(flows::begin_2fa_flow)
.service(flows::finish_2fa_flow)
.service(flows::remove_2fa)
.service(flows::reset_password_begin)
.service(flows::change_password)
.service(flows::resend_verify_email)
.service(flows::set_email)
.service(flows::verify_email)
.service(flows::subscribe_newsletter)
.service(flows::get_newsletter_subscription_status),
);
cfg.service(pats::get_pats);
cfg.service(pats::create_pat);
cfg.service(pats::edit_pat);
cfg.service(pats::delete_pat);
})
.configure(oauth_clients::config)
.configure(session::config)
.configure(flows::config)
.configure(pats::config)
.configure(billing::config)
.configure(gdpr::config)
.configure(gotenberg::config)
+47 -7
View File
@@ -22,14 +22,23 @@ use crate::util::validate::validation_errors_to_string;
use serde::Deserialize;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(get_pats);
cfg.service(create_pat);
cfg.service(edit_pat);
cfg.service(delete_pat);
}
#[get("pat")]
#[utoipa::path(
get,
operation_id = "getPats",
responses(
(status = 200, description = "List of PATs"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_READ"]))
)]
#[get("/pat")]
pub async fn get_pats(
req: HttpRequest,
pool: Data<PgPool>,
@@ -65,7 +74,7 @@ pub async fn get_pats(
))
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct NewPersonalAccessToken {
pub scopes: Scopes,
#[validate(length(min = 3, max = 255))]
@@ -73,7 +82,17 @@ pub struct NewPersonalAccessToken {
pub expires: DateTime<Utc>,
}
#[post("pat")]
#[utoipa::path(
post,
operation_id = "createPat",
responses(
(status = 200, description = "PAT created"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_CREATE"]))
)]
#[post("/pat")]
pub async fn create_pat(
req: HttpRequest,
info: web::Json<NewPersonalAccessToken>,
@@ -158,7 +177,7 @@ pub async fn create_pat(
}))
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct ModifyPersonalAccessToken {
pub scopes: Option<Scopes>,
#[validate(length(min = 3, max = 255))]
@@ -166,7 +185,18 @@ pub struct ModifyPersonalAccessToken {
pub expires: Option<DateTime<Utc>>,
}
#[patch("pat/{id}")]
#[utoipa::path(
patch,
operation_id = "editPat",
params(("id" = String, Path, description = "The PAT ID")),
responses(
(status = 204, description = "PAT updated"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_WRITE"]))
)]
#[patch("/pat/{id}")]
pub async fn edit_pat(
req: HttpRequest,
id: web::Path<(String,)>,
@@ -263,7 +293,17 @@ pub async fn edit_pat(
Ok(HttpResponse::NoContent().finish())
}
#[delete("pat/{id}")]
#[utoipa::path(
delete,
operation_id = "deletePat",
params(("id" = String, Path, description = "The PAT ID")),
responses(
(status = 204, description = "PAT deleted"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_DELETE"]))
)]
#[delete("/pat/{id}")]
pub async fn delete_pat(
req: HttpRequest,
id: web::Path<(String,)>,
+33 -6
View File
@@ -11,7 +11,7 @@ use crate::models::sessions::Session;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use actix_web::http::header::AUTHORIZATION;
use actix_web::web::{Data, ServiceConfig, scope};
use actix_web::web::Data;
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use chrono::{DateTime, Utc};
use rand::distributions::Alphanumeric;
@@ -19,9 +19,9 @@ use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use woothee::parser::Parser;
pub fn config(cfg: &mut ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
scope("session")
utoipa_actix_web::scope("/session")
.service(list)
.service(delete)
.service(refresh),
@@ -133,7 +133,16 @@ pub async fn issue_session(
Ok(session)
}
#[get("list")]
#[utoipa::path(
get,
operation_id = "listSessions",
responses(
(status = 200, description = "List of active sessions"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["SESSION_READ"]))
)]
#[get("/list")]
pub async fn list(
req: HttpRequest,
pool: Data<PgPool>,
@@ -169,7 +178,17 @@ pub async fn list(
Ok(HttpResponse::Ok().json(sessions))
}
#[delete("{id}")]
#[utoipa::path(
delete,
operation_id = "deleteSession",
params(("id" = String, Path, description = "The session ID")),
responses(
(status = 204, description = "Session deleted"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["SESSION_DELETE"]))
)]
#[delete("/{id}")]
pub async fn delete(
info: web::Path<(String,)>,
req: HttpRequest,
@@ -209,7 +228,15 @@ pub async fn delete(
Ok(HttpResponse::NoContent().body(""))
}
#[post("refresh")]
#[utoipa::path(
post,
operation_id = "refreshSession",
responses(
(status = 200, description = "Session refreshed"),
(status = 401, description = "Unauthorized")
)
)]
#[post("/refresh")]
pub async fn refresh(
req: HttpRequest,
pool: Data<PgPool>,