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 14:25:35 +01:00
committed by GitHub
parent baee34b0b6
commit f12bd7b4b8
28 changed files with 1979 additions and 211 deletions

View File

@@ -353,7 +353,6 @@ pub fn app_config(
.app_data(web::Data::new(labrinth_config.stripe_client.clone())) .app_data(web::Data::new(labrinth_config.stripe_client.clone()))
.app_data(web::Data::new(labrinth_config.anrok_client.clone())) .app_data(web::Data::new(labrinth_config.anrok_client.clone()))
.app_data(labrinth_config.rate_limiter.clone()) .app_data(labrinth_config.rate_limiter.clone())
.configure(routes::v2::config)
.configure(routes::v3::config) .configure(routes::v3::config)
.configure(routes::internal::config) .configure(routes::internal::config)
.configure(routes::root_config) .configure(routes::root_config)
@@ -374,6 +373,7 @@ pub fn utoipa_app_config(
|_cfg| () |_cfg| ()
} }
}) })
.configure(routes::v2::utoipa_config)
.configure(routes::v3::utoipa_config) .configure(routes::v3::utoipa_config)
.configure(routes::internal::utoipa_config); .configure(routes::internal::utoipa_config);
} }

View File

@@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
/// A project returned from the API /// A project returned from the API
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct LegacyProject { pub struct LegacyProject {
/// Relevant V2 fields- these were removed or modified in V3, /// Relevant V2 fields- these were removed or modified in V3,
/// and are now part of the dynamic fields system /// and are now part of the dynamic fields system
@@ -253,7 +253,9 @@ impl LegacyProject {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Copy)] #[derive(
Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Copy, utoipa::ToSchema,
)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum LegacySideType { pub enum LegacySideType {
Required, Required,
@@ -290,7 +292,7 @@ impl LegacySideType {
} }
/// A specific version of a project /// A specific version of a project
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct LegacyVersion { pub struct LegacyVersion {
/// Relevant V2 fields- these were removed or modified in V3, /// Relevant V2 fields- these were removed or modified in V3,
/// and are now part of the dynamic fields system /// and are now part of the dynamic fields system
@@ -368,7 +370,7 @@ impl From<Version> for LegacyVersion {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
pub struct LegacyGalleryItem { pub struct LegacyGalleryItem {
pub url: String, pub url: String,
pub raw_url: String, pub raw_url: String,
@@ -393,7 +395,9 @@ impl LegacyGalleryItem {
} }
} }
#[derive(Serialize, Deserialize, Validate, Clone, Eq, PartialEq)] #[derive(
Serialize, Deserialize, Validate, Clone, Eq, PartialEq, utoipa::ToSchema,
)]
pub struct DonationLink { pub struct DonationLink {
pub id: String, pub id: String,
pub platform: String, pub platform: String,

View File

@@ -124,6 +124,19 @@ bitflags::bitflags! {
bitflags_serde_impl!(Scopes, u64); bitflags_serde_impl!(Scopes, u64);
impl utoipa::PartialSchema for Scopes {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::Type::Integer)
.format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int64,
)))
.into()
}
}
impl utoipa::ToSchema for Scopes {}
impl Scopes { impl Scopes {
// these scopes cannot be specified in a personal access token // these scopes cannot be specified in a personal access token
pub fn restricted() -> Scopes { pub fn restricted() -> Scopes {

View File

@@ -33,6 +33,23 @@ bitflags::bitflags! {
bitflags_serde_impl!(ProjectPermissions, u64); bitflags_serde_impl!(ProjectPermissions, u64);
impl utoipa::PartialSchema for ProjectPermissions {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
u64::schema()
}
}
impl utoipa::ToSchema for ProjectPermissions {
fn schemas(
schemas: &mut Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>,
) {
u64::schemas(schemas);
}
}
impl Default for ProjectPermissions { impl Default for ProjectPermissions {
fn default() -> ProjectPermissions { fn default() -> ProjectPermissions {
ProjectPermissions::empty() ProjectPermissions::empty()
@@ -92,6 +109,23 @@ bitflags::bitflags! {
bitflags_serde_impl!(OrganizationPermissions, u64); bitflags_serde_impl!(OrganizationPermissions, u64);
impl utoipa::PartialSchema for OrganizationPermissions {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
u64::schema()
}
}
impl utoipa::ToSchema for OrganizationPermissions {
fn schemas(
schemas: &mut Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>,
) {
u64::schemas(schemas);
}
}
impl Default for OrganizationPermissions { impl Default for OrganizationPermissions {
fn default() -> OrganizationPermissions { fn default() -> OrganizationPermissions {
OrganizationPermissions::NONE OrganizationPermissions::NONE

View File

@@ -17,15 +17,15 @@ use std::collections::HashMap;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::sync::Arc; use std::sync::Arc;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service( cfg.service(
web::scope("admin") utoipa_actix_web::scope("/admin")
.service(count_download) .service(count_download)
.service(force_reindex), .service(force_reindex),
); );
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct DownloadBody { pub struct DownloadBody {
pub url: String, pub url: String,
pub project_id: ProjectId, pub project_id: ProjectId,
@@ -36,6 +36,14 @@ pub struct DownloadBody {
} }
// This is an internal route, cannot be used without key // 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")] #[patch("/_count-download", guard = "admin_key_guard")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn count_download( pub async fn count_download(
@@ -150,6 +158,14 @@ pub async fn count_download(
Ok(HttpResponse::NoContent().body("")) 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")] #[post("/_force_reindex", guard = "admin_key_guard")]
pub async fn force_reindex( pub async fn force_reindex(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,

View File

@@ -44,7 +44,7 @@ use tracing::warn;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::scope("billing") web::scope("/billing")
.service(products) .service(products)
.service(subscriptions) .service(subscriptions)
.service(user_customer) .service(user_customer)

View File

@@ -37,7 +37,7 @@ pub mod rescan;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::scope("delphi") web::scope("/delphi")
.service(ingest_report) .service(ingest_report)
.service(_run) .service(_run)
.service(version) .service(version)

View File

@@ -22,7 +22,7 @@ use crate::util::error::Context;
use crate::util::ext::get_image_ext; use crate::util::ext::get_image_ext;
use crate::util::img::upload_image_optimized; use crate::util::img::upload_image_optimized;
use crate::util::validate::validation_errors_to_string; 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 actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use argon2::password_hash::SaltString; use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
@@ -43,9 +43,9 @@ use tracing::info;
use validator::Validate; use validator::Validate;
use zxcvbn::Score; use zxcvbn::Score;
pub fn config(cfg: &mut ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service( cfg.service(
scope("auth") utoipa_actix_web::scope("/auth")
.service(init) .service(init)
.service(auth_callback) .service(auth_callback)
.service(delete_auth_provider) .service(delete_auth_provider)
@@ -1041,7 +1041,7 @@ impl AuthProvider {
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct AuthorizationInit { pub struct AuthorizationInit {
pub url: String, pub url: String,
#[serde(default)] #[serde(default)]
@@ -1051,7 +1051,7 @@ pub struct AuthorizationInit {
/// this will be set to the user's auth token from the frontend. /// this will be set to the user's auth token from the frontend.
pub auth_token: Option<String>, pub auth_token: Option<String>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Authorization { pub struct Authorization {
pub code: String, pub code: String,
pub state: 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 // 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 // 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( pub async fn init(
req: HttpRequest, req: HttpRequest,
Query(info): Query<AuthorizationInit>, // callback url Query(info): Query<AuthorizationInit>, // callback url
@@ -1140,7 +1148,15 @@ pub async fn init(
.json(serde_json::json!({ "url": url }))) .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( pub async fn auth_callback(
req: HttpRequest, req: HttpRequest,
Query(query): Query<HashMap<String, String>>, Query(query): Query<HashMap<String, String>>,
@@ -1336,12 +1352,22 @@ pub async fn auth_callback(
Ok(res?) Ok(res?)
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct DeleteAuthProvider { pub struct DeleteAuthProvider {
pub provider: AuthProvider, 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( pub async fn delete_auth_provider(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -1425,7 +1451,7 @@ pub async fn check_sendy_subscription(
Ok(response.trim() == "Subscribed") Ok(response.trim() == "Subscribed")
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct NewAccount { pub struct NewAccount {
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))] #[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))]
pub username: String, pub username: String,
@@ -1437,7 +1463,15 @@ pub struct NewAccount {
pub sign_up_newsletter: Option<bool>, 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( pub async fn create_account_with_password(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -1566,7 +1600,7 @@ pub async fn create_account_with_password(
Ok(HttpResponse::Ok().json(res)) Ok(HttpResponse::Ok().json(res))
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct Login { pub struct Login {
#[serde(rename = "username")] #[serde(rename = "username")]
pub username_or_email: String, pub username_or_email: String,
@@ -1574,7 +1608,15 @@ pub struct Login {
pub challenge: String, 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( pub async fn login_password(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -1639,7 +1681,7 @@ pub async fn login_password(
} }
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct Login2FA { pub struct Login2FA {
pub code: String, pub code: String,
pub flow: 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( pub async fn login_2fa(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, 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( pub async fn begin_2fa_flow(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, 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( pub async fn finish_2fa_flow(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -1930,12 +1998,21 @@ pub async fn finish_2fa_flow(
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct Remove2FA { pub struct Remove2FA {
pub code: String, 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( pub async fn remove_2fa(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -2016,14 +2093,22 @@ pub async fn remove_2fa(
Ok(HttpResponse::NoContent().finish()) Ok(HttpResponse::NoContent().finish())
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct ResetPassword { pub struct ResetPassword {
#[serde(rename = "username")] #[serde(rename = "username")]
pub username_or_email: String, pub username_or_email: String,
pub challenge: 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( pub async fn reset_password_begin(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -2111,14 +2196,24 @@ pub async fn reset_password_begin(
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct ChangePassword { pub struct ChangePassword {
pub flow: Option<String>, pub flow: Option<String>,
pub old_password: Option<String>, pub old_password: Option<String>,
pub new_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( pub async fn change_password(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -2265,13 +2360,23 @@ pub async fn change_password(
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct SetEmail { pub struct SetEmail {
#[validate(email)] #[validate(email)]
pub email: String, 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( pub async fn set_email(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -2380,7 +2485,16 @@ pub async fn set_email(
Ok(HttpResponse::Ok().finish()) 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( pub async fn resend_verify_email(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -2438,12 +2552,20 @@ pub async fn resend_verify_email(
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct VerifyEmail { pub struct VerifyEmail {
pub flow: String, 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( pub async fn verify_email(
pool: Data<PgPool>, pool: Data<PgPool>,
redis: Data<RedisPool>, 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( pub async fn subscribe_newsletter(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -2535,7 +2666,16 @@ pub async fn subscribe_newsletter(
Ok(HttpResponse::NoContent().finish()) 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( pub async fn get_newsletter_subscription_status(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,

View File

@@ -7,7 +7,7 @@ use crate::routes::ApiError;
use actix_web::{HttpRequest, HttpResponse, post, web}; use actix_web::{HttpRequest, HttpResponse, post, web};
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("gdpr").service(export)); cfg.service(web::scope("/gdpr").service(export));
} }
#[post("/export")] #[post("/export")]

View File

@@ -14,7 +14,7 @@ use crate::routes::ApiError;
use crate::util::guards::medal_key_guard; use crate::util::guards::medal_key_guard;
pub fn config(cfg: &mut web::ServiceConfig) { 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)] #[derive(Deserialize)]

View File

@@ -22,13 +22,45 @@ use crate::util::cors::default_cors;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) { pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service( cfg.service(
actix_web::web::scope("_internal") actix_web::web::scope("/_internal")
.wrap(default_cors()) .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(oauth_clients::config)
.configure(session::config)
.configure(flows::config)
.configure(pats::config)
.configure(billing::config) .configure(billing::config)
.configure(gdpr::config) .configure(gdpr::config)
.configure(gotenberg::config) .configure(gotenberg::config)

View File

@@ -22,14 +22,23 @@ use crate::util::validate::validation_errors_to_string;
use serde::Deserialize; use serde::Deserialize;
use validator::Validate; 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(get_pats);
cfg.service(create_pat); cfg.service(create_pat);
cfg.service(edit_pat); cfg.service(edit_pat);
cfg.service(delete_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( pub async fn get_pats(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -65,7 +74,7 @@ pub async fn get_pats(
)) ))
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct NewPersonalAccessToken { pub struct NewPersonalAccessToken {
pub scopes: Scopes, pub scopes: Scopes,
#[validate(length(min = 3, max = 255))] #[validate(length(min = 3, max = 255))]
@@ -73,7 +82,17 @@ pub struct NewPersonalAccessToken {
pub expires: DateTime<Utc>, 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( pub async fn create_pat(
req: HttpRequest, req: HttpRequest,
info: web::Json<NewPersonalAccessToken>, 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 struct ModifyPersonalAccessToken {
pub scopes: Option<Scopes>, pub scopes: Option<Scopes>,
#[validate(length(min = 3, max = 255))] #[validate(length(min = 3, max = 255))]
@@ -166,7 +185,18 @@ pub struct ModifyPersonalAccessToken {
pub expires: Option<DateTime<Utc>>, 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( pub async fn edit_pat(
req: HttpRequest, req: HttpRequest,
id: web::Path<(String,)>, id: web::Path<(String,)>,
@@ -263,7 +293,17 @@ pub async fn edit_pat(
Ok(HttpResponse::NoContent().finish()) 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( pub async fn delete_pat(
req: HttpRequest, req: HttpRequest,
id: web::Path<(String,)>, id: web::Path<(String,)>,

View File

@@ -11,7 +11,7 @@ use crate::models::sessions::Session;
use crate::queue::session::AuthQueue; use crate::queue::session::AuthQueue;
use crate::routes::ApiError; use crate::routes::ApiError;
use actix_web::http::header::AUTHORIZATION; 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 actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use rand::distributions::Alphanumeric; use rand::distributions::Alphanumeric;
@@ -19,9 +19,9 @@ use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng; use rand_chacha::ChaCha20Rng;
use woothee::parser::Parser; use woothee::parser::Parser;
pub fn config(cfg: &mut ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service( cfg.service(
scope("session") utoipa_actix_web::scope("/session")
.service(list) .service(list)
.service(delete) .service(delete)
.service(refresh), .service(refresh),
@@ -133,7 +133,16 @@ pub async fn issue_session(
Ok(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( pub async fn list(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,
@@ -169,7 +178,17 @@ pub async fn list(
Ok(HttpResponse::Ok().json(sessions)) 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( pub async fn delete(
info: web::Path<(String,)>, info: web::Path<(String,)>,
req: HttpRequest, req: HttpRequest,
@@ -209,7 +228,15 @@ pub async fn delete(
Ok(HttpResponse::NoContent().body("")) 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( pub async fn refresh(
req: HttpRequest, req: HttpRequest,
pool: Data<PgPool>, pool: Data<PgPool>,

View File

@@ -25,17 +25,17 @@ pub use self::not_found::not_found;
pub fn root_config(cfg: &mut web::ServiceConfig) { pub fn root_config(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::scope("maven") web::scope("/maven")
.wrap(default_cors()) .wrap(default_cors())
.configure(maven::config), .configure(maven::config),
); );
cfg.service( cfg.service(
web::scope("updates") web::scope("/updates")
.wrap(default_cors()) .wrap(default_cors())
.configure(updates::config), .configure(updates::config),
); );
cfg.service( cfg.service(
web::scope("analytics") web::scope("/analytics")
.wrap( .wrap(
Cors::default() Cors::default()
.allowed_origin_fn(|origin, _req_head| { .allowed_origin_fn(|origin, _req_head| {
@@ -59,7 +59,7 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
.configure(analytics::config), .configure(analytics::config),
); );
cfg.service( cfg.service(
web::scope("api/v1") web::scope("/api/v1")
.wrap(default_cors()) .wrap(default_cors())
.wrap_fn(|req, _srv| { .wrap_fn(|req, _srv| {
async { async {

View File

@@ -15,12 +15,13 @@ mod versions;
pub use super::ApiError; pub use super::ApiError;
use crate::util::cors::default_cors; use crate::util::cors::default_cors;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) { pub fn utoipa_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
) {
cfg.service( cfg.service(
actix_web::web::scope("v2") utoipa_actix_web::scope("/v2")
.wrap(default_cors()) .wrap(default_cors())
.configure(super::internal::admin::config) .configure(super::internal::admin::config)
// Todo: separate these- they need to also follow v2-v3 conversion
.configure(super::internal::session::config) .configure(super::internal::session::config)
.configure(super::internal::flows::config) .configure(super::internal::flows::config)
.configure(super::internal::pats::config) .configure(super::internal::pats::config)

View File

@@ -8,8 +8,8 @@ use crate::{database::redis::RedisPool, routes::v2_reroute};
use actix_web::{HttpRequest, HttpResponse, get, web}; use actix_web::{HttpRequest, HttpResponse, get, web};
use serde::Deserialize; use serde::Deserialize;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(web::scope("moderation").service(get_projects)); cfg.service(utoipa_actix_web::scope("/moderation").service(get_projects));
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -22,7 +22,31 @@ fn default_count() -> u16 {
100 100
} }
#[get("projects")] /// Get projects in the moderation queue.
#[utoipa::path(
get,
operation_id = "getModerationProjects",
params(
(
"count" = Option<u16>,
Query,
description = "Maximum number of projects to return"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_READ"]))
)]
#[get("/projects")]
pub async fn get_projects( pub async fn get_projects(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,

View File

@@ -10,13 +10,12 @@ use crate::routes::v3;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(notifications_get); cfg.service(notifications_get);
cfg.service(notifications_delete); cfg.service(notifications_delete);
cfg.service(notifications_read); cfg.service(notifications_read);
cfg.service( cfg.service(
web::scope("notification") utoipa_actix_web::scope("/notification")
.service(notification_get) .service(notification_get)
.service(notification_read) .service(notification_read)
.service(notification_delete), .service(notification_delete),
@@ -28,7 +27,31 @@ pub struct NotificationIds {
pub ids: String, pub ids: String,
} }
#[get("notifications")] /// Get multiple notifications by ID.
#[utoipa::path(
get,
operation_id = "getNotifications",
params(
(
"ids" = String,
Query,
description = "The JSON array of notification IDs"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["NOTIFICATION_READ"]))
)]
#[get("/notifications")]
pub async fn notifications_get( pub async fn notifications_get(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>, web::Query(ids): web::Query<NotificationIds>,
@@ -57,7 +80,25 @@ pub async fn notifications_get(
} }
} }
#[get("{id}")] /// Get a notification by ID.
#[utoipa::path(
get,
operation_id = "getNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["NOTIFICATION_READ"]))
)]
#[get("/{id}")]
pub async fn notification_get( pub async fn notification_get(
req: HttpRequest, req: HttpRequest,
info: web::Path<(NotificationId,)>, info: web::Path<(NotificationId,)>,
@@ -83,7 +124,25 @@ pub async fn notification_get(
} }
} }
#[patch("{id}")] /// Mark a notification as read.
#[utoipa::path(
patch,
operation_id = "readNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["NOTIFICATION_WRITE"]))
)]
#[patch("/{id}")]
pub async fn notification_read( pub async fn notification_read(
req: HttpRequest, req: HttpRequest,
info: web::Path<(NotificationId,)>, info: web::Path<(NotificationId,)>,
@@ -97,7 +156,25 @@ pub async fn notification_read(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}")] /// Delete a notification by ID.
#[utoipa::path(
delete,
operation_id = "deleteNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["NOTIFICATION_WRITE"]))
)]
#[delete("/{id}")]
pub async fn notification_delete( pub async fn notification_delete(
req: HttpRequest, req: HttpRequest,
info: web::Path<(NotificationId,)>, info: web::Path<(NotificationId,)>,
@@ -117,7 +194,31 @@ pub async fn notification_delete(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[patch("notifications")] /// Mark multiple notifications as read.
#[utoipa::path(
patch,
operation_id = "readNotifications",
params(
(
"ids" = String,
Query,
description = "The JSON array of notification IDs"
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["NOTIFICATION_WRITE"]))
)]
#[patch("/notifications")]
pub async fn notifications_read( pub async fn notifications_read(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>, web::Query(ids): web::Query<NotificationIds>,
@@ -137,7 +238,31 @@ pub async fn notifications_read(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("notifications")] /// Delete multiple notifications by ID.
#[utoipa::path(
delete,
operation_id = "deleteNotifications",
params(
(
"ids" = String,
Query,
description = "The JSON array of notification IDs"
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["NOTIFICATION_WRITE"]))
)]
#[delete("/notifications")]
pub async fn notifications_delete( pub async fn notifications_delete(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>, web::Query(ids): web::Query<NotificationIds>,

View File

@@ -25,7 +25,7 @@ use validator::Validate;
use super::version_creation::InitialVersionData; use super::version_creation::InitialVersionData;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(project_create); cfg.service(project_create);
} }
@@ -134,6 +134,24 @@ struct ProjectCreateData {
pub organization_id: Option<models::ids::OrganizationId>, pub organization_id: Option<models::ids::OrganizationId>,
} }
/// Create a new project with initial versions.
#[utoipa::path(
post,
operation_id = "createProject",
request_body(
content(("multipart/form-data")),
description = "Multipart payload containing `data` and uploaded files"
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_CREATE"]))
)]
#[post("/project")] #[post("/project")]
pub async fn project_create( pub async fn project_create(
req: HttpRequest, req: HttpRequest,

View File

@@ -21,14 +21,13 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use validator::Validate; use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(project_search); cfg.service(project_search);
cfg.service(projects_get); cfg.service(projects_get);
cfg.service(projects_edit); cfg.service(projects_edit);
cfg.service(random_projects_get); cfg.service(random_projects_get);
cfg.service( cfg.service(
web::scope("project") utoipa_actix_web::scope("/project")
.service(project_get) .service(project_get)
.service(project_get_check) .service(project_get_check)
.service(project_delete) .service(project_delete)
@@ -42,7 +41,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(project_unfollow) .service(project_unfollow)
.service(super::teams::team_members_get_project) .service(super::teams::team_members_get_project)
.service( .service(
web::scope("{project_id}") utoipa_actix_web::scope("/{project_id}")
.service(super::versions::version_list) .service(super::versions::version_list)
.service(super::versions::version_project_get) .service(super::versions::version_project_get)
.service(dependency_list), .service(dependency_list),
@@ -50,7 +49,43 @@ pub fn config(cfg: &mut web::ServiceConfig) {
); );
} }
#[get("search")] /// Search projects.
#[utoipa::path(
get,
operation_id = "searchProjects",
params(
(
"query" = Option<String>,
Query,
description = "The query to search for"
),
(
"facets" = Option<String>,
Query,
description = "Search facets JSON"
),
(
"index" = Option<String>,
Query,
description = "Search index to use"
),
(
"offset" = Option<String>,
Query,
description = "Search result offset"
),
(
"limit" = Option<String>,
Query,
description = "Maximum number of search results"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[get("/search")]
pub async fn project_search( pub async fn project_search(
web::Query(info): web::Query<SearchRequest>, web::Query(info): web::Query<SearchRequest>,
search_backend: web::Data<dyn SearchBackend>, search_backend: web::Data<dyn SearchBackend>,
@@ -141,13 +176,29 @@ fn parse_facet(facet: &str) -> Option<(String, String, String)> {
None None
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct RandomProjects { pub struct RandomProjects {
#[validate(range(min = 1, max = 100))] #[validate(range(min = 1, max = 100))]
pub count: u32, pub count: u32,
} }
#[get("projects_random")] /// Get random projects.
#[utoipa::path(
get,
operation_id = "randomProjects",
params(
(
"count" = u32,
Query,
description = "Number of projects to return"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[get("/projects_random")]
pub async fn random_projects_get( pub async fn random_projects_get(
web::Query(count): web::Query<RandomProjects>, web::Query(count): web::Query<RandomProjects>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -174,7 +225,20 @@ pub async fn random_projects_get(
} }
} }
#[get("projects")] /// Get multiple projects by ID or slug.
#[utoipa::path(
get,
operation_id = "getProjects",
params(
(
"ids" = String,
Query,
description = "The JSON array of project IDs or slugs"
)
),
responses((status = 200, description = "Expected response to a valid request"))
)]
#[get("/projects")]
pub async fn projects_get( pub async fn projects_get(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<ProjectIds>, web::Query(ids): web::Query<ProjectIds>,
@@ -205,7 +269,20 @@ pub async fn projects_get(
} }
} }
#[get("{id}")] /// Get a project by ID or slug.
#[utoipa::path(
get,
operation_id = "getProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{id}")]
pub async fn project_get( pub async fn project_get(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -241,7 +318,20 @@ pub async fn project_get(
} }
//checks the validity of a project id or slug //checks the validity of a project id or slug
#[get("{id}/check")] /// Check that a project ID or slug exists.
#[utoipa::path(
get,
operation_id = "checkProjectValidity",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{id}/check")]
pub async fn project_get_check( pub async fn project_get_check(
info: web::Path<(String,)>, info: web::Path<(String,)>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -253,13 +343,26 @@ pub async fn project_get_check(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize)] #[derive(Serialize, utoipa::ToSchema)]
struct DependencyInfo { struct DependencyInfo {
pub projects: Vec<LegacyProject>, pub projects: Vec<LegacyProject>,
pub versions: Vec<LegacyVersion>, pub versions: Vec<LegacyVersion>,
} }
#[get("dependencies")] /// Get dependency projects and versions for a project.
#[utoipa::path(
get,
operation_id = "getDependencies",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/dependencies")]
pub async fn dependency_list( pub async fn dependency_list(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -305,7 +408,7 @@ pub async fn dependency_list(
} }
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct EditProject { pub struct EditProject {
#[validate( #[validate(
length(min = 3, max = 64), length(min = 3, max = 64),
@@ -404,7 +507,26 @@ pub struct EditProject {
pub monetization_status: Option<MonetizationStatus>, pub monetization_status: Option<MonetizationStatus>,
} }
#[patch("{id}")] /// Modify a project.
#[utoipa::path(
patch,
operation_id = "modifyProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
request_body = EditProject,
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[patch("/{id}")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn project_edit( pub async fn project_edit(
req: HttpRequest, req: HttpRequest,
@@ -579,7 +701,7 @@ pub async fn project_edit(
Ok(response) Ok(response)
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct BulkEditProject { pub struct BulkEditProject {
#[validate(length(max = 3))] #[validate(length(max = 3))]
pub categories: Option<Vec<String>>, pub categories: Option<Vec<String>>,
@@ -642,7 +764,29 @@ pub struct BulkEditProject {
pub discord_url: Option<Option<String>>, pub discord_url: Option<Option<String>>,
} }
#[patch("projects")] /// Bulk-edit multiple projects.
#[utoipa::path(
patch,
operation_id = "patchProjects",
params(
(
"ids" = String,
Query,
description = "The JSON array of project IDs or slugs"
)
),
request_body = BulkEditProject,
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[patch("/projects")]
pub async fn projects_edit( pub async fn projects_edit(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<ProjectIds>, web::Query(ids): web::Query<ProjectIds>,
@@ -739,12 +883,40 @@ pub async fn projects_edit(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Extension { pub struct Extension {
pub ext: String, pub ext: String,
} }
#[patch("{id}/icon")] /// Change a project's icon.
#[utoipa::path(
patch,
operation_id = "changeProjectIcon",
params(
("id" = String, Path, description = "The ID or slug of the project"),
(
"ext" = String,
Query,
description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)"
)
),
request_body(
content(
("image/png"),
("image/jpeg"),
("image/bmp"),
("image/gif"),
("image/webp"),
("image/svg+xml")
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[patch("/{id}/icon")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn project_icon_edit( pub async fn project_icon_edit(
web::Query(ext): web::Query<Extension>, web::Query(ext): web::Query<Extension>,
@@ -771,7 +943,22 @@ pub async fn project_icon_edit(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}/icon")] /// Delete a project's icon.
#[utoipa::path(
delete,
operation_id = "deleteProjectIcon",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[delete("/{id}/icon")]
pub async fn delete_project_icon( pub async fn delete_project_icon(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -793,7 +980,7 @@ pub async fn delete_project_icon(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct GalleryCreateQuery { pub struct GalleryCreateQuery {
pub featured: bool, pub featured: bool,
#[validate(length(min = 1, max = 255))] #[validate(length(min = 1, max = 255))]
@@ -803,7 +990,63 @@ pub struct GalleryCreateQuery {
pub ordering: Option<i64>, pub ordering: Option<i64>,
} }
#[post("{id}/gallery")] /// Add a gallery image to a project.
#[utoipa::path(
post,
operation_id = "addGalleryImage",
params(
("id" = String, Path, description = "The ID or slug of the project"),
(
"ext" = String,
Query,
description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)"
),
(
"featured" = bool,
Query,
description = "Whether this image is featured"
),
(
"title" = Option<String>,
Query,
description = "Image title"
),
(
"description" = Option<String>,
Query,
description = "Image description"
),
(
"ordering" = Option<i64>,
Query,
description = "Image ordering"
)
),
request_body(
content(
("image/png"),
("image/jpeg"),
("image/bmp"),
("image/gif"),
("image/webp"),
("image/svg+xml")
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[post("/{id}/gallery")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn add_gallery_item( pub async fn add_gallery_item(
web::Query(ext): web::Query<Extension>, web::Query(ext): web::Query<Extension>,
@@ -837,7 +1080,7 @@ pub async fn add_gallery_item(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct GalleryEditQuery { pub struct GalleryEditQuery {
/// The url of the gallery item to edit /// The url of the gallery item to edit
pub url: String, pub url: String,
@@ -859,7 +1102,48 @@ pub struct GalleryEditQuery {
pub ordering: Option<i64>, pub ordering: Option<i64>,
} }
#[patch("{id}/gallery")] /// Modify a gallery image.
#[utoipa::path(
patch,
operation_id = "modifyGalleryImage",
params(
("id" = String, Path, description = "The ID or slug of the project"),
("url" = String, Query, description = "URL of the image to edit"),
(
"featured" = Option<bool>,
Query,
description = "Whether this image is featured"
),
(
"title" = Option<Option<String>>,
Query,
description = "Image title"
),
(
"description" = Option<Option<String>>,
Query,
description = "Image description"
),
(
"ordering" = Option<i64>,
Query,
description = "Image ordering"
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[patch("/{id}/gallery")]
pub async fn edit_gallery_item( pub async fn edit_gallery_item(
req: HttpRequest, req: HttpRequest,
web::Query(item): web::Query<GalleryEditQuery>, web::Query(item): web::Query<GalleryEditQuery>,
@@ -885,12 +1169,30 @@ pub async fn edit_gallery_item(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct GalleryDeleteQuery { pub struct GalleryDeleteQuery {
pub url: String, pub url: String,
} }
#[delete("{id}/gallery")] /// Delete a gallery image.
#[utoipa::path(
delete,
operation_id = "deleteGalleryImage",
params(
("id" = String, Path, description = "The ID or slug of the project"),
("url" = String, Query, description = "URL of the image to delete")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[delete("/{id}/gallery")]
pub async fn delete_gallery_item( pub async fn delete_gallery_item(
req: HttpRequest, req: HttpRequest,
web::Query(item): web::Query<GalleryDeleteQuery>, web::Query(item): web::Query<GalleryDeleteQuery>,
@@ -912,7 +1214,22 @@ pub async fn delete_gallery_item(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}")] /// Delete a project by ID or slug.
#[utoipa::path(
delete,
operation_id = "deleteProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_DELETE"]))
)]
#[delete("/{id}")]
pub async fn project_delete( pub async fn project_delete(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -935,7 +1252,22 @@ pub async fn project_delete(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[post("{id}/follow")] /// Follow a project.
#[utoipa::path(
post,
operation_id = "followProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_WRITE"]))
)]
#[post("/{id}/follow")]
pub async fn project_follow( pub async fn project_follow(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -949,7 +1281,22 @@ pub async fn project_follow(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}/follow")] /// Unfollow a project.
#[utoipa::path(
delete,
operation_id = "unfollowProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_WRITE"]))
)]
#[delete("/{id}/follow")]
pub async fn project_unfollow( pub async fn project_unfollow(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,

View File

@@ -8,7 +8,7 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use serde::Deserialize; use serde::Deserialize;
use validator::Validate; use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(reports_get); cfg.service(reports_get);
cfg.service(reports); cfg.service(reports);
cfg.service(report_create); cfg.service(report_create);
@@ -17,7 +17,21 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(report_get); cfg.service(report_get);
} }
#[post("report")] /// Create a report for a project, version, or user.
#[utoipa::path(
post,
operation_id = "submitReport",
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["REPORT_CREATE"]))
)]
#[post("/report")]
pub async fn report_create( pub async fn report_create(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -40,7 +54,7 @@ pub async fn report_create(
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct ReportsRequestOptions { pub struct ReportsRequestOptions {
#[serde(default = "default_count")] #[serde(default = "default_count")]
count: u16, count: u16,
@@ -55,7 +69,31 @@ fn default_all() -> bool {
true true
} }
#[get("report")] /// Get open reports for the current user.
#[utoipa::path(
get,
operation_id = "getOpenReports",
params(
(
"count" = Option<u16>,
Query,
description = "Maximum number of reports to return"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["REPORT_READ"]))
)]
#[get("/report")]
pub async fn reports( pub async fn reports(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -88,12 +126,36 @@ pub async fn reports(
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct ReportIds { pub struct ReportIds {
pub ids: String, pub ids: String,
} }
#[get("reports")] /// Get multiple reports by ID.
#[utoipa::path(
get,
operation_id = "getReports",
params(
(
"ids" = String,
Query,
description = "The JSON array of report IDs"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["REPORT_READ"]))
)]
#[get("/reports")]
pub async fn reports_get( pub async fn reports_get(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<ReportIds>, web::Query(ids): web::Query<ReportIds>,
@@ -122,7 +184,25 @@ pub async fn reports_get(
} }
} }
#[get("report/{id}")] /// Get a report by ID.
#[utoipa::path(
get,
operation_id = "getReport",
params(("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["REPORT_READ"]))
)]
#[get("/report/{id}")]
pub async fn report_get( pub async fn report_get(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -145,14 +225,34 @@ pub async fn report_get(
} }
} }
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct EditReport { pub struct EditReport {
#[validate(length(max = 65536))] #[validate(length(max = 65536))]
pub body: Option<String>, pub body: Option<String>,
pub closed: Option<bool>, pub closed: Option<bool>,
} }
#[patch("report/{id}")] /// Modify a report.
#[utoipa::path(
patch,
operation_id = "modifyReport",
params(("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")),
request_body = EditReport,
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["REPORT_WRITE"]))
)]
#[patch("/report/{id}")]
pub async fn report_edit( pub async fn report_edit(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -178,7 +278,25 @@ pub async fn report_edit(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("report/{id}")] /// Delete a report by ID.
#[utoipa::path(
delete,
operation_id = "deleteReport",
params(("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["REPORT_DELETE"]))
)]
#[delete("/report/{id}")]
pub async fn report_delete( pub async fn report_delete(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,

View File

@@ -5,11 +5,11 @@ use crate::routes::{
}; };
use actix_web::{HttpResponse, get, web}; use actix_web::{HttpResponse, get, web};
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(get_stats); cfg.service(get_stats);
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize, utoipa::ToSchema)]
pub struct V2Stats { pub struct V2Stats {
pub projects: Option<i64>, pub projects: Option<i64>,
pub versions: Option<i64>, pub versions: Option<i64>,
@@ -17,7 +17,19 @@ pub struct V2Stats {
pub files: Option<i64>, pub files: Option<i64>,
} }
#[get("statistics")] /// Get aggregate instance statistics.
#[utoipa::path(
get,
operation_id = "statistics",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = V2Stats
)
)
)]
#[get("/statistics")]
pub async fn get_stats( pub async fn get_stats(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {

View File

@@ -12,9 +12,9 @@ use actix_web::{HttpResponse, get, web};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use itertools::Itertools; use itertools::Itertools;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service( cfg.service(
web::scope("tag") utoipa_actix_web::scope("/tag")
.service(category_list) .service(category_list)
.service(loader_list) .service(loader_list)
.service(game_version_list) .service(game_version_list)
@@ -27,7 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct CategoryData { pub struct CategoryData {
pub icon: String, pub icon: String,
pub name: String, pub name: String,
@@ -35,7 +35,19 @@ pub struct CategoryData {
pub header: String, pub header: String,
} }
#[get("category")] /// Get the list of project categories.
#[utoipa::path(
get,
operation_id = "categoryList",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<CategoryData>
)
)
)]
#[get("/category")]
pub async fn category_list( pub async fn category_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
@@ -62,14 +74,26 @@ pub async fn category_list(
} }
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct LoaderData { pub struct LoaderData {
pub icon: String, pub icon: String,
pub name: String, pub name: String,
pub supported_project_types: Vec<String>, pub supported_project_types: Vec<String>,
} }
#[get("loader")] /// Get the list of loaders.
#[utoipa::path(
get,
operation_id = "loaderList",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<LoaderData>
)
)
)]
#[get("/loader")]
pub async fn loader_list( pub async fn loader_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
@@ -116,7 +140,7 @@ pub async fn loader_list(
} }
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct GameVersionQueryData { pub struct GameVersionQueryData {
pub version: String, pub version: String,
pub version_type: String, pub version_type: String,
@@ -124,14 +148,38 @@ pub struct GameVersionQueryData {
pub major: bool, pub major: bool,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct GameVersionQuery { pub struct GameVersionQuery {
#[serde(rename = "type")] #[serde(rename = "type")]
type_: Option<String>, type_: Option<String>,
major: Option<bool>, major: Option<bool>,
} }
#[get("game_version")] /// Get the list of game versions.
#[utoipa::path(
get,
operation_id = "versionList",
params(
(
"type" = Option<String>,
Query,
description = "Optional game version type filter"
),
(
"major" = Option<bool>,
Query,
description = "Whether to return only major versions"
)
),
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<GameVersionQueryData>
)
)
)]
#[get("/game_version")]
pub async fn game_version_list( pub async fn game_version_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
query: web::Query<GameVersionQuery>, query: web::Query<GameVersionQuery>,
@@ -185,13 +233,25 @@ pub async fn game_version_list(
) )
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize, utoipa::ToSchema)]
pub struct License { pub struct License {
pub short: String, pub short: String,
pub name: String, pub name: String,
} }
#[get("license")] /// Get SPDX license identifiers and names.
#[utoipa::path(
get,
operation_id = "licenseList",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<License>
)
)
)]
#[get("/license")]
pub async fn license_list() -> HttpResponse { pub async fn license_list() -> HttpResponse {
let response = v3::tags::license_list().await; let response = v3::tags::license_list().await;
@@ -212,13 +272,27 @@ pub async fn license_list() -> HttpResponse {
} }
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize, utoipa::ToSchema)]
pub struct LicenseText { pub struct LicenseText {
pub title: String, pub title: String,
pub body: String, pub body: String,
} }
#[get("license/{id}")] /// Get full license text by SPDX ID.
#[utoipa::path(
get,
operation_id = "licenseText",
params(("id" = String, Path, description = "The license ID to get the text for")),
responses(
(
status = 200,
description = "Expected response to a valid request",
body = LicenseText
),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[get("/license/{id}")]
pub async fn license_text( pub async fn license_text(
params: web::Path<(String,)>, params: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
@@ -240,7 +314,9 @@ pub async fn license_text(
) )
} }
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)] #[derive(
serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, utoipa::ToSchema,
)]
pub struct DonationPlatformQueryData { pub struct DonationPlatformQueryData {
// The difference between name and short is removed in v3. // The difference between name and short is removed in v3.
// Now, the 'id' becomes the name, and the 'name' is removed (the frontend uses the id as the name) // Now, the 'id' becomes the name, and the 'name' is removed (the frontend uses the id as the name)
@@ -249,7 +325,19 @@ pub struct DonationPlatformQueryData {
pub name: String, pub name: String,
} }
#[get("donation_platform")] /// Get available donation platforms.
#[utoipa::path(
get,
operation_id = "donationPlatformList",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<DonationPlatformQueryData>
)
)
)]
#[get("/donation_platform")]
pub async fn donation_platform_list( pub async fn donation_platform_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
@@ -295,7 +383,19 @@ pub async fn donation_platform_list(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[get("report_type")] /// Get valid report types.
#[utoipa::path(
get,
operation_id = "reportTypeList",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<String>
)
)
)]
#[get("/report_type")]
pub async fn report_type_list( pub async fn report_type_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
@@ -306,7 +406,19 @@ pub async fn report_type_list(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[get("project_type")] /// Get valid project types.
#[utoipa::path(
get,
operation_id = "projectTypeList",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<String>
)
)
)]
#[get("/project_type")]
pub async fn project_type_list( pub async fn project_type_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
@@ -317,7 +429,19 @@ pub async fn project_type_list(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[get("side_type")] /// Get valid side-type values.
#[utoipa::path(
get,
operation_id = "sideTypeList",
responses(
(
status = 200,
description = "Expected response to a valid request",
body = Vec<String>
)
)
)]
#[get("/side_type")]
pub async fn side_type_list() -> Result<HttpResponse, ApiError> { pub async fn side_type_list() -> Result<HttpResponse, ApiError> {
// Original side types are no longer reflected in the database. // Original side types are no longer reflected in the database.
// Therefore, we hardcode and return all the fields that are supported by our v2 conversion logic. // Therefore, we hardcode and return all the fields that are supported by our v2 conversion logic.

View File

@@ -12,11 +12,10 @@ use ariadne::ids::UserId;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(teams_get); cfg.service(teams_get);
cfg.service( cfg.service(
web::scope("team") utoipa_actix_web::scope("/team")
.service(team_members_get) .service(team_members_get)
.service(edit_team_member) .service(edit_team_member)
.service(transfer_ownership) .service(transfer_ownership)
@@ -31,7 +30,20 @@ pub fn config(cfg: &mut web::ServiceConfig) {
// also the members of the organization's team if the project is associated with an organization // also the members of the organization's team if the project is associated with an organization
// (Unlike team_members_get_project, which only returns the members of the project's team) // (Unlike team_members_get_project, which only returns the members of the project's team)
// They can be differentiated by the "organization_permissions" field being null or not // They can be differentiated by the "organization_permissions" field being null or not
#[get("{id}/members")] /// Get a project's team members.
#[utoipa::path(
get,
operation_id = "getProjectTeamMembers",
params(("id" = String, Path, description = "The ID or slug of the project")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{id}/members")]
pub async fn team_members_get_project( pub async fn team_members_get_project(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -62,7 +74,15 @@ pub async fn team_members_get_project(
} }
// Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project) // Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project)
#[get("{id}/members")] /// Get a team's members.
#[utoipa::path(
get,
operation_id = "getTeamMembers",
params(("id" = TeamId, Path, description = "The ID of the team")),
responses((status = 200, description = "Expected response to a valid request")),
security(("bearer_auth" = ["PROJECT_READ"]))
)]
#[get("/{id}/members")]
pub async fn team_members_get( pub async fn team_members_get(
req: HttpRequest, req: HttpRequest,
info: web::Path<(TeamId,)>, info: web::Path<(TeamId,)>,
@@ -87,12 +107,19 @@ pub async fn team_members_get(
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct TeamIds { pub struct TeamIds {
pub ids: String, pub ids: String,
} }
#[get("teams")] /// Get the members of multiple teams.
#[utoipa::path(
get,
operation_id = "getTeams",
params(("ids" = String, Query, description = "The JSON array of team IDs")),
responses((status = 200, description = "Expected response to a valid request"))
)]
#[get("/teams")]
pub async fn teams_get( pub async fn teams_get(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<TeamIds>, web::Query(ids): web::Query<TeamIds>,
@@ -127,7 +154,25 @@ pub async fn teams_get(
} }
} }
#[post("{id}/join")] /// Join a team with a pending invite.
#[utoipa::path(
post,
operation_id = "joinTeam",
params(("id" = TeamId, Path, description = "The ID of the team")),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[post("/{id}/join")]
pub async fn join_team( pub async fn join_team(
req: HttpRequest, req: HttpRequest,
info: web::Path<(TeamId,)>, info: web::Path<(TeamId,)>,
@@ -149,7 +194,7 @@ fn default_ordering() -> i64 {
0 0
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct NewTeamMember { pub struct NewTeamMember {
pub user_id: UserId, pub user_id: UserId,
#[serde(default = "default_role")] #[serde(default = "default_role")]
@@ -165,7 +210,26 @@ pub struct NewTeamMember {
pub ordering: i64, pub ordering: i64,
} }
#[post("{id}/members")] /// Add a member to a team.
#[utoipa::path(
post,
operation_id = "addTeamMember",
params(("id" = TeamId, Path, description = "The ID of the team")),
request_body = NewTeamMember,
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[post("/{id}/members")]
pub async fn add_team_member( pub async fn add_team_member(
req: HttpRequest, req: HttpRequest,
info: web::Path<(TeamId,)>, info: web::Path<(TeamId,)>,
@@ -194,7 +258,7 @@ pub async fn add_team_member(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct EditTeamMember { pub struct EditTeamMember {
pub permissions: Option<ProjectPermissions>, pub permissions: Option<ProjectPermissions>,
pub organization_permissions: Option<OrganizationPermissions>, pub organization_permissions: Option<OrganizationPermissions>,
@@ -203,7 +267,33 @@ pub struct EditTeamMember {
pub ordering: Option<i64>, pub ordering: Option<i64>,
} }
#[patch("{id}/members/{user_id}")] /// Modify a team member.
#[utoipa::path(
patch,
operation_id = "modifyTeamMember",
params(
("id" = TeamId, Path, description = "The ID of the team"),
(
"user_id" = UserId,
Path,
description = "The ID of the user to modify"
)
),
request_body = EditTeamMember,
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[patch("/{id}/members/{user_id}")]
pub async fn edit_team_member( pub async fn edit_team_member(
req: HttpRequest, req: HttpRequest,
info: web::Path<(TeamId, UserId)>, info: web::Path<(TeamId, UserId)>,
@@ -231,12 +321,31 @@ pub async fn edit_team_member(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct TransferOwnership { pub struct TransferOwnership {
pub user_id: UserId, pub user_id: UserId,
} }
#[patch("{id}/owner")] /// Transfer team ownership.
#[utoipa::path(
patch,
operation_id = "transferTeamOwnership",
params(("id" = TeamId, Path, description = "The ID of the team")),
request_body = TransferOwnership,
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[patch("/{id}/owner")]
pub async fn transfer_ownership( pub async fn transfer_ownership(
req: HttpRequest, req: HttpRequest,
info: web::Path<(TeamId,)>, info: web::Path<(TeamId,)>,
@@ -260,7 +369,32 @@ pub async fn transfer_ownership(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}/members/{user_id}")] /// Remove a member from a team.
#[utoipa::path(
delete,
operation_id = "deleteTeamMember",
params(
("id" = TeamId, Path, description = "The ID of the team"),
(
"user_id" = UserId,
Path,
description = "The ID of the user to remove"
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["PROJECT_WRITE"]))
)]
#[delete("/{id}/members/{user_id}")]
pub async fn remove_team_member( pub async fn remove_team_member(
req: HttpRequest, req: HttpRequest,
info: web::Path<(TeamId, UserId)>, info: web::Path<(TeamId, UserId)>,

View File

@@ -11,17 +11,31 @@ use crate::routes::{ApiError, v2_reroute, v3};
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use serde::Deserialize; use serde::Deserialize;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service( cfg.service(
web::scope("thread") utoipa_actix_web::scope("/thread")
.service(thread_get) .service(thread_get)
.service(thread_send_message), .service(thread_send_message),
); );
cfg.service(web::scope("message").service(message_delete)); cfg.service(utoipa_actix_web::scope("/message").service(message_delete));
cfg.service(threads_get); cfg.service(threads_get);
} }
#[get("{id}")] /// Get a thread by ID.
#[utoipa::path(
get,
operation_id = "getThread",
params(("id" = ThreadId, Path, description = "The ID of the thread")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["THREAD_READ"]))
)]
#[get("/{id}")]
pub async fn thread_get( pub async fn thread_get(
req: HttpRequest, req: HttpRequest,
info: web::Path<(ThreadId,)>, info: web::Path<(ThreadId,)>,
@@ -34,12 +48,26 @@ pub async fn thread_get(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct ThreadIds { pub struct ThreadIds {
pub ids: String, pub ids: String,
} }
#[get("threads")] /// Get multiple threads by ID.
#[utoipa::path(
get,
operation_id = "getThreads",
params(("ids" = String, Query, description = "The JSON array of thread IDs")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["THREAD_READ"]))
)]
#[get("/threads")]
pub async fn threads_get( pub async fn threads_get(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<ThreadIds>, web::Query(ids): web::Query<ThreadIds>,
@@ -70,12 +98,28 @@ pub async fn threads_get(
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct NewThreadMessage { pub struct NewThreadMessage {
pub body: MessageBody, pub body: MessageBody,
} }
#[post("{id}")] /// Send a message to a thread.
#[utoipa::path(
post,
operation_id = "sendThreadMessage",
params(("id" = ThreadId, Path, description = "The ID of the thread")),
request_body = NewThreadMessage,
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["THREAD_WRITE"]))
)]
#[post("/{id}")]
pub async fn thread_send_message( pub async fn thread_send_message(
req: HttpRequest, req: HttpRequest,
info: web::Path<(ThreadId,)>, info: web::Path<(ThreadId,)>,
@@ -100,7 +144,25 @@ pub async fn thread_send_message(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}")] /// Delete a thread message by ID.
#[utoipa::path(
delete,
operation_id = "deleteThreadMessage",
params(("id" = ThreadMessageId, Path, description = "The ID of the message")),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["THREAD_WRITE"]))
)]
#[delete("/{id}")]
pub async fn message_delete( pub async fn message_delete(
req: HttpRequest, req: HttpRequest,
info: web::Path<(ThreadMessageId,)>, info: web::Path<(ThreadMessageId,)>,

View File

@@ -14,12 +14,11 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use validator::Validate; use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(user_auth_get); cfg.service(user_auth_get);
cfg.service(users_get); cfg.service(users_get);
cfg.service( cfg.service(
web::scope("user") utoipa_actix_web::scope("/user")
.service(user_get) .service(user_get)
.service(projects_list) .service(projects_list)
.service(user_delete) .service(user_delete)
@@ -31,7 +30,20 @@ pub fn config(cfg: &mut web::ServiceConfig) {
); );
} }
#[get("user")] /// Get the current user from the authorization header.
#[utoipa::path(
get,
operation_id = "getUserFromAuth",
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_READ"]))
)]
#[get("/user")]
pub async fn user_auth_get( pub async fn user_auth_get(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -52,12 +64,19 @@ pub async fn user_auth_get(
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserIds { pub struct UserIds {
pub ids: String, pub ids: String,
} }
#[get("users")] /// Get multiple users by ID.
#[utoipa::path(
get,
operation_id = "getUsers",
params(("ids" = String, Query, description = "The JSON array of user IDs")),
responses((status = 200, description = "Expected response to a valid request"))
)]
#[get("/users")]
pub async fn users_get( pub async fn users_get(
web::Query(ids): web::Query<UserIds>, web::Query(ids): web::Query<UserIds>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -82,7 +101,20 @@ pub async fn users_get(
} }
} }
#[get("{id}")] /// Get a user by ID or username.
#[utoipa::path(
get,
operation_id = "getUser",
params(("id" = String, Path, description = "The ID or username of the user")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{id}")]
pub async fn user_get( pub async fn user_get(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -104,7 +136,20 @@ pub async fn user_get(
} }
} }
#[get("{user_id}/projects")] /// Get a user's projects.
#[utoipa::path(
get,
operation_id = "getUserProjects",
params(("user_id" = String, Path, description = "The ID or username of the user")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{user_id}/projects")]
pub async fn projects_list( pub async fn projects_list(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -133,7 +178,7 @@ pub async fn projects_list(
} }
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct EditUser { pub struct EditUser {
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_USERNAME))] #[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_USERNAME))]
pub username: Option<String>, pub username: Option<String>,
@@ -156,7 +201,26 @@ pub struct EditUser {
pub allow_friend_requests: Option<bool>, pub allow_friend_requests: Option<bool>,
} }
#[patch("{id}")] /// Modify a user.
#[utoipa::path(
patch,
operation_id = "modifyUser",
params(("id" = String, Path, description = "The ID or username of the user")),
request_body = EditUser,
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_WRITE"]))
)]
#[patch("/{id}")]
pub async fn user_edit( pub async fn user_edit(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -186,12 +250,44 @@ pub async fn user_edit(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Extension { pub struct Extension {
pub ext: String, pub ext: String,
} }
#[patch("{id}/icon")] /// Change a user's avatar.
#[utoipa::path(
patch,
operation_id = "changeUserIcon",
params(
("id" = String, Path, description = "The ID or username of the user"),
(
"ext" = String,
Query,
description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)"
)
),
request_body(
content(
("image/png"),
("image/jpeg"),
("image/bmp"),
("image/gif"),
("image/webp"),
("image/svg+xml")
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_WRITE"]))
)]
#[patch("/{id}/icon")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn user_icon_edit( pub async fn user_icon_edit(
web::Query(ext): web::Query<Extension>, web::Query(ext): web::Query<Extension>,
@@ -218,7 +314,22 @@ pub async fn user_icon_edit(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}/icon")] /// Remove a user's avatar.
#[utoipa::path(
delete,
operation_id = "deleteUserIcon",
params(("id" = String, Path, description = "The ID or username of the user")),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_WRITE"]))
)]
#[delete("/{id}/icon")]
pub async fn user_icon_delete( pub async fn user_icon_delete(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -240,7 +351,25 @@ pub async fn user_icon_delete(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[delete("{id}")] /// Delete a user by ID or username.
#[utoipa::path(
delete,
operation_id = "deleteUser",
params(("id" = String, Path, description = "The ID or username of the user")),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_DELETE"]))
)]
#[delete("/{id}")]
pub async fn user_delete( pub async fn user_delete(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -255,7 +384,25 @@ pub async fn user_delete(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[get("{id}/follows")] /// Get projects followed by a user.
#[utoipa::path(
get,
operation_id = "getFollowedProjects",
params(("id" = String, Path, description = "The ID or username of the user")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["USER_READ"]))
)]
#[get("/{id}/follows")]
pub async fn user_follows( pub async fn user_follows(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -284,7 +431,25 @@ pub async fn user_follows(
} }
} }
#[get("{id}/notifications")] /// Get notifications for a user.
#[utoipa::path(
get,
operation_id = "getUserNotifications",
params(("id" = String, Path, description = "The ID or username of the user")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["NOTIFICATION_READ"]))
)]
#[get("/{id}/notifications")]
pub async fn user_notifications( pub async fn user_notifications(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,

View File

@@ -75,7 +75,25 @@ pub struct InitialVersionData {
} }
// under `/api/v1/version` // under `/api/v1/version`
#[post("version")] /// Create a version on an existing project.
#[utoipa::path(
post,
operation_id = "createVersion",
request_body(
content(("multipart/form-data")),
description = "Multipart payload containing `data` and uploaded files"
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["VERSION_CREATE"]))
)]
#[post("/version")]
pub async fn version_create( pub async fn version_create(
req: HttpRequest, req: HttpRequest,
payload: Multipart, payload: Multipart,
@@ -280,7 +298,29 @@ async fn get_example_version_fields(
} }
// under /api/v1/version/{version_id} // under /api/v1/version/{version_id}
#[post("{version_id}/file")] /// Add files to an existing version.
#[utoipa::path(
post,
operation_id = "addFilesToVersion",
params(("version_id" = VersionId, Path, description = "The ID of the version")),
request_body(
content(("multipart/form-data")),
description = "Multipart payload containing files to upload"
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["VERSION_WRITE"]))
)]
#[post("/{version_id}/file")]
pub async fn upload_file_to_version( pub async fn upload_file_to_version(
req: HttpRequest, req: HttpRequest,
url_data: web::Path<(VersionId,)>, url_data: web::Path<(VersionId,)>,

View File

@@ -11,9 +11,9 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service( cfg.service(
web::scope("version_file") utoipa_actix_web::scope("/version_file")
.service(delete_file) .service(delete_file)
.service(get_version_from_hash) .service(get_version_from_hash)
.service(download_version) .service(download_version)
@@ -22,7 +22,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
); );
cfg.service( cfg.service(
web::scope("version_files") utoipa_actix_web::scope("/version_files")
.service(get_versions_from_hashes) .service(get_versions_from_hashes)
.service(update_files) .service(update_files)
.service(update_files_many) .service(update_files_many)
@@ -31,7 +31,36 @@ pub fn config(cfg: &mut web::ServiceConfig) {
} }
// under /api/v1/version_file/{hash} // under /api/v1/version_file/{hash}
#[get("{version_id}")] /// Get version metadata by file hash.
#[utoipa::path(
get,
operation_id = "versionFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID when hash maps to multiple files"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{version_id}")]
pub async fn get_version_from_hash( pub async fn get_version_from_hash(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -62,7 +91,36 @@ pub async fn get_version_from_hash(
} }
// under /api/v1/version_file/{hash}/download // under /api/v1/version_file/{hash}/download
#[get("{version_id}/download")] /// Download a file by hash.
#[utoipa::path(
get,
operation_id = "downloadVersionFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID when hash maps to multiple files"
)
),
responses(
(status = 302, description = "Temporary redirect to file URL"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{version_id}/download")]
pub async fn download_version( pub async fn download_version(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -85,7 +143,41 @@ pub async fn download_version(
} }
// under /api/v1/version_file/{hash} // under /api/v1/version_file/{hash}
#[delete("{version_id}")] /// Delete a file by hash.
#[utoipa::path(
delete,
operation_id = "deleteFileFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID to delete from"
)
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["VERSION_WRITE"]))
)]
#[delete("/{version_id}")]
pub async fn delete_file( pub async fn delete_file(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -107,14 +199,45 @@ pub async fn delete_file(
.or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct UpdateData { pub struct UpdateData {
pub loaders: Option<Vec<String>>, pub loaders: Option<Vec<String>>,
pub game_versions: Option<Vec<String>>, pub game_versions: Option<Vec<String>>,
pub version_types: Option<Vec<VersionType>>, pub version_types: Option<Vec<VersionType>>,
} }
#[post("{version_id}/update")] /// Get the latest compatible version from a file hash.
#[utoipa::path(
post,
operation_id = "getLatestVersionFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID when hash maps to multiple files"
)
),
request_body = UpdateData,
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[post("/{version_id}/update")]
pub async fn get_update_from_hash( pub async fn get_update_from_hash(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -162,13 +285,23 @@ pub async fn get_update_from_hash(
} }
// Requests above with multiple versions below // Requests above with multiple versions below
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct FileHashes { pub struct FileHashes {
pub algorithm: Option<String>, pub algorithm: Option<String>,
pub hashes: Vec<String>, pub hashes: Vec<String>,
} }
// under /api/v2/version_files // under /api/v2/version_files
/// Get versions from file hashes.
#[utoipa::path(
post,
operation_id = "versionsFromHashes",
request_body = FileHashes,
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[post("")] #[post("")]
pub async fn get_versions_from_hashes( pub async fn get_versions_from_hashes(
req: HttpRequest, req: HttpRequest,
@@ -210,7 +343,17 @@ pub async fn get_versions_from_hashes(
} }
} }
#[post("project")] /// Get projects from file hashes.
#[utoipa::path(
post,
operation_id = "projectsFromHashes",
request_body = FileHashes,
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[post("/project")]
pub async fn get_projects_from_hashes( pub async fn get_projects_from_hashes(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@@ -268,7 +411,7 @@ pub async fn get_projects_from_hashes(
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct ManyUpdateData { pub struct ManyUpdateData {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<String>, pub hashes: Vec<String>,
@@ -277,7 +420,17 @@ pub struct ManyUpdateData {
pub version_types: Option<Vec<VersionType>>, pub version_types: Option<Vec<VersionType>>,
} }
#[post("update")] /// Get latest compatible versions for multiple hashes.
#[utoipa::path(
post,
operation_id = "getLatestVersionsFromHashes",
request_body = ManyUpdateData,
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[post("/update")]
pub async fn update_files( pub async fn update_files(
pool: web::Data<ReadOnlyPgPool>, pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
@@ -316,7 +469,17 @@ pub async fn update_files(
Ok(HttpResponse::Ok().json(v3_versions)) Ok(HttpResponse::Ok().json(v3_versions))
} }
#[post("update_many")] /// Get all latest compatible versions for multiple hashes.
#[utoipa::path(
post,
operation_id = "getLatestVersionsFromHashesMany",
request_body = ManyUpdateData,
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[post("/update_many")]
pub async fn update_files_many( pub async fn update_files_many(
pool: web::Data<ReadOnlyPgPool>, pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
@@ -358,7 +521,7 @@ pub async fn update_files_many(
Ok(HttpResponse::Ok().json(v3_versions)) Ok(HttpResponse::Ok().json(v3_versions))
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct FileUpdateData { pub struct FileUpdateData {
pub hash: String, pub hash: String,
pub loaders: Option<Vec<String>>, pub loaders: Option<Vec<String>>,
@@ -366,13 +529,23 @@ pub struct FileUpdateData {
pub version_types: Option<Vec<VersionType>>, pub version_types: Option<Vec<VersionType>>,
} }
#[derive(Deserialize)] #[derive(Deserialize, utoipa::ToSchema)]
pub struct ManyFileUpdateData { pub struct ManyFileUpdateData {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<FileUpdateData>, pub hashes: Vec<FileUpdateData>,
} }
#[post("update_individual")] /// Get latest versions with per-hash filters.
#[utoipa::path(
post,
operation_id = "getLatestVersionsFromHashesIndividual",
request_body = ManyFileUpdateData,
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[post("/update_individual")]
pub async fn update_individual_files( pub async fn update_individual_files(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,

View File

@@ -16,12 +16,11 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(versions_get); cfg.service(versions_get);
cfg.service(super::version_creation::version_create); cfg.service(super::version_creation::version_create);
cfg.service( cfg.service(
web::scope("version") utoipa_actix_web::scope("/version")
.service(version_get) .service(version_get)
.service(version_delete) .service(version_delete)
.service(version_edit) .service(version_edit)
@@ -29,7 +28,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct VersionListFilters { pub struct VersionListFilters {
pub game_versions: Option<String>, pub game_versions: Option<String>,
pub loaders: Option<String>, pub loaders: Option<String>,
@@ -45,7 +44,46 @@ fn default_true() -> bool {
true true
} }
#[get("version")] /// List versions for a project.
#[utoipa::path(
get,
operation_id = "getProjectVersions",
params(
(
"project_id" = String,
Path,
description = "The ID or slug of the project"
),
(
"loaders" = Option<String>,
Query,
description = "JSON array of loaders to filter by"
),
(
"game_versions" = Option<String>,
Query,
description = "JSON array of game versions to filter by"
),
(
"featured" = Option<bool>,
Query,
description = "Filter by featured status"
),
(
"include_changelog" = Option<bool>,
Query,
description = "Whether to include changelog fields"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/version")]
pub async fn version_list( pub async fn version_list(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
@@ -129,7 +167,31 @@ pub async fn version_list(
} }
// Given a project ID/slug and a version slug // Given a project ID/slug and a version slug
#[get("version/{slug}")] /// Get a project version by ID or version number.
#[utoipa::path(
get,
operation_id = "getVersionFromIdOrNumber",
params(
(
"project_id" = String,
Path,
description = "The ID or slug of the project"
),
(
"slug" = String,
Path,
description = "The version ID or version number"
)
),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/version/{slug}")]
pub async fn version_project_get( pub async fn version_project_get(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String, String)>, info: web::Path<(String, String)>,
@@ -157,14 +219,21 @@ pub async fn version_project_get(
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct VersionIds { pub struct VersionIds {
pub ids: String, pub ids: String,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub include_changelog: bool, pub include_changelog: bool,
} }
#[get("versions")] /// Get multiple versions by ID.
#[utoipa::path(
get,
operation_id = "getVersions",
params(("ids" = String, Query, description = "The JSON array of version IDs")),
responses((status = 200, description = "Expected response to a valid request"))
)]
#[get("/versions")]
pub async fn versions_get( pub async fn versions_get(
req: HttpRequest, req: HttpRequest,
web::Query(ids): web::Query<VersionIds>, web::Query(ids): web::Query<VersionIds>,
@@ -199,7 +268,20 @@ pub async fn versions_get(
} }
} }
#[get("{version_id}")] /// Get a version by ID.
#[utoipa::path(
get,
operation_id = "getVersion",
params(("version_id" = models::ids::VersionId, Path, description = "The ID of the version")),
responses(
(status = 200, description = "Expected response to a valid request"),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/{version_id}")]
pub async fn version_get( pub async fn version_get(
req: HttpRequest, req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>, info: web::Path<(models::ids::VersionId,)>,
@@ -223,7 +305,7 @@ pub async fn version_get(
} }
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct EditVersion { pub struct EditVersion {
#[validate( #[validate(
length(min = 1, max = 64), length(min = 1, max = 64),
@@ -251,14 +333,33 @@ pub struct EditVersion {
pub file_types: Option<Vec<EditVersionFileType>>, pub file_types: Option<Vec<EditVersionFileType>>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct EditVersionFileType { pub struct EditVersionFileType {
pub algorithm: String, pub algorithm: String,
pub hash: String, pub hash: String,
pub file_type: Option<FileType>, pub file_type: Option<FileType>,
} }
#[patch("{id}")] /// Modify an existing version.
#[utoipa::path(
patch,
operation_id = "modifyVersion",
params(("id" = VersionId, Path, description = "The ID of the version")),
request_body = EditVersion,
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["VERSION_WRITE"]))
)]
#[patch("/{id}")]
pub async fn version_edit( pub async fn version_edit(
req: HttpRequest, req: HttpRequest,
info: web::Path<(VersionId,)>, info: web::Path<(VersionId,)>,
@@ -350,7 +451,25 @@ pub async fn version_edit(
Ok(response) Ok(response)
} }
#[delete("{version_id}")] /// Delete a version by ID.
#[utoipa::path(
delete,
operation_id = "deleteVersion",
params(("version_id" = VersionId, Path, description = "The ID of the version")),
responses(
(status = 204, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
),
security(("bearer_auth" = ["VERSION_DELETE"]))
)]
#[delete("/{version_id}")]
pub async fn version_delete( pub async fn version_delete(
req: HttpRequest, req: HttpRequest,
info: web::Path<(VersionId,)>, info: web::Path<(VersionId,)>,