feat: better api docs (#6586)

* feat: docs

* fix tombi

* chore: fix some of the routes rendering with missing /

* response schemas

* fix: restore labrinth docs routes

* Fix path parameter docs in routes

* remove utoipa-actix-web

* consistency

* improve version intros

* improve formatting, examples

* better hash examples, move openapi stuff to openapi.rs

* more utoipa param fixes

* request body docs

* chore: remove moderation route from v2, remove ingest & webhooks from v3 spec

* fixes

* chore: tweak sources titles

* fix

* fix test

* improve examples

* increase compiler spawned thread stack size

* remove unused tests & script

* test

* bro what

* fix

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
This commit is contained in:
François-Xavier Talbot
2026-07-04 14:19:21 +00:00
committed by GitHub
co-authored by aecsocket
parent b26d048a63
commit 4a6fa9fc3d
103 changed files with 4546 additions and 1387 deletions
+11 -2
View File
@@ -23,9 +23,9 @@ use std::str::FromStr;
use std::sync::Arc;
use tracing::trace;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
utoipa_actix_web::scope("/admin")
web::scope("/admin")
.service(count_download)
.service(force_reindex)
.service(force_reindex_project),
@@ -130,7 +130,10 @@ async fn resolve_download_attribution_version(
}
// This is an internal route, cannot be used without key
/// Count a download.
#[utoipa::path(
context_path = "/admin",
tag = "v2 admin",
patch,
operation_id = "countDownload",
responses(
@@ -308,7 +311,10 @@ pub async fn count_download(
Ok(HttpResponse::NoContent().body(""))
}
/// Reindex all projects.
#[utoipa::path(
context_path = "/admin",
tag = "v2 admin",
post,
operation_id = "forceReindex",
responses(
@@ -330,7 +336,10 @@ pub async fn force_reindex(
Ok(HttpResponse::NoContent().finish())
}
/// Reindex a project.
#[utoipa::path(
context_path = "/admin",
tag = "v2 admin",
post,
operation_id = "forceReindexProject",
responses(
+34 -10
View File
@@ -25,7 +25,7 @@ use url::Url;
use crate::routes::ApiError;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(ingest_click)
.service(get_all)
.service(create)
@@ -40,9 +40,14 @@ pub struct IngestClick {
pub affiliate_code_id: AffiliateCodeId,
}
#[utoipa::path]
/// Ingest an affiliate click.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = NO_CONTENT))
)]
#[post("/ingest-click")]
async fn ingest_click(
pub async fn ingest_click(
req: HttpRequest,
web::Json(ingest_click): web::Json<IngestClick>,
pool: web::Data<PgPool>,
@@ -136,11 +141,14 @@ async fn ingest_click(
Ok(())
}
/// List affiliate codes.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = OK, body = inline(Vec<AffiliateCode>)))
)]
#[get("")]
async fn get_all(
pub async fn get_all(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -187,11 +195,14 @@ pub struct CreateRequest {
pub source_name: String,
}
/// Create an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = OK, body = inline(AffiliateCode)))
)]
#[put("")]
async fn create(
pub async fn create(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -263,11 +274,14 @@ async fn create(
Ok(web::Json(AffiliateCode::from(code, is_admin)))
}
/// Get an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = OK, body = inline(AffiliateCode)))
)]
#[get("/{id}")]
async fn get(
pub async fn get(
req: HttpRequest,
path: web::Path<(AffiliateCodeId,)>,
pool: web::Data<PgPool>,
@@ -302,9 +316,14 @@ async fn get(
}
}
#[utoipa::path]
/// Delete an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
async fn delete(
pub async fn delete(
req: HttpRequest,
path: web::Path<(AffiliateCodeId,)>,
pool: web::Data<PgPool>,
@@ -350,9 +369,14 @@ pub struct PatchRequest {
pub source_name: String,
}
#[utoipa::path]
/// Update an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = NO_CONTENT))
)]
#[patch("/{id}")]
async fn patch(
pub async fn patch(
req: HttpRequest,
path: web::Path<(AffiliateCodeId,)>,
pool: web::Data<PgPool>,
@@ -28,7 +28,7 @@ use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::error::Context;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(list)
.service(update_group)
.service(scan)
@@ -37,7 +37,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
.service(split);
}
#[derive(Serialize)]
#[derive(Serialize, utoipa::ToSchema)]
struct AttributionGroupResponse {
id: crate::models::ids::AttributionGroupId,
flame_project: Option<FlameProject>,
@@ -48,7 +48,7 @@ struct AttributionGroupResponse {
versions: Vec<VersionInfo>,
}
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, utoipa::ToSchema)]
struct VersionInfo {
id: VersionId,
name: String,
@@ -56,7 +56,7 @@ struct VersionInfo {
date_created: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize)]
#[derive(Serialize, utoipa::ToSchema)]
struct AttributionFileResponse {
name: String,
sha1: String,
@@ -67,7 +67,7 @@ struct AttributionFileResponse {
moderation_external_license: Option<ModerationExternalLicenseResponse>,
}
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, utoipa::ToSchema)]
struct ModerationExternalLicenseResponse {
id: i64,
title: Option<String>,
@@ -92,9 +92,14 @@ struct ScanResponse {
queued_files: u64,
}
#[utoipa::path]
/// Queue an attribution scan.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = OK, body = ScanResponse))
)]
#[post("/scan")]
async fn scan(
pub async fn scan(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -278,9 +283,17 @@ async fn force_scan_file(
Ok(web::Json(scan_summary))
}
#[utoipa::path]
/// List project attribution groups.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
params(
("project_id" = ProjectId, Path)
),
responses((status = OK, body = inline(Vec<AttributionGroupResponse>)))
)]
#[get("/{project_id}")]
async fn list(
pub async fn list(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -528,9 +541,14 @@ struct UpdateGroupBody {
attribution: AttributionResolution,
}
#[utoipa::path]
/// Update an attribution group.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = NO_CONTENT))
)]
#[patch("/group/{group_id}")]
async fn update_group(
pub async fn update_group(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -609,9 +627,14 @@ struct AssignBody {
project_id: ProjectId,
}
#[utoipa::path]
/// Move a file to an attribution group.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = NO_CONTENT))
)]
#[post("/assign")]
async fn assign(
pub async fn assign(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -765,9 +788,14 @@ struct SplitBody {
project_id: ProjectId,
}
#[utoipa::path]
/// Split a file into a new attribution group.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = NO_CONTENT))
)]
#[post("/split")]
async fn split(
pub async fn split(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+145 -38
View File
@@ -42,7 +42,7 @@ use stripe::{
};
use tracing::warn;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
web::scope("/billing")
.service(products)
@@ -58,11 +58,18 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(active_servers)
.service(initiate_payment)
.service(stripe_webhook)
.service(refund_charge),
.service(refund_charge)
.service(reprocess_charge_tax),
);
}
#[get("products")]
/// List products.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[get("/products")]
pub async fn products(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -99,7 +106,14 @@ struct SubscriptionsQuery {
pub user_id: Option<ariadne::ids::UserId>,
}
#[get("subscriptions")]
/// List subscriptions.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("user_id" = Option<ariadne::ids::UserId>, Query)),
responses((status = OK, body = serde_json::Value))
)]
#[get("/subscriptions")]
pub async fn subscriptions(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -141,7 +155,7 @@ pub async fn subscriptions(
Ok(HttpResponse::Ok().json(subscriptions))
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChargeRefundAmount {
Full,
@@ -149,14 +163,21 @@ pub enum ChargeRefundAmount {
None,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ChargeRefund {
#[serde(flatten)]
pub amount: ChargeRefundAmount,
pub unprovision: Option<bool>,
}
#[post("charge/{id}/refund")]
/// Refund a charge.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
request_body = ChargeRefund,
responses((status = NO_CONTENT))
)]
#[post("/charge/{id}/refund")]
#[allow(clippy::too_many_arguments)]
pub async fn refund_charge(
req: HttpRequest,
@@ -419,7 +440,13 @@ pub async fn refund_charge(
Ok(HttpResponse::NoContent().finish())
}
#[post("charge/{id}/tax/reprocess")]
/// Reprocess tax for a charge.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[post("/charge/{id}/tax/reprocess")]
pub async fn reprocess_charge_tax(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -587,8 +614,9 @@ pub async fn reprocess_charge_tax(
Ok(HttpResponse::NoContent().finish())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct SubscriptionEdit {
#[schema(value_type = String)]
pub interval: Option<PriceDuration>,
pub payment_method: Option<String>,
pub cancelled: Option<bool>,
@@ -601,7 +629,17 @@ pub struct SubscriptionEditQuery {
pub dry: Option<bool>,
}
#[patch("subscription/{id}")]
/// Update a subscription.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("dry" = Option<bool>, Query)),
responses(
(status = OK, body = serde_json::Value),
(status = NO_CONTENT),
)
)]
#[patch("/subscription/{id}")]
#[allow(clippy::too_many_arguments)]
pub async fn edit_subscription(
req: HttpRequest,
@@ -1091,7 +1129,13 @@ pub async fn edit_subscription(
}
}
#[get("customer")]
/// Get the current customer.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[get("/customer")]
pub async fn user_customer(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1129,7 +1173,14 @@ pub struct ChargesQuery {
pub user_id: Option<ariadne::ids::UserId>,
}
#[get("payments")]
/// List payments.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("user_id" = Option<ariadne::ids::UserId>, Query)),
responses((status = OK, body = serde_json::Value))
)]
#[get("/payments")]
pub async fn charges(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1188,7 +1239,13 @@ pub async fn charges(
))
}
#[post("payment_method")]
/// Start a payment method flow.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[post("/payment_method")]
pub async fn add_payment_method_flow(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1241,7 +1298,13 @@ pub struct EditPaymentMethod {
pub primary: bool,
}
#[patch("payment_method/{id}")]
/// Update a payment method.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[patch("/payment_method/{id}")]
pub async fn edit_payment_method(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1305,7 +1368,13 @@ pub async fn edit_payment_method(
}
}
#[delete("payment_method/{id}")]
/// Remove a payment method.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[delete("/payment_method/{id}")]
pub async fn remove_payment_method(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1388,7 +1457,16 @@ pub async fn remove_payment_method(
}
}
#[get("payment_methods")]
/// List payment methods.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses(
(status = OK, body = serde_json::Value),
(status = NO_CONTENT),
)
)]
#[get("/payment_methods")]
pub async fn payment_methods(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1432,7 +1510,24 @@ pub struct ActiveServersQuery {
pub subscription_status: Option<SubscriptionStatus>,
}
#[get("active_servers")]
#[derive(Serialize, utoipa::ToSchema)]
struct ActiveServerResponse {
pub user_id: ariadne::ids::UserId,
pub server_id: String,
pub price_id: crate::models::ids::ProductPriceId,
#[schema(value_type = String)]
pub interval: PriceDuration,
pub region: Option<String>,
}
/// List active servers.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("subscription_status" = Option<String>, Query)),
responses((status = OK, body = inline(Vec<ActiveServerResponse>)))
)]
#[get("/active_servers")]
pub async fn active_servers(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1457,21 +1552,12 @@ pub async fn active_servers(
)
.await?;
#[derive(Serialize)]
struct ActiveServer {
pub user_id: ariadne::ids::UserId,
pub server_id: String,
pub price_id: crate::models::ids::ProductPriceId,
pub interval: PriceDuration,
pub region: Option<String>,
}
let server_ids = servers
.into_iter()
.filter_map(|x| {
x.metadata.as_ref().and_then(|metadata| match metadata {
SubscriptionMetadata::Pyro { id, region } => {
Some(ActiveServer {
Some(ActiveServerResponse {
user_id: x.user_id.into(),
server_id: id.clone(),
price_id: x.price_id.into(),
@@ -1482,12 +1568,12 @@ pub async fn active_servers(
SubscriptionMetadata::Medal { .. } => None,
})
})
.collect::<Vec<ActiveServer>>();
.collect::<Vec<ActiveServerResponse>>();
Ok(HttpResponse::Ok().json(server_ids))
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PaymentRequestType {
PaymentMethod { id: String },
@@ -1507,7 +1593,7 @@ impl PaymentRequestType {
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChargeRequestType {
Existing {
@@ -1515,11 +1601,12 @@ pub enum ChargeRequestType {
},
New {
product_id: crate::models::ids::ProductId,
#[schema(value_type = String)]
interval: Option<PriceDuration>,
},
}
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct PaymentRequestMetadata {
#[serde(flatten)]
@@ -1527,7 +1614,7 @@ pub struct PaymentRequestMetadata {
pub affiliate_code: Option<AffiliateCodeId>,
}
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PaymentRequestMetadataKind {
Pyro {
@@ -1537,16 +1624,23 @@ pub enum PaymentRequestMetadataKind {
},
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct PaymentRequest {
#[serde(flatten)]
pub type_: PaymentRequestType,
pub charge: ChargeRequestType,
#[schema(value_type = String)]
pub existing_payment_intent: Option<stripe::PaymentIntentId>,
pub metadata: Option<PaymentRequestMetadata>,
}
#[post("payment")]
/// Initiate a payment.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[post("/payment")]
pub async fn initiate_payment(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1610,7 +1704,14 @@ pub async fn initiate_payment(
}
}
#[post("_stripe")]
/// Receive a Stripe webhook.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
request_body(content = String, content_type = "text/plain"),
responses((status = NO_CONTENT))
)]
#[post("/_stripe")]
pub async fn stripe_webhook(
req: HttpRequest,
payload: String,
@@ -2503,7 +2604,7 @@ async fn apply_credit_many(
Ok(())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct CreditRequest {
#[serde(flatten)]
pub target: CreditTarget,
@@ -2512,7 +2613,7 @@ pub struct CreditRequest {
pub message: String,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(untagged)]
pub enum CreditTarget {
Subscriptions {
@@ -2526,7 +2627,13 @@ pub enum CreditTarget {
},
}
#[post("credit")]
/// Credit subscriptions.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[post("/credit")]
pub async fn credit(
req: HttpRequest,
pool: web::Data<PgPool>,
+14 -3
View File
@@ -27,7 +27,7 @@ use crate::{
util::{error::Context, http::HttpClient, tiltify::TiltifyClient},
};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(tiltify_webhook).service(pride_26);
}
@@ -143,7 +143,13 @@ impl CampaignDonation {
}
}
#[utoipa::path]
/// Receive a Tiltify webhook.
#[utoipa::path(
context_path = "/campaign",
tag = "campaigns",
request_body(content = String, content_type = "text/plain"),
responses((status = NO_CONTENT))
)]
#[post("/webhook")]
pub async fn tiltify_webhook(
req: HttpRequest,
@@ -301,7 +307,12 @@ fn verify_tiltify_webhook_signature(
Ok(())
}
#[utoipa::path]
/// Get Pride campaign data.
#[utoipa::path(
context_path = "/campaign",
tag = "campaigns",
responses((status = OK, body = CampaignInfo))
)]
#[get("/pride-26")]
pub async fn pride_26(
http: web::Data<HttpClient>,
@@ -35,7 +35,7 @@ use crate::{
pub mod rescan;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
web::scope("/delphi")
.service(ingest_report)
@@ -141,8 +141,14 @@ pub struct DelphiRunParameters {
pub file_id: crate::models::ids::FileId,
}
#[post("ingest", guard = "admin_key_guard")]
async fn ingest_report(
/// Ingest a Delphi report.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
responses((status = NO_CONTENT))
)]
#[post("/ingest", guard = "admin_key_guard")]
pub async fn ingest_report(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
web::Json(report): web::Json<serde_json::Value>,
@@ -466,8 +472,15 @@ pub async fn send_tech_review_exit_file_deleted_message_if_exited(
Ok(())
}
#[post("run")]
async fn _run(
/// Run Delphi.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
params(("file_id" = crate::models::ids::FileId, Query)),
responses((status = NO_CONTENT))
)]
#[post("/run")]
pub async fn _run(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -487,8 +500,14 @@ async fn _run(
run(&**pool, run_parameters.into_inner(), &http).await
}
#[get("version")]
async fn version(
/// Get the Delphi version.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
responses((status = OK, body = inline(Option<i32>)))
)]
#[get("/version")]
pub async fn version(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -510,8 +529,14 @@ async fn version(
))
}
#[get("issue_type/schema")]
async fn issue_type_schema(
/// Get the Delphi issue type schema.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
responses((status = OK, body = serde_json::Value))
)]
#[get("/issue_type/schema")]
pub async fn issue_type_schema(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -28,14 +28,14 @@ use eyre::eyre;
use lettre::message::Mailbox;
use serde::{Deserialize, Serialize};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(create)
.service(create_email_sync)
.service(remove)
.service(send_custom_email);
}
#[derive(Deserialize, PartialEq, Default)]
#[derive(Deserialize, PartialEq, Default, utoipa::ToSchema)]
enum EmailStrategy {
#[default]
Async,
@@ -43,8 +43,9 @@ enum EmailStrategy {
None,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
struct CreateNotification {
#[schema(value_type = serde_json::Value)]
pub body: NotificationBody,
pub user_ids: Vec<UserId>,
#[serde(default)]
@@ -78,7 +79,12 @@ where
error.as_api_error().serialize(serializer)
}
#[post("external_notifications", guard = "external_notification_key_guard")]
/// Create external notifications.
#[utoipa::path(
tag = "external notifications",
responses((status = ACCEPTED))
)]
#[post("/external_notifications", guard = "external_notification_key_guard")]
pub async fn create(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -89,6 +95,46 @@ pub async fn create(
.await
}
/// Create notifications and send emails.
///
/// Responds with the user IDs that could not be emailed:
/// - `200` if every recipient was emailed (empty list)
/// - `207` if some recipients could not be emailed (list of failed IDs)
/// Create email sync.
#[utoipa::path(
tag = "external notifications",
responses(
(status = OK, body = inline(Vec<UserId>)),
(status = 207, body = inline(Vec<UserId>)),
)
)]
#[post(
"external_notifications/email-sync",
guard = "external_notification_key_guard"
)]
pub async fn create_email_sync(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
email_queue: web::Data<EmailQueue>,
data: web::Json<CreateNotification>,
) -> Result<(web::Json<Vec<UserId>>, StatusCode), ApiError> {
let data = data.into_inner();
create_impl(
pool,
redis,
email_queue,
CreateNotification {
body: data.body,
user_ids: data.user_ids,
email: EmailStrategy::Sync,
},
)
.await
.map(|(res, code)| {
(web::Json(res.into_inner().into_keys().collect()), code)
})
}
async fn create_impl(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -224,44 +270,19 @@ async fn create_impl(
Ok((web::Json(HashMap::new()), StatusCode::ACCEPTED))
}
/// Inserts notifications for all users and tries to send emails immediately.
///
/// Responds with the user IDs that could not be emailed and a reason why
#[post(
"external_notifications/email-sync",
guard = "external_notification_key_guard"
)]
pub async fn create_email_sync(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
email_queue: web::Data<EmailQueue>,
data: web::Json<CreateNotification>,
) -> Result<(web::Json<Vec<UserId>>, StatusCode), ApiError> {
let data = data.into_inner();
create_impl(
pool,
redis,
email_queue,
CreateNotification {
body: data.body,
user_ids: data.user_ids,
email: EmailStrategy::Sync,
},
)
.await
.map(|(res, code)| {
(web::Json(res.into_inner().into_keys().collect()), code)
})
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
struct NotificationFilter {
pub user_ids: Vec<UserId>,
#[serde(flatten)]
pub body: serde_json::Map<String, serde_json::Value>,
}
#[delete("external_notifications", guard = "external_notification_key_guard")]
/// Remove external notifications.
#[utoipa::path(
tag = "external notifications",
responses((status = NO_CONTENT))
)]
#[delete("/external_notifications", guard = "external_notification_key_guard")]
pub async fn remove(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -301,7 +322,7 @@ pub async fn remove(
Ok(HttpResponse::NoContent().finish())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
struct SendEmail {
pub users: Vec<UserId>,
pub key: String,
@@ -309,7 +330,12 @@ struct SendEmail {
pub title: String,
}
#[post("external_notifications/send_custom_email")]
/// Send a custom email.
#[utoipa::path(
tag = "external notifications",
responses((status = ACCEPTED))
)]
#[post("/external_notifications/send_custom_email")]
pub async fn send_custom_email(
req: HttpRequest,
pool: web::Data<PgPool>,
+127 -56
View File
@@ -60,9 +60,9 @@ use webauthn_rs::prelude::{
};
use zxcvbn::Score;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
utoipa_actix_web::scope("/auth")
web::scope("/auth")
.service(init)
.service(auth_callback)
.service(delete_auth_provider)
@@ -1068,12 +1068,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
#[utoipa::path(
get,
operation_id = "authInit",
responses(
(status = 307, description = "Redirect to OAuth provider"),
(status = 400, description = "Invalid input")
)
context_path = "/auth",
tag = "auth",
params(
("url" = Url, Query),
("provider" = Option<AuthProvider>, Query),
("token" = Option<String>, Query),
("auth_token" = Option<String>, Query)
),
responses((status = TEMPORARY_REDIRECT), (status = OK))
)]
#[get("/init")]
pub async fn init(
@@ -1166,12 +1169,8 @@ pub async fn init(
}
#[utoipa::path(
get,
operation_id = "authCallback",
responses(
(status = 307, description = "Redirect with auth code"),
(status = 401, description = "Authentication failed")
)
context_path = "/auth",
tag = "auth", responses((status = OK))
)]
#[get("/callback")]
pub async fn auth_callback(
@@ -1441,16 +1440,19 @@ struct NewOAuthAccount {
pub sign_up_newsletter: bool,
}
/// Create account with OAuth.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "createOAuthAccount",
responses(
(status = 200, description = "OAuth account created"),
(status = 400, description = "Invalid input")
)
operation_id = "createOAuthAccount",
responses(
(status = 200, description = "OAuth account created", body = serde_json::Value),
(status = 400, description = "Invalid input")
)
)]
#[post("/create/oauth")]
async fn create_oauth_account(
pub async fn create_oauth_account(
req: HttpRequest,
db: Data<PgPool>,
file_host: Data<dyn FileHost>,
@@ -1525,7 +1527,10 @@ struct DiscordCommunityHandoffPayload {
nonce: String,
}
/// Link Discord community.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
operation_id = "discordCommunityLink",
responses(
(status = 200, description = "Discord community bot handoff URL", body = DiscordCommunityLinkResponse),
@@ -1599,7 +1604,10 @@ pub async fn discord_community_link(
Ok(web::Json(DiscordCommunityLinkResponse { url }))
}
/// Remove an auth provider.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
delete,
operation_id = "deleteAuthProvider",
responses(
@@ -1939,11 +1947,14 @@ impl ReadyAccountRegisterFlow {
}
}
/// Validate password account creation.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "validateCreateAccountWithPassword",
responses(
(status = 200, description = "Account input is valid"),
(status = NO_CONTENT, description = "Account input is valid"),
(status = 400, description = "Invalid input")
)
)]
@@ -1964,13 +1975,16 @@ pub async fn validate_create_account_with_password(
Ok(())
}
/// Create account with a password.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "createAccountPassword",
responses(
(status = 200, description = "Account created"),
(status = 400, description = "Invalid input")
)
operation_id = "createAccountPassword",
responses(
(status = 200, description = "Account created", body = serde_json::Value),
(status = 400, description = "Invalid input")
)
)]
#[post("/create")]
pub async fn create_account_with_password(
@@ -2010,13 +2024,16 @@ pub struct Login {
pub challenge: String,
}
/// Log in with a password.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "loginPassword",
responses(
(status = 200, description = "Login successful"),
(status = 401, description = "Invalid credentials")
)
operation_id = "loginPassword",
responses(
(status = 200, description = "Login successful", body = serde_json::Value),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login")]
pub async fn login_password(
@@ -2168,13 +2185,16 @@ async fn validate_2fa_code(
}
}
/// Complete login with 2FA.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "login2fa",
responses(
(status = 200, description = "2FA login successful"),
(status = 401, description = "Invalid credentials")
)
operation_id = "login2fa",
responses(
(status = 200, description = "2FA login successful", body = serde_json::Value),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login/2fa")]
pub async fn login_2fa(
@@ -2225,13 +2245,16 @@ pub async fn login_2fa(
}
}
/// Start 2FA setup.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "begin2faFlow",
responses(
(status = 200, description = "2FA secret generated"),
(status = 401, description = "Unauthorized")
),
operation_id = "begin2faFlow",
responses(
(status = 200, description = "2FA secret generated", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa/get_secret")]
@@ -2273,13 +2296,16 @@ pub async fn begin_2fa_flow(
}
}
/// Finish 2FA setup.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "finish2faFlow",
responses(
(status = 200, description = "2FA enabled"),
(status = 401, description = "Unauthorized")
),
operation_id = "finish2faFlow",
responses(
(status = 200, description = "2FA enabled", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa")]
@@ -2405,7 +2431,10 @@ pub struct Remove2FA {
pub code: String,
}
/// Remove 2FA.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
delete,
operation_id = "remove2fa",
responses(
@@ -2502,7 +2531,10 @@ pub struct ResetPassword {
pub challenge: String,
}
/// Start password reset.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "resetPasswordBegin",
responses(
@@ -2605,7 +2637,10 @@ pub struct ChangePassword {
pub new_password: Option<String>,
}
/// Change password.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
patch,
operation_id = "changePassword",
responses(
@@ -2768,7 +2803,10 @@ pub struct SetEmail {
pub email: String,
}
/// Set email address.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
patch,
operation_id = "setEmail",
responses(
@@ -2887,7 +2925,10 @@ pub async fn set_email(
Ok(HttpResponse::Ok().finish())
}
/// Resend verification email.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "resendVerifyEmail",
responses(
@@ -2959,7 +3000,10 @@ pub struct VerifyEmail {
pub flow: String,
}
/// Verify email address.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "verifyEmail",
responses(
@@ -3022,7 +3066,10 @@ pub async fn verify_email(
}
}
/// Subscribe to the newsletter.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "subscribeNewsletter",
responses(
@@ -3068,13 +3115,16 @@ pub async fn subscribe_newsletter(
Ok(HttpResponse::NoContent().finish())
}
/// Get newsletter subscription status.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
get,
operation_id = "getNewsletterSubscriptionStatus",
responses(
(status = 200, description = "Subscription status"),
(status = 401, description = "Unauthorized")
),
operation_id = "getNewsletterSubscriptionStatus",
responses(
(status = 200, description = "Subscription status", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[get("/email/subscribe")]
@@ -3115,7 +3165,10 @@ pub struct RegisterPasskeyResponse {
pub flow: String,
}
/// Start passkey registration.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "registerPasskeyStart",
responses(
@@ -3212,7 +3265,10 @@ pub struct PasskeyResponse {
pub last_used: Option<chrono::DateTime<Utc>>,
}
/// Finish passkey registration.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "registerPasskeyFinish",
responses(
@@ -3318,7 +3374,10 @@ pub struct AuthenticatePasskeyResponse {
pub flow: String,
}
/// Start passkey authentication.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "authenticatePasskeyStart",
responses(
@@ -3358,13 +3417,16 @@ pub struct AuthenticatePasskeyFinish {
pub credential: PublicKeyCredential,
}
/// Finish passkey authentication.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "authenticatePasskeyFinish",
responses(
(status = 200, description = "Passkey authentication successful"),
(status = 400, description = "Invalid input")
)
operation_id = "authenticatePasskeyFinish",
responses(
(status = 200, description = "Passkey authentication successful", body = serde_json::Value),
(status = 400, description = "Invalid input")
)
)]
#[post("/passkey/finish")]
pub async fn authenticate_passkey_finish(
@@ -3472,7 +3534,10 @@ pub async fn authenticate_passkey_finish(
}
}
/// List passkeys.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
get,
operation_id = "listPasskeys",
responses(
@@ -3519,7 +3584,10 @@ pub struct RenamePasskey {
pub name: String,
}
/// Rename a passkey.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
patch,
operation_id = "renamePasskey",
responses(
@@ -3571,7 +3639,10 @@ pub async fn rename_passkey(
Ok(HttpResponse::NoContent().finish())
}
/// Delete a passkey.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
delete,
operation_id = "deletePasskey",
responses(
+7 -1
View File
@@ -6,10 +6,16 @@ use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use actix_web::{HttpRequest, HttpResponse, post, web};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(web::scope("/gdpr").service(export));
}
/// Export GDPR data.
#[utoipa::path(
context_path = "/gdpr",
tag = "GDPR",
responses((status = OK, body = serde_json::Value))
)]
#[post("/export")]
pub async fn export(
req: HttpRequest,
+7 -3
View File
@@ -10,7 +10,7 @@ use chrono::{Datelike, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_globals);
}
@@ -89,8 +89,12 @@ pub fn tax_compliance_payout_threshold_for_year(
value
}
/// Gets configured global non-secret variables for this backend instance.
#[utoipa::path]
/// Get backend globals.
#[utoipa::path(
context_path = "/globals",
tag = "globals",
responses((status = OK, body = Globals))
)]
#[get("")]
pub async fn get_globals() -> web::Json<Globals> {
web::Json(GLOBALS.clone())
+12 -1
View File
@@ -28,6 +28,12 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(success_callback).service(error_callback);
}
/// Receive a Gotenberg success callback.
#[utoipa::path(
tag = "gotenberg",
request_body = Vec<u8>,
responses((status = NO_CONTENT))
)]
#[post("/gotenberg/success", guard = "internal_network_guard")]
pub async fn success_callback(
web::Header(header::ContentDisposition {
@@ -82,7 +88,7 @@ pub async fn success_callback(
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
#[derive(Debug, Clone, Serialize, Deserialize, Error, utoipa::ToSchema)]
pub struct GotenbergError {
pub status: Option<String>,
pub message: Option<String>,
@@ -101,6 +107,11 @@ impl fmt::Display for GotenbergError {
}
}
/// Receive a Gotenberg error callback.
#[utoipa::path(
tag = "gotenberg",
responses((status = NO_CONTENT))
)]
#[post("/gotenberg/error", guard = "internal_network_guard")]
pub async fn error_callback(
web::Header(GotenbergTrace(trace)): web::Header<GotenbergTrace>,
+21 -9
View File
@@ -13,7 +13,7 @@ use crate::queue::billing::try_process_user_redeemal;
use crate::routes::ApiError;
use crate::util::guards::medal_key_guard;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(web::scope("/medal").service(verify).service(redeem));
}
@@ -22,7 +22,19 @@ struct MedalQuery {
username: String,
}
#[post("verify", guard = "medal_key_guard")]
#[derive(Serialize, utoipa::ToSchema)]
struct VerifyResponse {
user_id: UserId,
redeemed: bool,
}
/// Verify Medal credentials.
#[utoipa::path(
context_path = "/medal",
tag = "medal",
responses((status = OK, body = VerifyResponse))
)]
#[post("/verify", guard = "medal_key_guard")]
pub async fn verify(
pool: web::Data<PgPool>,
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
@@ -35,12 +47,6 @@ pub async fn verify(
)
.await?;
#[derive(Serialize)]
struct VerifyResponse {
user_id: UserId,
redeemed: bool,
}
match maybe_fields {
None => Err(ApiError::NotFound),
Some(fields) => Ok(HttpResponse::Ok().json(VerifyResponse {
@@ -50,7 +56,13 @@ pub async fn verify(
}
}
#[post("redeem", guard = "medal_key_guard")]
/// Redeem Medal credit.
#[utoipa::path(
context_path = "/medal",
tag = "medal",
responses((status = ACCEPTED), (status = CREATED))
)]
#[post("/redeem", guard = "medal_key_guard")]
pub async fn redeem(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+154 -81
View File
@@ -19,99 +19,172 @@ pub mod session;
pub mod statuses;
pub use super::ApiError;
use super::SecurityAddon;
use super::v3::oauth_clients;
use crate::util::cors::default_cors;
use actix_web::web;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
actix_web::web::scope("/_internal")
web::scope("/_internal")
.wrap(default_cors())
.configure(|cfg| {
cfg.service(
actix_web::web::scope("/admin")
.service(admin::count_download)
.service(admin::force_reindex)
.service(admin::force_reindex_project),
);
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)
.service(flows::discord_community_link),
);
cfg.service(pats::get_pats);
cfg.service(pats::create_pat);
cfg.service(pats::edit_pat);
cfg.service(pats::delete_pat);
})
.configure(admin::config)
.configure(session::config)
.configure(flows::config)
.configure(pats::config)
.configure(oauth_clients::config)
.service(web::scope("/moderation").configure(moderation::config))
.service(web::scope("/affiliate").configure(affiliate::config))
.service(web::scope("/campaign").configure(campaign::config))
.service(web::scope("/search-management").configure(search::config))
.service(web::scope("/globals").configure(globals::config))
.service(web::scope("/server-ping").configure(server_ping::config))
.service(web::scope("/attribution").configure(attribution::config))
.configure(billing::config)
.configure(delphi::config)
.configure(external_notifications::config)
.configure(gdpr::config)
.configure(gotenberg::config)
.configure(statuses::config)
.configure(medal::config)
.configure(external_notifications::config)
.configure(mural::config)
.configure(delphi::config),
.configure(statuses::config),
)
.service(
web::scope("/v3/analytics-event")
.wrap(default_cors())
.configure(super::v3::analytics_event::config),
);
}
pub fn utoipa_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
) {
cfg.service(
utoipa_actix_web::scope("/_internal/moderation")
.wrap(default_cors())
.configure(moderation::config),
)
.service(
utoipa_actix_web::scope("/_internal/affiliate")
.wrap(default_cors())
.configure(affiliate::config),
)
.service(
utoipa_actix_web::scope("/_internal/campaign")
.wrap(default_cors())
.configure(campaign::config),
)
.service(
utoipa_actix_web::scope("/_internal/search-management")
.wrap(default_cors())
.configure(search::config),
)
.service(
utoipa_actix_web::scope("/_internal/globals")
.wrap(default_cors())
.configure(globals::config),
)
.service(
utoipa_actix_web::scope("/_internal/server-ping")
.wrap(default_cors())
.configure(server_ping::config),
)
.service(
utoipa_actix_web::scope("/_internal/attribution")
.wrap(default_cors())
.configure(attribution::config),
);
#[derive(utoipa::OpenApi)]
#[openapi(
info(
title = "Internal API (UNSTABLE)",
version = "internal",
description = include_str!("../../api_internal_description.md"),
),
paths(
admin::count_download,
admin::force_reindex,
admin::force_reindex_project,
session::list,
session::delete,
session::refresh,
flows::init,
flows::auth_callback,
flows::create_oauth_account,
flows::discord_community_link,
flows::delete_auth_provider,
flows::validate_create_account_with_password,
flows::create_account_with_password,
flows::login_password,
flows::login_2fa,
flows::begin_2fa_flow,
flows::finish_2fa_flow,
flows::remove_2fa,
flows::reset_password_begin,
flows::change_password,
flows::set_email,
flows::resend_verify_email,
flows::verify_email,
flows::subscribe_newsletter,
flows::get_newsletter_subscription_status,
flows::register_passkey_start,
flows::register_passkey_finish,
flows::authenticate_passkey_start,
flows::authenticate_passkey_finish,
flows::list_passkeys,
flows::rename_passkey,
flows::delete_passkey,
pats::get_pats,
pats::create_pat,
pats::edit_pat,
pats::delete_pat,
moderation::get_projects,
moderation::get_project_meta,
moderation::set_project_meta,
moderation::acquire_lock,
moderation::override_lock,
moderation::get_lock_status,
moderation::release_lock,
moderation::release_lock_beacon,
moderation::delete_all_locks,
moderation::tech_review::get_issue,
moderation::tech_review::get_report,
moderation::tech_review::search_projects,
moderation::tech_review::get_project_report,
moderation::tech_review::submit_report,
moderation::tech_review::update_issue_details,
moderation::tech_review::add_report,
moderation::external_license::search,
moderation::external_license::lookup,
moderation::external_license::get_by_sha1,
moderation::external_license::get_by_sha1_bulk,
moderation::external_license::add_file,
moderation::external_license::reassign_file,
moderation::external_license::update_license,
affiliate::ingest_click,
affiliate::get_all,
affiliate::create,
affiliate::get,
affiliate::delete,
affiliate::patch,
campaign::tiltify_webhook,
campaign::pride_26,
search::tasks,
search::tasks_cancel,
globals::get_globals,
server_ping::ping_minecraft_java,
attribution::scan,
attribution::list,
attribution::update_group,
attribution::assign,
attribution::split,
billing::products,
billing::subscriptions,
billing::refund_charge,
billing::reprocess_charge_tax,
billing::edit_subscription,
billing::user_customer,
billing::charges,
billing::add_payment_method_flow,
billing::edit_payment_method,
billing::remove_payment_method,
billing::payment_methods,
billing::active_servers,
billing::initiate_payment,
billing::stripe_webhook,
billing::credit,
delphi::ingest_report,
delphi::_run,
delphi::version,
delphi::issue_type_schema,
external_notifications::create,
external_notifications::create_email_sync,
external_notifications::remove,
external_notifications::send_custom_email,
gdpr::export,
gotenberg::success_callback,
gotenberg::error_callback,
medal::verify,
medal::redeem,
mural::get_bank_details,
statuses::ws_init,
super::v3::analytics_event::analytics_events_get,
super::v3::analytics_event::analytics_event_create,
super::v3::analytics_event::analytics_event_edit,
super::v3::analytics_event::analytics_event_delete,
),
modifiers(&InternalPathModifier, &SecurityAddon)
)]
pub struct ApiDoc;
struct InternalPathModifier;
impl utoipa::Modify for InternalPathModifier {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
super::prefix_openapi_paths(openapi, "/_internal", |path| {
path.starts_with("/v3/")
});
}
}
@@ -13,7 +13,7 @@ use crate::queue::moderation::ApprovalType;
use crate::routes::ApiError;
use crate::{auth::check_is_moderator_from_headers, queue::session::AuthQueue};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(search)
.service(get_by_sha1)
.service(get_by_sha1_bulk)
@@ -329,9 +329,14 @@ async fn fetch_by_flame_ids(
Ok(results)
}
#[utoipa::path]
/// Search external licenses.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = inline(Vec<ExternalProject>)))
)]
#[post("/search")]
async fn search(
pub async fn search(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -393,9 +398,14 @@ async fn search(
Ok(web::Json(results))
}
#[utoipa::path]
/// Look up external license metadata.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalLicenseLookupResponse))
)]
#[post("/lookup")]
async fn lookup(
pub async fn lookup(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -422,9 +432,14 @@ async fn lookup(
}))
}
#[utoipa::path]
/// Get external license by SHA-1.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[get("/by-sha1/{sha1}")]
async fn get_by_sha1(
pub async fn get_by_sha1(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -448,9 +463,14 @@ async fn get_by_sha1(
Ok(web::Json(result))
}
#[utoipa::path]
/// Get external licenses by SHA-1.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = inline(HashMap<String, ExternalProject>)))
)]
#[post("/by-sha1")]
async fn get_by_sha1_bulk(
pub async fn get_by_sha1_bulk(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -472,9 +492,14 @@ async fn get_by_sha1_bulk(
Ok(web::Json(results))
}
#[utoipa::path]
/// Add an external license file.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[post("/file")]
async fn add_file(
pub async fn add_file(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -484,9 +509,14 @@ async fn add_file(
upsert_file_license(req, pool, redis, session_queue, body).await
}
#[utoipa::path]
/// Reassign an external license file.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[post("/file/reassign")]
async fn reassign_file(
pub async fn reassign_file(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -584,9 +614,14 @@ async fn upsert_file_license(
))
}
#[utoipa::path]
/// Update an external license.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[patch("/{id}")]
async fn update_license(
pub async fn update_license(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -21,11 +21,11 @@ use ownership::get_projects_ownership;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
mod external_license;
pub mod external_license;
mod ownership;
mod tech_review;
pub mod tech_review;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_projects)
.service(get_project_meta)
.service(set_project_meta)
@@ -35,13 +35,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
.service(release_lock)
.service(release_lock_beacon)
.service(delete_all_locks)
.service(web::scope("/tech-review").configure(tech_review::config))
.service(
utoipa_actix_web::scope("/tech-review")
.configure(tech_review::config),
)
.service(
utoipa_actix_web::scope("/external-license")
.configure(external_license::config),
web::scope("/external-license").configure(external_license::config),
);
}
@@ -162,12 +158,19 @@ pub struct DeleteAllLocksResponse {
pub deleted_count: u64,
}
/// Fetch all projects which are in the moderation queue.
/// List projects in the moderation queue.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
params(
("count" = Option<u16>, Query),
("offset" = Option<u32>, Query),
("has_external_dependencies" = Option<bool>, Query)
),
responses((status = OK, body = inline(Vec<FetchedProject>)))
)]
#[get("/projects")]
async fn get_projects(
pub async fn get_projects(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -291,12 +294,14 @@ pub async fn get_projects_internal(
Ok(web::Json(projects))
}
/// Fetch moderation metadata for a specific project.
/// Get project moderation metadata.
#[utoipa::path(
responses((status = OK, body = inline(Vec<Project>)))
context_path = "/moderation",
tag = "moderation",
responses((status = OK, body = MissingMetadata))
)]
#[get("/project/{id}")]
async fn get_project_meta(
pub async fn get_project_meta(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -447,10 +452,14 @@ pub enum Judgement {
},
}
/// Update moderation judgements for projects in the review queue.
#[utoipa::path]
/// Update project moderation judgements.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses((status = NO_CONTENT))
)]
#[post("/project")]
async fn set_project_meta(
pub async fn set_project_meta(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -536,16 +545,18 @@ async fn set_project_meta(
Ok(())
}
/// Acquire or refresh a moderation lock on a project.
/// Acquire a moderation lock.
/// Returns success if acquired, or info about who holds the lock if blocked.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockAcquireResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[post("/lock/{project_id}")]
async fn acquire_lock(
pub async fn acquire_lock(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -594,15 +605,17 @@ async fn acquire_lock(
}
}
/// Force-acquire a moderation lock on a project (moderator override).
/// Override a moderation lock.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockAcquireResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[post("/lock/{project_id}/override")]
async fn override_lock(
pub async fn override_lock(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -639,15 +652,17 @@ async fn override_lock(
}))
}
/// Check the lock status for a project
/// Get moderation lock status.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockStatusResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[get("/lock/{project_id}")]
async fn get_lock_status(
pub async fn get_lock_status(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -699,15 +714,17 @@ async fn get_lock_status(
}
}
/// Release a moderation lock on a project
/// Release a moderation lock.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockReleaseResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[delete("/lock/{project_id}")]
async fn release_lock(
pub async fn release_lock(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -740,12 +757,14 @@ async fn release_lock(
Ok(web::Json(LockReleaseResponse { success: released }))
}
/// Release a moderation lock using credentials in the request body.
/// Release a moderation lock by beacon.
///
/// For use with `navigator.sendBeacon`, which cannot set `Authorization` or send `DELETE`.
/// The body must be `text/plain` containing the same token value as the `Authorization` header
/// (optional `Bearer ` prefix). This avoids a CORS preflight compared to `application/json`.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
request_body(
content = String,
description = "Token value (same as Authorization header)",
@@ -757,7 +776,7 @@ async fn release_lock(
)
)]
#[post("/lock/{project_id}/release")]
async fn release_lock_beacon(
pub async fn release_lock_beacon(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -811,15 +830,17 @@ async fn release_lock_beacon(
Ok(web::Json(LockReleaseResponse { success: released }))
}
/// Delete all moderation locks (admin only)
/// Delete all moderation locks.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = DeleteAllLocksResponse),
(status = UNAUTHORIZED, description = "Not an admin")
)
)]
#[delete("/locks")]
async fn delete_all_locks(
pub async fn delete_all_locks(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -37,7 +37,7 @@ use crate::{
};
use eyre::eyre;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(search_projects)
.service(get_project_report)
.service(get_report)
@@ -193,13 +193,15 @@ pub enum FlagReason {
Delphi,
}
/// Get info on an issue in a Delphi report.
/// Get a Delphi report issue.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(FileIssue)))
)]
#[get("/issue/{issue_id}")]
async fn get_issue(
pub async fn get_issue(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -252,13 +254,15 @@ async fn get_issue(
Ok(web::Json(row.data.0))
}
/// Get info on a specific report for a project.
/// Get a project technical report.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(FileReport)))
)]
#[get("/report/{id}")]
async fn get_report(
pub async fn get_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -662,13 +666,15 @@ async fn fetch_project_reports(
Ok(project_reports)
}
/// Searches all projects which are awaiting technical review.
/// Search projects awaiting technical review.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(Vec<SearchResponse>)))
responses((status = OK, body = SearchResponse))
)]
#[post("/search")]
async fn search_projects(
pub async fn search_projects(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -872,13 +878,15 @@ async fn search_projects(
}))
}
/// Gets the technical review report for a specific project.
/// Get a project technical review report.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(ProjectReportResponse)))
)]
#[get("/project/{id}")]
async fn get_project_report(
pub async fn get_project_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -963,18 +971,20 @@ pub struct SubmitReport {
pub message: Option<String>,
}
/// Submits a verdict for a project based on its technical reports.
/// Submit a technical review verdict.
///
/// Before this is called, all issues for this project's reports must have been
/// marked as either safe or unsafe. Otherwise, this will error with
/// [`ApiError::TechReviewIssuesWithNoVerdict`], providing the issue IDs which
/// are still unmarked.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = NO_CONTENT))
)]
#[post("/submit/{project_id}")]
async fn submit_report(
pub async fn submit_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -1163,16 +1173,18 @@ pub struct UpdateIssue {
pub verdict: DelphiVerdict,
}
/// Updates the state of a technical review issue detail.
/// Update technical review issue details.
///
/// This will not automatically reject the project for malware, but just flag
/// this issue with a verdict.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = NO_CONTENT))
)]
#[patch("/issue-detail")]
async fn update_issue_details(
pub async fn update_issue_details(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -1273,11 +1285,15 @@ pub struct AddReport {
pub file_id: FileId,
}
/// Adds a file to the technical review queue by adding an empty report, if one
/// Add a technical review report.
/// does not already exist for it.
#[utoipa::path]
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
responses((status = OK, body = DelphiReportId))
)]
#[put("/report")]
async fn add_report(
pub async fn add_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+7 -2
View File
@@ -6,12 +6,17 @@ use crate::{
queue::payouts::PayoutsQueue, routes::ApiError, util::error::Context,
};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_bank_details);
}
/// Get bank details.
#[utoipa::path(
tag = "mural",
responses((status = OK, body = serde_json::Value))
)]
#[get("/mural/bank-details")]
async fn get_bank_details(
pub async fn get_bank_details(
payouts_queue: web::Data<PayoutsQueue>,
) -> Result<web::Json<muralpay::BankDetailsResponse>, ApiError> {
let mural = payouts_queue.muralpay.load();
+26 -14
View File
@@ -22,20 +22,22 @@ use crate::util::validate::validation_errors_to_string;
use serde::Deserialize;
use validator::Validate;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_pats);
cfg.service(create_pat);
cfg.service(edit_pat);
cfg.service(delete_pat);
}
/// List personal access tokens.
#[utoipa::path(
tag = "personal access tokens",
get,
operation_id = "getPats",
responses(
(status = 200, description = "List of PATs"),
(status = 401, description = "Unauthorized")
),
operation_id = "getPats",
responses(
(status = 200, description = "List of PATs", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_READ"]))
)]
#[get("/pat")]
@@ -82,14 +84,16 @@ pub struct NewPersonalAccessToken {
pub expires: DateTime<Utc>,
}
/// Create a personal access token.
#[utoipa::path(
tag = "personal access tokens",
post,
operation_id = "createPat",
responses(
(status = 200, description = "PAT created"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
operation_id = "createPat",
responses(
(status = 200, description = "PAT created", body = serde_json::Value),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_CREATE"]))
)]
#[post("/pat")]
@@ -185,10 +189,14 @@ pub struct ModifyPersonalAccessToken {
pub expires: Option<DateTime<Utc>>,
}
/// Update a personal access token.
#[utoipa::path(
tag = "personal access tokens",
patch,
operation_id = "editPat",
params(("id" = String, Path, description = "The PAT ID")),
params(
("id" = String, Path, description = "The PAT ID")
),
responses(
(status = 204, description = "PAT updated"),
(status = 400, description = "Invalid input"),
@@ -293,10 +301,14 @@ pub async fn edit_pat(
Ok(HttpResponse::NoContent().finish())
}
/// Delete a personal access token.
#[utoipa::path(
tag = "personal access tokens",
delete,
operation_id = "deletePat",
params(("id" = String, Path, description = "The PAT ID")),
params(
("id" = String, Path, description = "The PAT ID")
),
responses(
(status = 204, description = "PAT deleted"),
(status = 401, description = "Unauthorized")
+15 -5
View File
@@ -5,20 +5,30 @@ use crate::{
};
use actix_web::{delete, get, web};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(tasks).service(tasks_cancel);
}
#[utoipa::path]
#[get("tasks", guard = "admin_key_guard")]
/// List search tasks.
#[utoipa::path(
context_path = "/search-management",
tag = "search",
responses((status = OK, body = serde_json::Value))
)]
#[get("/tasks", guard = "admin_key_guard")]
pub async fn tasks(
search: web::Data<dyn SearchBackend>,
) -> Result<web::Json<serde_json::Value>, ApiError> {
Ok(web::Json(search.tasks().await.map_err(ApiError::Internal)?))
}
#[utoipa::path]
#[delete("tasks", guard = "admin_key_guard")]
/// Cancel search tasks.
#[utoipa::path(
context_path = "/search-management",
tag = "search",
responses((status = NO_CONTENT))
)]
#[delete("/tasks", guard = "admin_key_guard")]
pub async fn tasks_cancel(
search: web::Data<dyn SearchBackend>,
body: web::Json<TasksCancelFilter>,
@@ -12,7 +12,7 @@ use crate::{
util::error::Context,
};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(ping_minecraft_java);
}
@@ -22,7 +22,12 @@ pub struct PingRequest {
pub timeout_ms: Option<u64>,
}
#[utoipa::path]
/// Ping Minecraft server.
#[utoipa::path(
context_path = "/server-ping",
tag = "server ping",
responses((status = NO_CONTENT))
)]
#[post("/minecraft-java")]
pub async fn ping_minecraft_java(
req: HttpRequest,
+24 -13
View File
@@ -19,9 +19,9 @@ use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use woothee::parser::Parser;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
utoipa_actix_web::scope("/session")
web::scope("/session")
.service(list)
.service(delete)
.service(refresh),
@@ -133,13 +133,16 @@ pub async fn issue_session(
Ok(session)
}
/// List sessions.
#[utoipa::path(
context_path = "/session",
tag = "sessions",
get,
operation_id = "listSessions",
responses(
(status = 200, description = "List of active sessions"),
(status = 401, description = "Unauthorized")
),
operation_id = "listSessions",
responses(
(status = 200, description = "List of active sessions", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["SESSION_READ"]))
)]
#[get("/list")]
@@ -178,10 +181,15 @@ pub async fn list(
Ok(HttpResponse::Ok().json(sessions))
}
/// Delete a session.
#[utoipa::path(
context_path = "/session",
tag = "sessions",
delete,
operation_id = "deleteSession",
params(("id" = String, Path, description = "The session ID")),
params(
("id" = String, Path, description = "The session ID")
),
responses(
(status = 204, description = "Session deleted"),
(status = 401, description = "Unauthorized")
@@ -228,13 +236,16 @@ pub async fn delete(
Ok(HttpResponse::NoContent().body(""))
}
/// Refresh a session.
#[utoipa::path(
context_path = "/session",
tag = "sessions",
post,
operation_id = "refreshSession",
responses(
(status = 200, description = "Session refreshed"),
(status = 401, description = "Unauthorized")
)
operation_id = "refreshSession",
responses(
(status = 200, description = "Session refreshed", body = serde_json::Value),
(status = 401, description = "Unauthorized")
)
)]
#[post("/refresh")]
pub async fn refresh(
@@ -35,7 +35,7 @@ use std::sync::atomic::Ordering;
use tokio::sync::oneshot::error::TryRecvError;
use tokio::time::{Duration, sleep};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(ws_init);
}
@@ -45,7 +45,12 @@ struct LauncherHeartbeatInit {
}
// TODO: Move launcher-specific tunnel traffic to a proper launcher websocket endpoint.
#[get("launcher_socket")]
/// Start launcher socket.
#[utoipa::path(
tag = "statuses",
responses((status = 101))
)]
#[get("/launcher_socket")]
pub async fn ws_init(
req: HttpRequest,
pool: Data<PgPool>,