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.anrok_client.clone()))
.app_data(labrinth_config.rate_limiter.clone())
.configure(routes::v2::config)
.configure(routes::v3::config)
.configure(routes::internal::config)
.configure(routes::root_config)
@@ -374,6 +373,7 @@ pub fn utoipa_app_config(
|_cfg| ()
}
})
.configure(routes::v2::utoipa_config)
.configure(routes::v3::utoipa_config)
.configure(routes::internal::utoipa_config);
}

View File

@@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize};
use validator::Validate;
/// A project returned from the API
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct LegacyProject {
/// Relevant V2 fields- these were removed or modified in V3,
/// 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")]
pub enum LegacySideType {
Required,
@@ -290,7 +292,7 @@ impl LegacySideType {
}
/// A specific version of a project
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct LegacyVersion {
/// Relevant V2 fields- these were removed or modified in V3,
/// 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 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 id: String,
pub platform: String,

View File

@@ -124,6 +124,19 @@ bitflags::bitflags! {
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 {
// these scopes cannot be specified in a personal access token
pub fn restricted() -> Scopes {

View File

@@ -33,6 +33,23 @@ bitflags::bitflags! {
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 {
fn default() -> ProjectPermissions {
ProjectPermissions::empty()
@@ -92,6 +109,23 @@ bitflags::bitflags! {
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 {
fn default() -> OrganizationPermissions {
OrganizationPermissions::NONE

View File

@@ -17,15 +17,15 @@ use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::sync::Arc;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
web::scope("admin")
utoipa_actix_web::scope("/admin")
.service(count_download)
.service(force_reindex),
);
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct DownloadBody {
pub url: String,
pub project_id: ProjectId,
@@ -36,6 +36,14 @@ pub struct DownloadBody {
}
// This is an internal route, cannot be used without key
#[utoipa::path(
patch,
operation_id = "countDownload",
responses(
(status = 204, description = "Download counted successfully"),
(status = 400, description = "Invalid input")
)
)]
#[patch("/_count-download", guard = "admin_key_guard")]
#[allow(clippy::too_many_arguments)]
pub async fn count_download(
@@ -150,6 +158,14 @@ pub async fn count_download(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path(
post,
operation_id = "forceReindex",
responses(
(status = 204, description = "Search index rebuilt successfully"),
(status = 401, description = "Unauthorized")
)
)]
#[post("/_force_reindex", guard = "admin_key_guard")]
pub async fn force_reindex(
pool: web::Data<PgPool>,

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ use crate::util::error::Context;
use crate::util::ext::get_image_ext;
use crate::util::img::upload_image_optimized;
use crate::util::validate::validation_errors_to_string;
use actix_web::web::{Data, Query, ServiceConfig, scope};
use actix_web::web::{Data, Query};
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
@@ -43,9 +43,9 @@ use tracing::info;
use validator::Validate;
use zxcvbn::Score;
pub fn config(cfg: &mut ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
scope("auth")
utoipa_actix_web::scope("/auth")
.service(init)
.service(auth_callback)
.service(delete_auth_provider)
@@ -1041,7 +1041,7 @@ impl AuthProvider {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct AuthorizationInit {
pub url: String,
#[serde(default)]
@@ -1051,7 +1051,7 @@ pub struct AuthorizationInit {
/// this will be set to the user's auth token from the frontend.
pub auth_token: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Authorization {
pub code: String,
pub state: String,
@@ -1059,7 +1059,15 @@ pub struct Authorization {
// Init link takes us to GitHub API and calls back to callback endpoint with a code and state
// http://localhost:8000/auth/init?url=https://modrinth.com
#[get("init")]
#[utoipa::path(
get,
operation_id = "authInit",
responses(
(status = 307, description = "Redirect to OAuth provider"),
(status = 400, description = "Invalid input")
)
)]
#[get("/init")]
pub async fn init(
req: HttpRequest,
Query(info): Query<AuthorizationInit>, // callback url
@@ -1140,7 +1148,15 @@ pub async fn init(
.json(serde_json::json!({ "url": url })))
}
#[get("callback")]
#[utoipa::path(
get,
operation_id = "authCallback",
responses(
(status = 307, description = "Redirect with auth code"),
(status = 401, description = "Authentication failed")
)
)]
#[get("/callback")]
pub async fn auth_callback(
req: HttpRequest,
Query(query): Query<HashMap<String, String>>,
@@ -1336,12 +1352,22 @@ pub async fn auth_callback(
Ok(res?)
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct DeleteAuthProvider {
pub provider: AuthProvider,
}
#[delete("provider")]
#[utoipa::path(
delete,
operation_id = "deleteAuthProvider",
responses(
(status = 204, description = "Auth provider removed"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["USER_AUTH_WRITE"]))
)]
#[delete("/provider")]
pub async fn delete_auth_provider(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1425,7 +1451,7 @@ pub async fn check_sendy_subscription(
Ok(response.trim() == "Subscribed")
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct NewAccount {
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))]
pub username: String,
@@ -1437,7 +1463,15 @@ pub struct NewAccount {
pub sign_up_newsletter: Option<bool>,
}
#[post("create")]
#[utoipa::path(
post,
operation_id = "createAccountPassword",
responses(
(status = 200, description = "Account created"),
(status = 400, description = "Invalid input")
)
)]
#[post("/create")]
pub async fn create_account_with_password(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1566,7 +1600,7 @@ pub async fn create_account_with_password(
Ok(HttpResponse::Ok().json(res))
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct Login {
#[serde(rename = "username")]
pub username_or_email: String,
@@ -1574,7 +1608,15 @@ pub struct Login {
pub challenge: String,
}
#[post("login")]
#[utoipa::path(
post,
operation_id = "loginPassword",
responses(
(status = 200, description = "Login successful"),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login")]
pub async fn login_password(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1639,7 +1681,7 @@ pub async fn login_password(
}
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct Login2FA {
pub code: String,
pub flow: String,
@@ -1724,7 +1766,15 @@ async fn validate_2fa_code(
}
}
#[post("login/2fa")]
#[utoipa::path(
post,
operation_id = "login2fa",
responses(
(status = 200, description = "2FA login successful"),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login/2fa")]
pub async fn login_2fa(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1773,7 +1823,16 @@ pub async fn login_2fa(
}
}
#[post("2fa/get_secret")]
#[utoipa::path(
post,
operation_id = "begin2faFlow",
responses(
(status = 200, description = "2FA secret generated"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa/get_secret")]
pub async fn begin_2fa_flow(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1812,7 +1871,16 @@ pub async fn begin_2fa_flow(
}
}
#[post("2fa")]
#[utoipa::path(
post,
operation_id = "finish2faFlow",
responses(
(status = 200, description = "2FA enabled"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa")]
pub async fn finish_2fa_flow(
req: HttpRequest,
pool: Data<PgPool>,
@@ -1930,12 +1998,21 @@ pub async fn finish_2fa_flow(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct Remove2FA {
pub code: String,
}
#[delete("2fa")]
#[utoipa::path(
delete,
operation_id = "remove2fa",
responses(
(status = 204, description = "2FA removed"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[delete("/2fa")]
pub async fn remove_2fa(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2016,14 +2093,22 @@ pub async fn remove_2fa(
Ok(HttpResponse::NoContent().finish())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ResetPassword {
#[serde(rename = "username")]
pub username_or_email: String,
pub challenge: String,
}
#[post("password/reset")]
#[utoipa::path(
post,
operation_id = "resetPasswordBegin",
responses(
(status = 204, description = "Password reset email sent"),
(status = 400, description = "Invalid input")
)
)]
#[post("/password/reset")]
pub async fn reset_password_begin(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2111,14 +2196,24 @@ pub async fn reset_password_begin(
Ok(HttpResponse::Ok().finish())
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct ChangePassword {
pub flow: Option<String>,
pub old_password: Option<String>,
pub new_password: Option<String>,
}
#[patch("password")]
#[utoipa::path(
patch,
operation_id = "changePassword",
responses(
(status = 204, description = "Password changed"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[patch("/password")]
pub async fn change_password(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2265,13 +2360,23 @@ pub async fn change_password(
Ok(HttpResponse::Ok().finish())
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct SetEmail {
#[validate(email)]
pub email: String,
}
#[patch("email")]
#[utoipa::path(
patch,
operation_id = "setEmail",
responses(
(status = 204, description = "Email set"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[patch("/email")]
pub async fn set_email(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2380,7 +2485,16 @@ pub async fn set_email(
Ok(HttpResponse::Ok().finish())
}
#[post("email/resend_verify")]
#[utoipa::path(
post,
operation_id = "resendVerifyEmail",
responses(
(status = 204, description = "Verification email resent"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/email/resend_verify")]
pub async fn resend_verify_email(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2438,12 +2552,20 @@ pub async fn resend_verify_email(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct VerifyEmail {
pub flow: String,
}
#[post("email/verify")]
#[utoipa::path(
post,
operation_id = "verifyEmail",
responses(
(status = 204, description = "Email verified"),
(status = 400, description = "Invalid input")
)
)]
#[post("/email/verify")]
pub async fn verify_email(
pool: Data<PgPool>,
redis: Data<RedisPool>,
@@ -2498,7 +2620,16 @@ pub async fn verify_email(
}
}
#[post("email/subscribe")]
#[utoipa::path(
post,
operation_id = "subscribeNewsletter",
responses(
(status = 204, description = "Newsletter subscription toggled"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/email/subscribe")]
pub async fn subscribe_newsletter(
req: HttpRequest,
pool: Data<PgPool>,
@@ -2535,7 +2666,16 @@ pub async fn subscribe_newsletter(
Ok(HttpResponse::NoContent().finish())
}
#[get("email/subscribe")]
#[utoipa::path(
get,
operation_id = "getNewsletterSubscriptionStatus",
responses(
(status = 200, description = "Subscription status"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[get("/email/subscribe")]
pub async fn get_newsletter_subscription_status(
req: HttpRequest,
pool: Data<PgPool>,

View File

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

View File

@@ -14,7 +14,7 @@ use crate::routes::ApiError;
use crate::util::guards::medal_key_guard;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("medal").service(verify).service(redeem));
cfg.service(web::scope("/medal").service(verify).service(redeem));
}
#[derive(Deserialize)]

View File

@@ -22,13 +22,45 @@ use crate::util::cors::default_cors;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
actix_web::web::scope("_internal")
actix_web::web::scope("/_internal")
.wrap(default_cors())
.configure(admin::config)
.configure(|cfg| {
cfg.service(
actix_web::web::scope("/admin")
.service(admin::count_download)
.service(admin::force_reindex),
);
cfg.service(
actix_web::web::scope("/session")
.service(session::list)
.service(session::delete)
.service(session::refresh),
);
cfg.service(
actix_web::web::scope("/auth")
.service(flows::init)
.service(flows::auth_callback)
.service(flows::delete_auth_provider)
.service(flows::create_account_with_password)
.service(flows::login_password)
.service(flows::login_2fa)
.service(flows::begin_2fa_flow)
.service(flows::finish_2fa_flow)
.service(flows::remove_2fa)
.service(flows::reset_password_begin)
.service(flows::change_password)
.service(flows::resend_verify_email)
.service(flows::set_email)
.service(flows::verify_email)
.service(flows::subscribe_newsletter)
.service(flows::get_newsletter_subscription_status),
);
cfg.service(pats::get_pats);
cfg.service(pats::create_pat);
cfg.service(pats::edit_pat);
cfg.service(pats::delete_pat);
})
.configure(oauth_clients::config)
.configure(session::config)
.configure(flows::config)
.configure(pats::config)
.configure(billing::config)
.configure(gdpr::config)
.configure(gotenberg::config)

View File

@@ -22,14 +22,23 @@ use crate::util::validate::validation_errors_to_string;
use serde::Deserialize;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(get_pats);
cfg.service(create_pat);
cfg.service(edit_pat);
cfg.service(delete_pat);
}
#[get("pat")]
#[utoipa::path(
get,
operation_id = "getPats",
responses(
(status = 200, description = "List of PATs"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_READ"]))
)]
#[get("/pat")]
pub async fn get_pats(
req: HttpRequest,
pool: Data<PgPool>,
@@ -65,7 +74,7 @@ pub async fn get_pats(
))
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct NewPersonalAccessToken {
pub scopes: Scopes,
#[validate(length(min = 3, max = 255))]
@@ -73,7 +82,17 @@ pub struct NewPersonalAccessToken {
pub expires: DateTime<Utc>,
}
#[post("pat")]
#[utoipa::path(
post,
operation_id = "createPat",
responses(
(status = 200, description = "PAT created"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_CREATE"]))
)]
#[post("/pat")]
pub async fn create_pat(
req: HttpRequest,
info: web::Json<NewPersonalAccessToken>,
@@ -158,7 +177,7 @@ pub async fn create_pat(
}))
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct ModifyPersonalAccessToken {
pub scopes: Option<Scopes>,
#[validate(length(min = 3, max = 255))]
@@ -166,7 +185,18 @@ pub struct ModifyPersonalAccessToken {
pub expires: Option<DateTime<Utc>>,
}
#[patch("pat/{id}")]
#[utoipa::path(
patch,
operation_id = "editPat",
params(("id" = String, Path, description = "The PAT ID")),
responses(
(status = 204, description = "PAT updated"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_WRITE"]))
)]
#[patch("/pat/{id}")]
pub async fn edit_pat(
req: HttpRequest,
id: web::Path<(String,)>,
@@ -263,7 +293,17 @@ pub async fn edit_pat(
Ok(HttpResponse::NoContent().finish())
}
#[delete("pat/{id}")]
#[utoipa::path(
delete,
operation_id = "deletePat",
params(("id" = String, Path, description = "The PAT ID")),
responses(
(status = 204, description = "PAT deleted"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_DELETE"]))
)]
#[delete("/pat/{id}")]
pub async fn delete_pat(
req: HttpRequest,
id: web::Path<(String,)>,

View File

@@ -11,7 +11,7 @@ use crate::models::sessions::Session;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use actix_web::http::header::AUTHORIZATION;
use actix_web::web::{Data, ServiceConfig, scope};
use actix_web::web::Data;
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use chrono::{DateTime, Utc};
use rand::distributions::Alphanumeric;
@@ -19,9 +19,9 @@ use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use woothee::parser::Parser;
pub fn config(cfg: &mut ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
scope("session")
utoipa_actix_web::scope("/session")
.service(list)
.service(delete)
.service(refresh),
@@ -133,7 +133,16 @@ pub async fn issue_session(
Ok(session)
}
#[get("list")]
#[utoipa::path(
get,
operation_id = "listSessions",
responses(
(status = 200, description = "List of active sessions"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["SESSION_READ"]))
)]
#[get("/list")]
pub async fn list(
req: HttpRequest,
pool: Data<PgPool>,
@@ -169,7 +178,17 @@ pub async fn list(
Ok(HttpResponse::Ok().json(sessions))
}
#[delete("{id}")]
#[utoipa::path(
delete,
operation_id = "deleteSession",
params(("id" = String, Path, description = "The session ID")),
responses(
(status = 204, description = "Session deleted"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["SESSION_DELETE"]))
)]
#[delete("/{id}")]
pub async fn delete(
info: web::Path<(String,)>,
req: HttpRequest,
@@ -209,7 +228,15 @@ pub async fn delete(
Ok(HttpResponse::NoContent().body(""))
}
#[post("refresh")]
#[utoipa::path(
post,
operation_id = "refreshSession",
responses(
(status = 200, description = "Session refreshed"),
(status = 401, description = "Unauthorized")
)
)]
#[post("/refresh")]
pub async fn refresh(
req: HttpRequest,
pool: Data<PgPool>,

View File

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

View File

@@ -15,12 +15,13 @@ mod versions;
pub use super::ApiError;
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(
actix_web::web::scope("v2")
utoipa_actix_web::scope("/v2")
.wrap(default_cors())
.configure(super::internal::admin::config)
// Todo: separate these- they need to also follow v2-v3 conversion
.configure(super::internal::session::config)
.configure(super::internal::flows::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 serde::Deserialize;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("moderation").service(get_projects));
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(utoipa_actix_web::scope("/moderation").service(get_projects));
}
#[derive(Deserialize)]
@@ -22,7 +22,31 @@ fn default_count() -> u16 {
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(
req: HttpRequest,
pool: web::Data<PgPool>,

View File

@@ -10,13 +10,12 @@ use crate::routes::v3;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
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_delete);
cfg.service(notifications_read);
cfg.service(
web::scope("notification")
utoipa_actix_web::scope("/notification")
.service(notification_get)
.service(notification_read)
.service(notification_delete),
@@ -28,7 +27,31 @@ pub struct NotificationIds {
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(
req: HttpRequest,
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(
req: HttpRequest,
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(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
@@ -97,7 +156,25 @@ pub async fn notification_read(
.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(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
@@ -117,7 +194,31 @@ pub async fn notification_delete(
.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(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
@@ -137,7 +238,31 @@ pub async fn notifications_read(
.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(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,

View File

@@ -25,7 +25,7 @@ use validator::Validate;
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);
}
@@ -134,6 +134,24 @@ struct ProjectCreateData {
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")]
pub async fn project_create(
req: HttpRequest,

View File

@@ -21,14 +21,13 @@ use std::collections::HashMap;
use std::sync::Arc;
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(projects_get);
cfg.service(projects_edit);
cfg.service(random_projects_get);
cfg.service(
web::scope("project")
utoipa_actix_web::scope("/project")
.service(project_get)
.service(project_get_check)
.service(project_delete)
@@ -42,7 +41,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(project_unfollow)
.service(super::teams::team_members_get_project)
.service(
web::scope("{project_id}")
utoipa_actix_web::scope("/{project_id}")
.service(super::versions::version_list)
.service(super::versions::version_project_get)
.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(
web::Query(info): web::Query<SearchRequest>,
search_backend: web::Data<dyn SearchBackend>,
@@ -141,13 +176,29 @@ fn parse_facet(facet: &str) -> Option<(String, String, String)> {
None
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct RandomProjects {
#[validate(range(min = 1, max = 100))]
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(
web::Query(count): web::Query<RandomProjects>,
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(
req: HttpRequest,
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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -241,7 +318,20 @@ pub async fn project_get(
}
//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(
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -253,13 +343,26 @@ pub async fn project_get_check(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize)]
#[derive(Serialize, utoipa::ToSchema)]
struct DependencyInfo {
pub projects: Vec<LegacyProject>,
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(
req: HttpRequest,
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 {
#[validate(
length(min = 3, max = 64),
@@ -404,7 +507,26 @@ pub struct EditProject {
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)]
pub async fn project_edit(
req: HttpRequest,
@@ -579,7 +701,7 @@ pub async fn project_edit(
Ok(response)
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct BulkEditProject {
#[validate(length(max = 3))]
pub categories: Option<Vec<String>>,
@@ -642,7 +764,29 @@ pub struct BulkEditProject {
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(
req: HttpRequest,
web::Query(ids): web::Query<ProjectIds>,
@@ -739,12 +883,40 @@ pub async fn projects_edit(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Extension {
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)]
pub async fn project_icon_edit(
web::Query(ext): web::Query<Extension>,
@@ -771,7 +943,22 @@ pub async fn project_icon_edit(
.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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -793,7 +980,7 @@ pub async fn delete_project_icon(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize, Validate)]
#[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct GalleryCreateQuery {
pub featured: bool,
#[validate(length(min = 1, max = 255))]
@@ -803,7 +990,63 @@ pub struct GalleryCreateQuery {
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)]
pub async fn add_gallery_item(
web::Query(ext): web::Query<Extension>,
@@ -837,7 +1080,7 @@ pub async fn add_gallery_item(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize, Validate)]
#[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct GalleryEditQuery {
/// The url of the gallery item to edit
pub url: String,
@@ -859,7 +1102,48 @@ pub struct GalleryEditQuery {
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(
req: HttpRequest,
web::Query(item): web::Query<GalleryEditQuery>,
@@ -885,12 +1169,30 @@ pub async fn edit_gallery_item(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct GalleryDeleteQuery {
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(
req: HttpRequest,
web::Query(item): web::Query<GalleryDeleteQuery>,
@@ -912,7 +1214,22 @@ pub async fn delete_gallery_item(
.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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -935,7 +1252,22 @@ pub async fn project_delete(
.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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -949,7 +1281,22 @@ pub async fn project_follow(
.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(
req: HttpRequest,
info: web::Path<(String,)>,

View File

@@ -8,7 +8,7 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use serde::Deserialize;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(reports_get);
cfg.service(reports);
cfg.service(report_create);
@@ -17,7 +17,21 @@ pub fn config(cfg: &mut web::ServiceConfig) {
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(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -40,7 +54,7 @@ pub async fn report_create(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ReportsRequestOptions {
#[serde(default = "default_count")]
count: u16,
@@ -55,7 +69,31 @@ fn default_all() -> bool {
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(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -88,12 +126,36 @@ pub async fn reports(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ReportIds {
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(
req: HttpRequest,
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(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -145,14 +225,34 @@ pub async fn report_get(
}
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct EditReport {
#[validate(length(max = 65536))]
pub body: Option<String>,
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(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -178,7 +278,25 @@ pub async fn report_edit(
.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(
req: HttpRequest,
pool: web::Data<PgPool>,

View File

@@ -5,11 +5,11 @@ use crate::routes::{
};
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);
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, utoipa::ToSchema)]
pub struct V2Stats {
pub projects: Option<i64>,
pub versions: Option<i64>,
@@ -17,7 +17,19 @@ pub struct V2Stats {
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(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {

View File

@@ -12,9 +12,9 @@ use actix_web::{HttpResponse, get, web};
use chrono::{DateTime, Utc};
use itertools::Itertools;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
web::scope("tag")
utoipa_actix_web::scope("/tag")
.service(category_list)
.service(loader_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 icon: String,
pub name: String,
@@ -35,7 +35,19 @@ pub struct CategoryData {
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(
pool: web::Data<PgPool>,
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 icon: String,
pub name: 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(
pool: web::Data<PgPool>,
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 version: String,
pub version_type: String,
@@ -124,14 +148,38 @@ pub struct GameVersionQueryData {
pub major: bool,
}
#[derive(serde::Deserialize)]
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct GameVersionQuery {
#[serde(rename = "type")]
type_: Option<String>,
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(
pool: web::Data<PgPool>,
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 short: 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 {
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 title: 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(
params: web::Path<(String,)>,
) -> 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 {
// 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)
@@ -249,7 +325,19 @@ pub struct DonationPlatformQueryData {
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(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -295,7 +383,19 @@ pub async fn donation_platform_list(
.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(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -306,7 +406,19 @@ pub async fn report_type_list(
.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(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -317,7 +429,19 @@ pub async fn project_type_list(
.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> {
// 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.

View File

@@ -12,11 +12,10 @@ use ariadne::ids::UserId;
use rust_decimal::Decimal;
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(
web::scope("team")
utoipa_actix_web::scope("/team")
.service(team_members_get)
.service(edit_team_member)
.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
// (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
#[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(
req: HttpRequest,
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)
#[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(
req: HttpRequest,
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 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(
req: HttpRequest,
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(
req: HttpRequest,
info: web::Path<(TeamId,)>,
@@ -149,7 +194,7 @@ fn default_ordering() -> i64 {
0
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct NewTeamMember {
pub user_id: UserId,
#[serde(default = "default_role")]
@@ -165,7 +210,26 @@ pub struct NewTeamMember {
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(
req: HttpRequest,
info: web::Path<(TeamId,)>,
@@ -194,7 +258,7 @@ pub async fn add_team_member(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct EditTeamMember {
pub permissions: Option<ProjectPermissions>,
pub organization_permissions: Option<OrganizationPermissions>,
@@ -203,7 +267,33 @@ pub struct EditTeamMember {
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(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
@@ -231,12 +321,31 @@ pub async fn edit_team_member(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct TransferOwnership {
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(
req: HttpRequest,
info: web::Path<(TeamId,)>,
@@ -260,7 +369,32 @@ pub async fn transfer_ownership(
.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(
req: HttpRequest,
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 serde::Deserialize;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
web::scope("thread")
utoipa_actix_web::scope("/thread")
.service(thread_get)
.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);
}
#[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(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
@@ -34,12 +48,26 @@ pub async fn thread_get(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ThreadIds {
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(
req: HttpRequest,
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 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(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
@@ -100,7 +144,25 @@ pub async fn thread_send_message(
.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(
req: HttpRequest,
info: web::Path<(ThreadMessageId,)>,

View File

@@ -14,12 +14,11 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
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(users_get);
cfg.service(
web::scope("user")
utoipa_actix_web::scope("/user")
.service(user_get)
.service(projects_list)
.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(
req: HttpRequest,
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 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(
web::Query(ids): web::Query<UserIds>,
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(
req: HttpRequest,
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(
req: HttpRequest,
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 {
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_USERNAME))]
pub username: Option<String>,
@@ -156,7 +201,26 @@ pub struct EditUser {
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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -186,12 +250,44 @@ pub async fn user_edit(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Extension {
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)]
pub async fn user_icon_edit(
web::Query(ext): web::Query<Extension>,
@@ -218,7 +314,22 @@ pub async fn user_icon_edit(
.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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -240,7 +351,25 @@ pub async fn user_icon_delete(
.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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -255,7 +384,25 @@ pub async fn user_delete(
.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(
req: HttpRequest,
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(
req: HttpRequest,
info: web::Path<(String,)>,

View File

@@ -75,7 +75,25 @@ pub struct InitialVersionData {
}
// 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(
req: HttpRequest,
payload: Multipart,
@@ -280,7 +298,29 @@ async fn get_example_version_fields(
}
// 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(
req: HttpRequest,
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 std::collections::HashMap;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(
web::scope("version_file")
utoipa_actix_web::scope("/version_file")
.service(delete_file)
.service(get_version_from_hash)
.service(download_version)
@@ -22,7 +22,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
);
cfg.service(
web::scope("version_files")
utoipa_actix_web::scope("/version_files")
.service(get_versions_from_hashes)
.service(update_files)
.service(update_files_many)
@@ -31,7 +31,36 @@ pub fn config(cfg: &mut web::ServiceConfig) {
}
// 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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -62,7 +91,36 @@ pub async fn get_version_from_hash(
}
// 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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -85,7 +143,41 @@ pub async fn download_version(
}
// 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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -107,14 +199,45 @@ pub async fn delete_file(
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct UpdateData {
pub loaders: Option<Vec<String>>,
pub game_versions: Option<Vec<String>>,
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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -162,13 +285,23 @@ pub async fn get_update_from_hash(
}
// Requests above with multiple versions below
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct FileHashes {
pub algorithm: Option<String>,
pub hashes: Vec<String>,
}
// 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("")]
pub async fn get_versions_from_hashes(
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(
req: HttpRequest,
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 algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<String>,
@@ -277,7 +420,17 @@ pub struct ManyUpdateData {
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(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
@@ -316,7 +469,17 @@ pub async fn update_files(
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(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
@@ -358,7 +521,7 @@ pub async fn update_files_many(
Ok(HttpResponse::Ok().json(v3_versions))
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct FileUpdateData {
pub hash: String,
pub loaders: Option<Vec<String>>,
@@ -366,13 +529,23 @@ pub struct FileUpdateData {
pub version_types: Option<Vec<VersionType>>,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ManyFileUpdateData {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
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(
req: HttpRequest,
pool: web::Data<PgPool>,

View File

@@ -16,12 +16,11 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::{Deserialize, Serialize};
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(super::version_creation::version_create);
cfg.service(
web::scope("version")
utoipa_actix_web::scope("/version")
.service(version_get)
.service(version_delete)
.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 game_versions: Option<String>,
pub loaders: Option<String>,
@@ -45,7 +44,46 @@ fn default_true() -> bool {
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(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -129,7 +167,31 @@ pub async fn version_list(
}
// 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(
req: HttpRequest,
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 ids: String,
#[serde(default = "default_true")]
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(
req: HttpRequest,
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(
req: HttpRequest,
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 {
#[validate(
length(min = 1, max = 64),
@@ -251,14 +333,33 @@ pub struct EditVersion {
pub file_types: Option<Vec<EditVersionFileType>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct EditVersionFileType {
pub algorithm: String,
pub hash: String,
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(
req: HttpRequest,
info: web::Path<(VersionId,)>,
@@ -350,7 +451,25 @@ pub async fn version_edit(
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(
req: HttpRequest,
info: web::Path<(VersionId,)>,