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
@@ -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>,