diff --git a/Cargo.lock b/Cargo.lock
index 19f6627e78..028da5aaf8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -59,9 +59,9 @@ dependencies = [
[[package]]
name = "actix-http"
-version = "3.11.2"
+version = "3.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
+checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2"
dependencies = [
"actix-codec",
"actix-rt",
@@ -75,7 +75,7 @@ dependencies = [
"derive_more 2.1.1",
"encoding_rs",
"flate2",
- "foldhash 0.1.5",
+ "foldhash 0.2.0",
"futures-core",
"h2 0.3.27",
"http 0.2.12",
@@ -87,8 +87,8 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
- "rand 0.9.2",
- "sha1",
+ "rand 0.10.1",
+ "sha1 0.11.0",
"smallvec",
"tokio",
"tokio-util",
@@ -146,9 +146,9 @@ dependencies = [
[[package]]
name = "actix-router"
-version = "0.5.3"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
+checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7"
dependencies = [
"bytestring",
"cfg-if",
@@ -209,9 +209,9 @@ dependencies = [
[[package]]
name = "actix-web"
-version = "4.11.0"
+version = "4.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea"
+checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4"
dependencies = [
"actix-codec",
"actix-http",
@@ -228,7 +228,7 @@ dependencies = [
"cookie 0.16.2",
"derive_more 2.1.1",
"encoding_rs",
- "foldhash 0.1.5",
+ "foldhash 0.2.0",
"futures-core",
"futures-util",
"impl-more",
@@ -244,7 +244,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"smallvec",
- "socket2 0.5.10",
+ "socket2 0.6.1",
"time",
"tracing",
"url",
@@ -1127,7 +1127,7 @@ dependencies = [
"http-body-util",
"md-5",
"pin-project-lite",
- "sha1",
+ "sha1 0.10.6",
"sha2",
"tracing",
]
@@ -1499,7 +1499,7 @@ version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
- "digest",
+ "digest 0.10.7",
]
[[package]]
@@ -1511,6 +1511,15 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
[[package]]
name = "block2"
version = "0.5.1"
@@ -2224,6 +2233,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
[[package]]
name = "const-random"
version = "0.1.18"
@@ -2424,7 +2439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d"
dependencies = [
"crc",
- "digest",
+ "digest 0.10.7",
"rustversion",
"spin 0.10.0",
]
@@ -2515,6 +2530,15 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
[[package]]
name = "cssparser"
version = "0.29.6"
@@ -2791,7 +2815,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
- "const-oid",
+ "const-oid 0.9.6",
"pem-rfc7468",
"zeroize",
]
@@ -2904,12 +2928,23 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "block-buffer",
- "const-oid",
- "crypto-common",
+ "block-buffer 0.10.4",
+ "const-oid 0.9.6",
+ "crypto-common 0.1.6",
"subtle",
]
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.1",
+ "const-oid 0.10.2",
+ "crypto-common 0.2.2",
+]
+
[[package]]
name = "directories"
version = "6.0.0"
@@ -3127,7 +3162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
- "digest",
+ "digest 0.10.7",
"elliptic-curve",
"rfc6979",
"signature",
@@ -3151,7 +3186,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
- "digest",
+ "digest 0.10.7",
"ff",
"generic-array",
"group",
@@ -4402,7 +4437,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
- "digest",
+ "digest 0.10.7",
]
[[package]]
@@ -4532,6 +4567,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+[[package]]
+name = "hybrid-array"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
+dependencies = [
+ "typenum",
+]
+
[[package]]
name = "hyper"
version = "0.14.32"
@@ -4893,9 +4937,9 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
[[package]]
name = "impl-more"
-version = "0.1.9"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
+checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870"
[[package]]
name = "indenter"
@@ -5390,11 +5434,12 @@ dependencies = [
"rust_iso3166",
"rustls 0.23.32",
"rusty-money",
+ "scalar_api_reference",
"sentry",
"serde",
"serde_json",
"serde_with",
- "sha1",
+ "sha1 0.10.6",
"sha2",
"spdx",
"sqlx",
@@ -5411,8 +5456,6 @@ dependencies = [
"url",
"urlencoding",
"utoipa",
- "utoipa-actix-web",
- "utoipa-scalar",
"uuid 1.23.3",
"validator",
"webauthn-rs",
@@ -5822,7 +5865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
- "digest",
+ "digest 0.10.7",
]
[[package]]
@@ -8494,8 +8537,8 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
dependencies = [
- "const-oid",
- "digest",
+ "const-oid 0.9.6",
+ "digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-traits",
@@ -8508,6 +8551,40 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rust-embed"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.106",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
[[package]]
name = "rust-ini"
version = "0.21.3"
@@ -8827,6 +8904,18 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "scalar_api_reference"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d67c85edd56c41f364603eff688a330f01dee99bdfc84c005d3bc3d0721d6b2"
+dependencies = [
+ "actix-web",
+ "rust-embed",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "scc"
version = "2.4.0"
@@ -9252,15 +9341,15 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.145"
+version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
- "ryu",
"serde",
"serde_core",
+ "zmij",
]
[[package]]
@@ -9455,7 +9544,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
- "digest",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha1"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
]
[[package]]
@@ -9472,7 +9572,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
- "digest",
+ "digest 0.10.7",
]
[[package]]
@@ -9505,7 +9605,7 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
- "digest",
+ "digest 0.10.7",
"rand_core 0.6.4",
]
@@ -9782,7 +9882,7 @@ dependencies = [
"bytes",
"chrono",
"crc",
- "digest",
+ "digest 0.10.7",
"dotenvy",
"either",
"futures-channel",
@@ -9803,7 +9903,7 @@ dependencies = [
"rsa",
"rust_decimal",
"serde",
- "sha1",
+ "sha1 0.10.6",
"sha2",
"smallvec",
"sqlx-core",
@@ -11290,7 +11390,7 @@ dependencies = [
"constant_time_eq",
"hmac",
"rand 0.9.2",
- "sha1",
+ "sha1 0.10.6",
"sha2",
]
@@ -11505,7 +11605,7 @@ dependencies = [
"rand 0.9.2",
"rustls 0.23.32",
"rustls-pki-types",
- "sha1",
+ "sha1 0.10.6",
"thiserror 2.0.17",
"utf-8",
]
@@ -11524,9 +11624,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
-version = "1.19.0"
+version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uds_windows"
@@ -11757,17 +11857,6 @@ dependencies = [
"utoipa-gen",
]
-[[package]]
-name = "utoipa-actix-web"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f"
-dependencies = [
- "actix-service",
- "actix-web",
- "utoipa",
-]
-
[[package]]
name = "utoipa-gen"
version = "5.4.0"
@@ -11782,18 +11871,6 @@ dependencies = [
"uuid 1.23.3",
]
-[[package]]
-name = "utoipa-scalar"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
-dependencies = [
- "actix-web",
- "serde",
- "serde_json",
- "utoipa",
-]
-
[[package]]
name = "uuid"
version = "0.8.2"
@@ -13450,6 +13527,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
[[package]]
name = "zopfli"
version = "0.8.2"
diff --git a/Cargo.toml b/Cargo.toml
index ac9044a826..a7a6d055b3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -158,6 +158,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
] }
rustls = "0.23.32"
rusty-money = "0.4.1"
+scalar_api_reference = { version = "0.2.2", default-features = false }
secrecy = "0.10.3"
sentry = { version = "0.45.0", default-features = false, features = [
"backtrace",
@@ -217,8 +218,6 @@ typed-path = "0.12.0"
url = "2.5.7"
urlencoding = "2.1.3"
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
-utoipa-actix-web = { version = "0.1.2" }
-utoipa-scalar = { version = "0.3.0", default-features = false }
uuid = "1.18.1"
validator = "0.20.0"
webauthn-rs = "0.5.5"
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index 1c1ba0c42c..4f702d6bab 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -100,6 +100,7 @@ rust_decimal = { workspace = true, features = [
rust_iso3166 = { workspace = true }
rustls.workspace = true
rusty-money = { workspace = true }
+scalar_api_reference = { workspace = true, features = ["actix-web"] }
sentry = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -128,8 +129,6 @@ tracing-actix-web = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
utoipa = { workspace = true, features = ["url"] }
-utoipa-actix-web = { workspace = true }
-utoipa-scalar = { workspace = true, features = ["actix-web"] }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4", "v7"] }
validator = { workspace = true, features = ["derive"] }
webauthn-rs = { workspace = true, features = [
diff --git a/apps/labrinth/src/api_v2_description.md b/apps/labrinth/src/api_v2_description.md
new file mode 100644
index 0000000000..268a6b3cee
--- /dev/null
+++ b/apps/labrinth/src/api_v2_description.md
@@ -0,0 +1,117 @@
+API v2 is Modrinth's official API. It is the recommended API for integrations and applications, and it will receive long-term support.
+
+## Authentication
+This API has two options for authentication: personal access tokens and [OAuth2](https://en.wikipedia.org/wiki/OAuth).
+All tokens are tied to a Modrinth user and use the `Authorization` header of the request.
+
+Example:
+```
+Authorization: mrp_RNtLRSPmGj2pd1v1ubi52nX7TJJM9sznrmwhAuj511oe4t1jAqAQ3D6Wc8Ic
+```
+
+You do not need a token for most requests. Generally speaking, only the following types of requests require a token:
+- those which create data (such as version creation)
+- those which modify data (such as editing a project)
+- those which access private data (such as draft projects, notifications, emails, and payout data)
+
+Each request requiring authentication has a certain scope. For example, to view the email of the user being requested, the token must have the `USER_READ_EMAIL` scope.
+You can find the list of available scopes [on GitHub](https://github.com/modrinth/labrinth/blob/master/src/models/pats.rs#L15). Making a request with an invalid scope will return a 401 error.
+
+Please note that certain scopes and requests cannot be completed with a personal access token or using OAuth.
+For example, deleting a user account can only be done through Modrinth's frontend.
+
+A detailed guide on OAuth has been published in [Modrinth's technical documentation](https://docs.modrinth.com/guide/oauth).
+
+### Personal access tokens
+Personal access tokens (PATs) can be generated in from [the user settings](https://modrinth.com/settings/account).
+
+### GitHub tokens
+For backwards compatibility purposes, some types of GitHub tokens also work for authenticating a user with Modrinth's API, granting all scopes.
+**We urge any application still using GitHub tokens to start using personal access tokens for security and reliability purposes.**
+
+## Cross-Origin Resource Sharing
+This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with the [W3C spec](https://www.w3.org/TR/cors/).
+This allows for cross-domain communication from the browser.
+All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
+
+## Identifiers
+The majority of items you can interact with in the API have a unique eight-digit base62 ID.
+Projects, versions, users, threads, teams, and reports all use this same way of identifying themselves.
+Version files use the sha1 or sha512 file hashes as identifiers.
+
+Each project and user has a friendlier way of identifying them; slugs and usernames, respectively.
+While unique IDs are constant, slugs and usernames can change at any moment.
+If you want to store something in the long term, it is recommended to use the unique ID.
+
+## Ratelimits
+The API has a ratelimit defined per IP. Limits and remaining amounts are given in the response headers.
+- `X-Ratelimit-Limit`: the maximum number of requests that can be made in a minute
+- `X-Ratelimit-Remaining`: the number of requests remaining in the current ratelimit window
+- `X-Ratelimit-Reset`: the time in seconds until the ratelimit window resets
+
+Ratelimits are the same no matter whether you use a token or not.
+The ratelimit is currently 300 requests per minute. If you have a use case requiring a higher limit, please [contact us](mailto:support@modrinth.com).
+
+## User Agents
+To access the Modrinth API, you **must** use provide a uniquely-identifying `User-Agent` header.
+Providing a user agent that only identifies your HTTP client library (such as "okhttp/4.9.3") increases the likelihood that we will block your traffic.
+It is recommended, but not required, to include contact information in your user agent.
+This allows us to contact you if we would like a change in your application's behavior without having to block your traffic.
+- Bad: `User-Agent: okhttp/4.9.3`
+- Good: `User-Agent: project_name`
+- Better: `User-Agent: github_username/project_name/1.56.0`
+- Best: `User-Agent: github_username/project_name/1.56.0 (launcher.com)` or `User-Agent: github_username/project_name/1.56.0 (contact@launcher.com)`
+
+## Versioning
+Modrinth follows a simple pattern for its API versioning.
+In the event of a breaking API change, the API version in the URL path is bumped, and migration steps will be published below.
+
+API v2 is the current official API and will receive long-term support.
+
+### Migrations
+Inside the following spoiler, you will be able to find all changes between versions of the Modrinth API, accompanied by tips and a guide to migrate applications to newer versions.
+
+Here, you can also find changes for [Minotaur](https://github.com/modrinth/minotaur), Modrinth's official Gradle plugin. Major versions of Minotaur directly correspond to major versions of the Modrinth API.
+
+API v1 to API v2
+
+These bullet points cover most changes in the v2 API, but please note that fields containing `mod` in most contexts have been shifted to `project`. For example, in the search route, the field `mod_id` was renamed to `project_id`.
+
+- The search route has been moved from `/api/v1/mod` to `/v2/search`
+- New project fields: `project_type` (may be `mod` or `modpack`), `moderation_message` (which has a `message` and `body`), `gallery`
+- New search facet: `project_type`
+- Alphabetical sort removed (it didn't work and is not possible due to limits in MeiliSearch)
+- New search fields: `project_type`, `gallery`
+ - The gallery field is an array of URLs to images that are part of the project's gallery
+- The gallery is a new feature which allows the user to upload images showcasing their mod to the CDN which will be displayed on their mod page
+- Internal change: Any project file uploaded to Modrinth is now validated to make sure it's a valid Minecraft mod, Modpack, etc.
+ - For example, a Forge 1.17 mod with a JAR not containing a mods.toml will not be allowed to be uploaded to Modrinth
+- In project creation, projects may not upload a mod with no versions to review, however they can be saved as a draft
+ - Similarly, for version creation, a version may not be uploaded without any files
+- Donation URLs have been enabled
+- New project status: `archived`. Projects with this status do not appear in search
+- Tags (such as categories, loaders) now have icons (SVGs) and specific project types attached
+- Dependencies have been wiped and replaced with a new system
+- Notifications now have a `type` field, such as `project_update`
+
+Along with this, project subroutes (such as `/v2/project/{id}/version`) now allow the slug to be used as the ID. This is also the case with user routes.
+
+Minotaur v1 to Minotaur v2
+
+Minotaur 2.x introduced a few breaking changes to how your buildscript is formatted.
+
+First, instead of registering your own `publishModrinth` task, Minotaur now automatically creates a `modrinth` task. As such, you can replace the `task publishModrinth(type: TaskModrinthUpload) {` line with just `modrinth {`.
+
+To declare supported Minecraft versions and mod loaders, the `gameVersions` and `loaders` arrays must now be used. The syntax for these are pretty self-explanatory.
+
+Instead of using `releaseType`, you must now use `versionType`. This was actually changed in v1.2.0, but very few buildscripts have moved on from v1.1.0.
+
+Dependencies have been changed to a special DSL. Create a `dependencies` block within the `modrinth` block, and then use `scope.type("project/version")`. For example, `required.project("fabric-api")` adds a required project dependency on Fabric API.
+
+You may now use the slug anywhere that a project ID was previously required.
+
+
+
+
+
+The above snippet about User Agents was adapted from https://crates.io/policies, copyright (c) 2014 The Rust Project Developers under MIT license.
diff --git a/apps/labrinth/src/auth/oauth/mod.rs b/apps/labrinth/src/auth/oauth/mod.rs
index d3fc5df6ea..95bec03a90 100644
--- a/apps/labrinth/src/auth/oauth/mod.rs
+++ b/apps/labrinth/src/auth/oauth/mod.rs
@@ -18,7 +18,7 @@ use crate::models::ids::OAuthClientId;
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use actix_web::http::header::{CACHE_CONTROL, LOCATION, PRAGMA};
-use actix_web::web::{Data, Query, ServiceConfig};
+use actix_web::web::{Data, Query};
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use chrono::{DateTime, Duration};
use rand::distributions::Alphanumeric;
@@ -33,7 +33,7 @@ use super::AuthenticationError;
pub mod errors;
pub mod uris;
-pub fn config(cfg: &mut ServiceConfig) {
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(init_oauth)
.service(accept_client_scopes)
.service(reject_client_scopes)
@@ -57,6 +57,7 @@ pub struct OAuthClientAccessRequest {
pub requested_scopes: Scopes,
}
+#[utoipa::path(path = "/authorize", tag = "oauth", responses((status = OK)))]
#[get("authorize")]
pub async fn init_oauth(
req: HttpRequest,
@@ -165,11 +166,12 @@ pub async fn init_oauth(
}
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct RespondToOAuthClientScopes {
pub flow: String,
}
+#[utoipa::path(path = "/accept", tag = "oauth", responses((status = OK)))]
#[post("accept")]
pub async fn accept_client_scopes(
req: HttpRequest,
@@ -189,6 +191,7 @@ pub async fn accept_client_scopes(
.await
}
+#[utoipa::path(path = "/reject", tag = "oauth", responses((status = OK)))]
#[post("reject")]
pub async fn reject_client_scopes(
req: HttpRequest,
@@ -201,7 +204,7 @@ pub async fn reject_client_scopes(
.await
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct TokenRequest {
pub grant_type: String,
pub code: String,
@@ -216,6 +219,7 @@ pub struct TokenResponse {
pub expires_in: i64,
}
+#[utoipa::path(path = "/token", tag = "oauth", responses((status = OK)))]
#[post("token")]
/// Params should be in the urlencoded request body
/// And client secret should be in the HTTP basic authorization header
@@ -464,3 +468,13 @@ fn append_params_to_uri(uri: &str, params: &[impl AsRef]) -> String {
uri
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ init_oauth,
+ accept_client_scopes,
+ reject_client_scopes,
+ request_token,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs
index 076a19dd4a..ce2054ce3d 100644
--- a/apps/labrinth/src/lib.rs
+++ b/apps/labrinth/src/lib.rs
@@ -361,6 +361,14 @@ pub fn app_setup(
pub fn app_config(
cfg: &mut web::ServiceConfig,
labrinth_config: LabrinthConfig,
+) {
+ app_data_config(cfg, labrinth_config);
+ app_fallback_config(cfg);
+}
+
+pub fn app_data_config(
+ cfg: &mut web::ServiceConfig,
+ labrinth_config: LabrinthConfig,
) {
cfg.app_data(web::FormConfig::default().error_handler(|err, _req| {
routes::ApiError::Validation(err.to_string()).into()
@@ -398,16 +406,17 @@ pub fn app_config(
.app_data(labrinth_config.rate_limiter.clone())
.app_data(labrinth_config.kafka_client.clone())
.app_data(labrinth_config.search_state.clone())
- .app_data(labrinth_config.webauthn.clone())
- .configure(routes::v3::config)
- .configure(routes::internal::config)
- .configure(routes::root_config)
- .default_service(web::get().wrap(default_cors()).to(routes::not_found));
+ .app_data(labrinth_config.webauthn.clone());
}
-pub fn utoipa_app_config(
- cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
- _labrinth_config: LabrinthConfig,
+pub fn app_fallback_config(cfg: &mut web::ServiceConfig) {
+ cfg.configure(routes::root_config)
+ .default_service(web::get().to(routes::not_found).wrap(default_cors()));
+}
+
+pub fn app_routes_config(
+ cfg: &mut web::ServiceConfig,
+ labrinth_config: LabrinthConfig,
) {
cfg.configure({
#[cfg(target_os = "linux")]
@@ -419,7 +428,29 @@ pub fn utoipa_app_config(
|_cfg| ()
}
})
- .configure(routes::v2::utoipa_config)
- .configure(routes::v3::utoipa_config)
- .configure(routes::internal::utoipa_config);
+ .configure(|cfg| app_routes_config_v2(cfg, labrinth_config.clone()))
+ .configure(|cfg| app_routes_config_v3(cfg, labrinth_config.clone()))
+ .configure(|cfg| app_routes_config_internal(cfg, labrinth_config));
+}
+
+pub fn app_routes_config_v2(
+ cfg: &mut web::ServiceConfig,
+ _labrinth_config: LabrinthConfig,
+) {
+ cfg.configure(routes::v2::config);
+}
+
+pub fn app_routes_config_v3(
+ cfg: &mut web::ServiceConfig,
+ _labrinth_config: LabrinthConfig,
+) {
+ cfg.configure(routes::public_config)
+ .configure(routes::v3::config);
+}
+
+pub fn app_routes_config_internal(
+ cfg: &mut web::ServiceConfig,
+ _labrinth_config: LabrinthConfig,
+) {
+ cfg.configure(routes::internal::config);
}
diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs
index 8b636f485e..2a75f394d7 100644
--- a/apps/labrinth/src/main.rs
+++ b/apps/labrinth/src/main.rs
@@ -2,7 +2,7 @@
use actix_web::dev::Service;
use actix_web::middleware::from_fn;
-use actix_web::{App, HttpServer, web};
+use actix_web::{App, HttpResponse, HttpServer, web};
use actix_web_prom::PrometheusMetricsBuilder;
use clap::Parser;
@@ -15,17 +15,20 @@ use labrinth::search;
use labrinth::util::anrok;
use labrinth::util::gotenberg::GotenbergClient;
use labrinth::util::ratelimit::rate_limit_middleware;
-use labrinth::utoipa_app_config;
-use labrinth::{app_config, env};
+use labrinth::{app_data_config, app_fallback_config, env};
+use labrinth::{
+ app_routes_config_internal, app_routes_config_v2, app_routes_config_v3,
+};
use labrinth::{clickhouse, database, file_hosting};
+use scalar_api_reference::actix_web::config as scalar_config;
+use serde_json::json;
use std::ffi::CStr;
use std::sync::Arc;
use tracing::{Instrument, info, info_span};
use tracing_actix_web::TracingLogger;
use utoipa::OpenApi;
-use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
-use utoipa_actix_web::AppExt;
-use utoipa_scalar::Servable;
+use utoipa::openapi::extensions::ExtensionsBuilder;
+use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
#[cfg(target_os = "linux")]
#[global_allocator]
@@ -223,7 +226,15 @@ async fn app() -> std::io::Result<()> {
info!("Starting Actix HTTP server!");
HttpServer::new(move || {
- App::new()
+ let docs_v2 = DocsV2::openapi()
+ .merge_from(labrinth::routes::v2::ApiDoc::openapi());
+ let docs_v3 = DocsV3::openapi()
+ .merge_from(labrinth::routes::PublicApiDoc::openapi())
+ .merge_from(labrinth::routes::v3::ApiDoc::openapi());
+ let docs_internal = DocsInternal::openapi()
+ .merge_from(labrinth::routes::internal::ApiDoc::openapi());
+
+ let app = App::new()
.wrap(TracingLogger::default())
.wrap_fn(|req, srv| {
// We capture the same fields as `tracing-actix-web`'s `RootSpanBuilder`.
@@ -260,12 +271,76 @@ async fn app() -> std::io::Result<()> {
// transactions out of HTTP requests. However, we have to use our
// own - See `sentry::SentryErrorReporting` for why.
.wrap(labrinth::util::sentry::SentryErrorReporting)
- // Use `utoipa` for OpenAPI generation
- .into_utoipa_app()
- .configure(|cfg| utoipa_app_config(cfg, labrinth_config.clone()))
- .openapi_service(|api| utoipa_scalar::Scalar::with_url("/docs", ApiDoc::openapi().merge_from(api)))
- .into_app()
- .configure(|cfg| app_config(cfg, labrinth_config.clone()))
+ .configure(|cfg| app_data_config(cfg, labrinth_config.clone()))
+ .configure(|cfg| app_routes_config_v2(cfg, labrinth_config.clone()))
+ .configure(|cfg| app_routes_config_v3(cfg, labrinth_config.clone()))
+ .configure(|cfg| {
+ app_routes_config_internal(cfg, labrinth_config.clone())
+ });
+
+ let scalar_configuration = json!({
+ "sources": [
+ {
+ "title": "API v2",
+ "slug": "v2",
+ "url": "/openapi/v2.json",
+ "default": true
+ },
+ {
+ "title": "API v3 (UNSTABLE - DO NOT USE)",
+ "slug": "v3",
+ "url": "/openapi/v3.json"
+ },
+ {
+ "title": "Internal API - HIGHLY UNSTABLE - DO NOT USE",
+ "slug": "internal",
+ "url": "/openapi/internal.json"
+ }
+ ],
+ "agent": {
+ "disabled": true
+ },
+ "mcp": {
+ "disabled": true
+ },
+ "telemetry": false,
+
+ "metaData": {
+ "title": "Modrinth API Documentation",
+ "description": "Reference documentation for the Modrinth API.",
+ "ogTitle": "Modrinth API Documentation",
+ "ogDescription": "Reference documentation for the Modrinth API."
+ },
+
+ "modelsSectionLabel": "Schemas",
+ "defaultOpenFirstTag": true,
+ "defaultOpenAllTags": false,
+ "expandAllResponses": false,
+ "expandAllSchemaProperties": false,
+ "expandAllModelSections": false,
+ "orderSchemaPropertiesBy": "preserve",
+ "orderRequiredPropertiesFirst": true,
+ "hideSearch": false,
+ "searchHotKey": "k",
+ "showOperationId": false,
+
+ "defaultHttpClient": {
+ "targetKey": "shell",
+ "clientKey": "curl"
+ },
+
+ "persistAuth": false,
+ "showDeveloperTools": "never",
+ });
+
+ app.service(openapi_json_service("/openapi/v2.json", docs_v2))
+ .service(openapi_json_service("/openapi/v3.json", docs_v3))
+ .service(openapi_json_service(
+ "/openapi/internal.json",
+ docs_internal,
+ ))
+ .configure(scalar_config("/docs", &scalar_configuration))
+ .configure(app_fallback_config)
})
.bind(&ENV.BIND_ADDR)?
.run()
@@ -273,19 +348,87 @@ async fn app() -> std::io::Result<()> {
}
#[derive(utoipa::OpenApi)]
-#[openapi(info(title = "Labrinth"), modifiers(&SecurityAddon))]
-struct ApiDoc;
+#[openapi(
+ info(title = "Modrinth API v2", version = "2.0.0"),
+ modifiers(&SecurityAddon, &V2DescriptionAddon)
+)]
+struct DocsV2;
+
+const API_V2_DESCRIPTION: &str = include_str!("api_v2_description.md");
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ info(
+ title = "API v3 (UNSTABLE - DO NOT USE)",
+ version = "3.0.0"
+ ),
+ modifiers(&SecurityAddon)
+)]
+struct DocsV3;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ info(
+ title = "Internal API - HIGHLY UNSTABLE - DO NOT USE",
+ version = "internal"
+ ),
+ modifiers(&SecurityAddon)
+)]
+struct DocsInternal;
+
+struct V2DescriptionAddon;
+
+impl utoipa::Modify for V2DescriptionAddon {
+ fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
+ openapi.info.description = Some(API_V2_DESCRIPTION.to_string());
+ }
+}
+
+fn openapi_json_service(
+ path: &'static str,
+ openapi: utoipa::openapi::OpenApi,
+) -> actix_web::Resource {
+ web::resource(path).route(web::get().to(move || {
+ let openapi = openapi.clone();
+ async move { openapi_json(openapi) }
+ }))
+}
+
+fn openapi_json(openapi: utoipa::openapi::OpenApi) -> HttpResponse {
+ match serde_json::to_string_pretty(&openapi) {
+ Ok(body) => HttpResponse::Ok()
+ .content_type("application/json; charset=utf-8")
+ .body(body),
+ Err(error) => {
+ tracing::error!(%error, "Failed to serialize OpenAPI schema");
+ HttpResponse::InternalServerError().finish()
+ }
+ }
+}
struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.as_mut().unwrap();
+ let mut bearer_auth = HttpBuilder::new()
+ .scheme(HttpAuthScheme::Bearer)
+ .description(Some(
+ "Use a personal access token. Example: `mrp_RNtLRSPmGj2pd1v1ubi52nX7TJJM9sznrmwhAuj511oe4t1jAqAQ3D6Wc8Ic`.",
+ ))
+ .build();
+ bearer_auth.extensions = Some(
+ ExtensionsBuilder::new()
+ .add(
+ "x-example",
+ "mrp_RNtLRSPmGj2pd1v1ubi52nX7TJJM9sznrmwhAuj511oe4t1jAqAQ3D6Wc8Ic",
+ )
+ .build(),
+ );
+
components.add_security_scheme(
"bearer_auth",
- SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new(
- "authorization",
- ))),
+ SecurityScheme::Http(bearer_auth),
);
}
}
diff --git a/apps/labrinth/src/models/v2/notifications.rs b/apps/labrinth/src/models/v2/notifications.rs
index 3e60cdbe89..044adf554e 100644
--- a/apps/labrinth/src/models/v2/notifications.rs
+++ b/apps/labrinth/src/models/v2/notifications.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::large_stack_arrays)]
+
use crate::models::ids::{ThreadMessageId, VersionId};
use crate::models::v3::billing::PriceDuration;
use crate::models::{
@@ -13,7 +15,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct LegacyNotification {
pub id: NotificationId,
pub user_id: UserId,
@@ -30,14 +32,14 @@ pub struct LegacyNotification {
pub actions: Vec,
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct LegacyNotificationAction {
pub title: String,
/// The route to call when this notification action is called. Formatted HTTP Method, route
pub action_route: (String, String),
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LegacyNotificationBody {
TaxNotification {
diff --git a/apps/labrinth/src/models/v2/reports.rs b/apps/labrinth/src/models/v2/reports.rs
index 7682941fb7..7fedbb59af 100644
--- a/apps/labrinth/src/models/v2/reports.rs
+++ b/apps/labrinth/src/models/v2/reports.rs
@@ -4,7 +4,7 @@ use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct LegacyReport {
pub id: ReportId,
pub report_type: String,
@@ -17,7 +17,7 @@ pub struct LegacyReport {
pub thread_id: ThreadId,
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
pub enum LegacyItemType {
Project,
diff --git a/apps/labrinth/src/models/v2/search.rs b/apps/labrinth/src/models/v2/search.rs
index 392dac7d17..c105fa3dc1 100644
--- a/apps/labrinth/src/models/v2/search.rs
+++ b/apps/labrinth/src/models/v2/search.rs
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::{routes::v2_reroute, search::ResultSearchProject};
-#[derive(Serialize, Deserialize, Debug)]
+#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)]
pub struct LegacySearchResults {
pub hits: Vec,
pub offset: usize,
@@ -11,7 +11,7 @@ pub struct LegacySearchResults {
pub total_hits: usize,
}
-#[derive(Serialize, Deserialize, Debug, Clone)]
+#[derive(Serialize, Deserialize, Debug, Clone, utoipa::ToSchema)]
pub struct LegacyResultSearchProject {
pub project_id: String,
pub project_type: String,
diff --git a/apps/labrinth/src/models/v2/teams.rs b/apps/labrinth/src/models/v2/teams.rs
index f265b77013..db7f2b9a7a 100644
--- a/apps/labrinth/src/models/v2/teams.rs
+++ b/apps/labrinth/src/models/v2/teams.rs
@@ -8,7 +8,7 @@ use crate::models::{
};
/// A member of a team
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct LegacyTeamMember {
pub role: String,
// is_owner removed, and role hardcoded to Owner if true,
diff --git a/apps/labrinth/src/models/v2/threads.rs b/apps/labrinth/src/models/v2/threads.rs
index 9a041277b8..ddaa6d0729 100644
--- a/apps/labrinth/src/models/v2/threads.rs
+++ b/apps/labrinth/src/models/v2/threads.rs
@@ -7,7 +7,7 @@ use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct LegacyThread {
pub id: ThreadId,
#[serde(rename = "type")]
@@ -18,7 +18,7 @@ pub struct LegacyThread {
pub members: Vec,
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct LegacyThreadMessage {
pub id: ThreadMessageId,
pub author_id: Option,
@@ -26,7 +26,7 @@ pub struct LegacyThreadMessage {
pub created: DateTime,
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LegacyMessageBody {
Text {
@@ -49,7 +49,9 @@ pub enum LegacyMessageBody {
},
}
-#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
+#[derive(
+ Serialize, Deserialize, Eq, PartialEq, Copy, Clone, utoipa::ToSchema,
+)]
#[serde(rename_all = "snake_case")]
pub enum LegacyThreadType {
Report,
diff --git a/apps/labrinth/src/models/v2/user.rs b/apps/labrinth/src/models/v2/user.rs
index f8e9d0d0db..d8fae78f38 100644
--- a/apps/labrinth/src/models/v2/user.rs
+++ b/apps/labrinth/src/models/v2/user.rs
@@ -6,7 +6,7 @@ use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
-#[derive(Serialize, Deserialize, Clone, Debug)]
+#[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
pub struct LegacyUser {
pub id: UserId,
pub username: String,
diff --git a/apps/labrinth/src/models/v3/billing.rs b/apps/labrinth/src/models/v3/billing.rs
index b7dd2a00ba..c3ef13facb 100644
--- a/apps/labrinth/src/models/v3/billing.rs
+++ b/apps/labrinth/src/models/v3/billing.rs
@@ -75,7 +75,17 @@ impl Price {
}
}
-#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)]
+#[derive(
+ Serialize,
+ Deserialize,
+ Hash,
+ Eq,
+ PartialEq,
+ Debug,
+ Copy,
+ Clone,
+ utoipa::ToSchema,
+)]
#[serde(rename_all = "kebab-case")]
pub enum PriceDuration {
FiveDays,
diff --git a/apps/labrinth/src/models/v3/moderation_notes.rs b/apps/labrinth/src/models/v3/moderation_notes.rs
index 8e972a1379..b6621dc8ea 100644
--- a/apps/labrinth/src/models/v3/moderation_notes.rs
+++ b/apps/labrinth/src/models/v3/moderation_notes.rs
@@ -28,7 +28,7 @@ impl From for ModerationNote {
}
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
pub struct PatchModerationNote {
pub notes: Option,
pub user_rating: Option,
diff --git a/apps/labrinth/src/models/v3/notifications.rs b/apps/labrinth/src/models/v3/notifications.rs
index ebef09548f..75904061d1 100644
--- a/apps/labrinth/src/models/v3/notifications.rs
+++ b/apps/labrinth/src/models/v3/notifications.rs
@@ -706,7 +706,7 @@ impl From for Notification {
}
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct NotificationAction {
pub name: String,
/// The route to call when this notification action is called. Formatted HTTP Method, route
diff --git a/apps/labrinth/src/models/v3/oauth_clients.rs b/apps/labrinth/src/models/v3/oauth_clients.rs
index 6790bb1f7e..efaddc0544 100644
--- a/apps/labrinth/src/models/v3/oauth_clients.rs
+++ b/apps/labrinth/src/models/v3/oauth_clients.rs
@@ -10,14 +10,14 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct OAuthRedirectUri {
pub id: OAuthRedirectUriId,
pub client_id: OAuthClientId,
pub uri: String,
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct OAuthClientCreationResult {
#[serde(flatten)]
pub client: OAuthClient,
@@ -25,7 +25,7 @@ pub struct OAuthClientCreationResult {
pub client_secret: String,
}
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct OAuthClient {
pub id: OAuthClientId,
pub name: String,
@@ -48,7 +48,7 @@ pub struct OAuthClient {
pub description: Option,
}
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct OAuthClientAuthorization {
pub id: OAuthClientAuthorizationId,
pub app_id: OAuthClientId,
diff --git a/apps/labrinth/src/models/v3/organizations.rs b/apps/labrinth/src/models/v3/organizations.rs
index aaa8c3a5e9..f7fa699bec 100644
--- a/apps/labrinth/src/models/v3/organizations.rs
+++ b/apps/labrinth/src/models/v3/organizations.rs
@@ -4,7 +4,7 @@ use crate::models::ids::{OrganizationId, TeamId};
use serde::{Deserialize, Serialize};
/// An organization of users who control a project
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Organization {
/// The id of the organization
pub id: OrganizationId,
diff --git a/apps/labrinth/src/models/v3/payouts.rs b/apps/labrinth/src/models/v3/payouts.rs
index 574dde676d..221205e95c 100644
--- a/apps/labrinth/src/models/v3/payouts.rs
+++ b/apps/labrinth/src/models/v3/payouts.rs
@@ -234,7 +234,7 @@ impl PayoutStatus {
}
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct PayoutMethod {
pub id: String,
#[serde(rename = "type")]
@@ -252,7 +252,7 @@ pub struct PayoutMethod {
pub exchange_rate: Option,
}
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PayoutMethodFee {
#[serde(with = "rust_decimal::serde::float")]
pub percentage: Decimal,
@@ -274,6 +274,17 @@ impl PayoutMethodFee {
#[derive(Clone)]
pub struct PayoutDecimal(pub Decimal);
+impl utoipa::PartialSchema for PayoutDecimal {
+ fn schema() -> utoipa::openapi::RefOr {
+ utoipa::openapi::ObjectBuilder::new()
+ .schema_type(utoipa::openapi::schema::Type::Number)
+ .build()
+ .into()
+ }
+}
+
+impl utoipa::ToSchema for PayoutDecimal {}
+
impl Serialize for PayoutDecimal {
fn serialize(&self, serializer: S) -> Result
where
@@ -293,7 +304,7 @@ impl<'de> Deserialize<'de> for PayoutDecimal {
}
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PayoutInterval {
Standard {
diff --git a/apps/labrinth/src/models/v3/shared_instances.rs b/apps/labrinth/src/models/v3/shared_instances.rs
index abbf773f93..a97003ad1e 100644
--- a/apps/labrinth/src/models/v3/shared_instances.rs
+++ b/apps/labrinth/src/models/v3/shared_instances.rs
@@ -38,7 +38,7 @@ impl SharedInstance {
}
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SharedInstanceVersion {
pub id: SharedInstanceVersionId,
pub shared_instance: SharedInstanceId,
diff --git a/apps/labrinth/src/models/v3/teams.rs b/apps/labrinth/src/models/v3/teams.rs
index b579eaacb4..97d866f5ac 100644
--- a/apps/labrinth/src/models/v3/teams.rs
+++ b/apps/labrinth/src/models/v3/teams.rs
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
pub const DEFAULT_ROLE: &str = "Member";
/// A team of users who control a project
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct Team {
/// The id of the team
pub id: TeamId,
@@ -157,7 +157,7 @@ impl OrganizationPermissions {
}
/// A member of a team
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
pub struct TeamMember {
/// The ID of the team this team member is a member of
pub team_id: TeamId,
diff --git a/apps/labrinth/src/models/v3/users.rs b/apps/labrinth/src/models/v3/users.rs
index 3bea96cde2..456faa87ea 100644
--- a/apps/labrinth/src/models/v3/users.rs
+++ b/apps/labrinth/src/models/v3/users.rs
@@ -242,7 +242,7 @@ impl Role {
}
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserFriend {
// The user who accepted the friend request
pub id: UserId,
diff --git a/apps/labrinth/src/routes/analytics.rs b/apps/labrinth/src/routes/analytics.rs
index ed25ef45aa..1b321870fa 100644
--- a/apps/labrinth/src/routes/analytics.rs
+++ b/apps/labrinth/src/routes/analytics.rs
@@ -46,19 +46,20 @@ pub const FILTERED_HEADERS: &[&str] = &[
"x-vercel-ip-country",
];
-pub fn config(cfg: &mut web::ServiceConfig) {
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(page_view_ingest)
.service(playtime_ingest)
.service(minecraft_server_play_ingest);
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
pub struct UrlInput {
url: String,
}
//this route should be behind the cloudflare WAF to prevent non-browsers from calling it
-#[post("view")]
+#[utoipa::path(tag = "analytics", responses((status = NO_CONTENT)))]
+#[post("/view")]
async fn page_view_ingest(
req: HttpRequest,
analytics_queue: web::Data>,
@@ -171,7 +172,7 @@ async fn page_view_ingest(
Ok(HttpResponse::NoContent().body(""))
}
-#[derive(Deserialize, Debug)]
+#[derive(Deserialize, Debug, utoipa::ToSchema)]
pub struct PlaytimeInput {
seconds: u16,
loader: String,
@@ -179,7 +180,8 @@ pub struct PlaytimeInput {
parent: Option,
}
-#[post("playtime")]
+#[utoipa::path(tag = "analytics", responses((status = NO_CONTENT)))]
+#[post("/playtime")]
async fn playtime_ingest(
req: HttpRequest,
analytics_queue: web::Data>,
@@ -249,7 +251,7 @@ struct MinecraftProfile {
name: String,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
pub struct MinecraftJavaServerPlayInput {
project_id: ProjectId,
username: String,
@@ -258,7 +260,8 @@ pub struct MinecraftJavaServerPlayInput {
pub const MINECRAFT_SERVER_PLAYS: &str = "minecraft_server_plays";
-#[post("minecraft-server-play")]
+#[utoipa::path(tag = "analytics", responses((status = NO_CONTENT)))]
+#[post("/minecraft-server-play")]
async fn minecraft_server_play_ingest(
req: HttpRequest,
analytics_queue: web::Data>,
@@ -355,3 +358,12 @@ async fn minecraft_server_play_ingest(
Ok(())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ page_view_ingest,
+ playtime_ingest,
+ minecraft_server_play_ingest,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/debug/mod.rs b/apps/labrinth/src/routes/debug/mod.rs
index 074beb6fb2..17c46037b8 100644
--- a/apps/labrinth/src/routes/debug/mod.rs
+++ b/apps/labrinth/src/routes/debug/mod.rs
@@ -1,5 +1,6 @@
use std::time::Duration;
+use actix_web::web;
use eyre::Context;
use eyre::eyre;
use prometheus::IntGauge;
@@ -9,21 +10,17 @@ use crate::util::cors::default_cors;
#[cfg(target_os = "linux")]
mod pprof;
-pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
- cfg.service(
- utoipa_actix_web::scope("/debug")
- .wrap(default_cors())
- .configure({
- #[cfg(target_os = "linux")]
- {
- pprof::config
- }
- #[cfg(not(target_os = "linux"))]
- {
- |_cfg| ()
- }
- }),
- );
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
+ cfg.service(web::scope("/debug").wrap(default_cors()).configure({
+ #[cfg(target_os = "linux")]
+ {
+ pprof::config
+ }
+ #[cfg(not(target_os = "linux"))]
+ {
+ |_cfg| ()
+ }
+ }));
}
pub fn register_and_set_metrics(
diff --git a/apps/labrinth/src/routes/debug/pprof.rs b/apps/labrinth/src/routes/debug/pprof.rs
index 5b583af383..3948a05a60 100644
--- a/apps/labrinth/src/routes/debug/pprof.rs
+++ b/apps/labrinth/src/routes/debug/pprof.rs
@@ -5,11 +5,19 @@ use eyre::{Context, eyre};
use prometheus::{IntGauge, Registry};
use std::time::Duration;
-pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(heap).service(flame_graph);
}
-#[utoipa::path]
+/// Get a heap profile.
+#[utoipa::path(
+ tag = "debug",
+ responses((
+ status = OK,
+ body = Vec,
+ content_type = "application/octet-stream"
+ ))
+)]
#[get("/pprof/heap", guard = "admin_key_guard")]
pub async fn heap() -> Result {
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
@@ -23,7 +31,11 @@ pub async fn heap() -> Result {
.body(pprof))
}
-#[utoipa::path]
+/// Get a heap flame graph.
+#[utoipa::path(
+ tag = "debug",
+ responses((status = OK, body = String, content_type = "image/svg+xml"))
+)]
#[get("/pprof/heap/flamegraph", guard = "admin_key_guard")]
pub async fn flame_graph() -> Result {
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
@@ -100,3 +112,8 @@ pub fn register_and_set_metrics(registry: &Registry) -> eyre::Result<()> {
Ok(())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(heap, flame_graph,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/admin.rs b/apps/labrinth/src/routes/internal/admin.rs
index 00eb793568..0f9b9f8863 100644
--- a/apps/labrinth/src/routes/internal/admin.rs
+++ b/apps/labrinth/src/routes/internal/admin.rs
@@ -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,9 @@ async fn resolve_download_attribution_version(
}
// This is an internal route, cannot be used without key
+/// Count a download.
#[utoipa::path(
+ tag = "v2 admin",
patch,
operation_id = "countDownload",
responses(
@@ -308,7 +310,9 @@ pub async fn count_download(
Ok(HttpResponse::NoContent().body(""))
}
+/// Reindex all projects.
#[utoipa::path(
+ tag = "v2 admin",
post,
operation_id = "forceReindex",
responses(
@@ -330,7 +334,9 @@ pub async fn force_reindex(
Ok(HttpResponse::NoContent().finish())
}
+/// Reindex a project.
#[utoipa::path(
+ tag = "v2 admin",
post,
operation_id = "forceReindexProject",
responses(
@@ -359,3 +365,16 @@ pub async fn force_reindex_project(
Ok(HttpResponse::NoContent().finish())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(count_download, force_reindex, force_reindex_project,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ nest(
+ (path = "/admin", api = RouteDoc),
+ )
+)]
+pub(crate) struct ApiDoc;
diff --git a/apps/labrinth/src/routes/internal/affiliate.rs b/apps/labrinth/src/routes/internal/affiliate.rs
index 5dca27b38f..aac12eb024 100644
--- a/apps/labrinth/src/routes/internal/affiliate.rs
+++ b/apps/labrinth/src/routes/internal/affiliate.rs
@@ -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,7 +40,11 @@ pub struct IngestClick {
pub affiliate_code_id: AffiliateCodeId,
}
-#[utoipa::path]
+/// Ingest an affiliate click.
+#[utoipa::path(
+ tag = "affiliates",
+ responses((status = NO_CONTENT))
+)]
#[post("/ingest-click")]
async fn ingest_click(
req: HttpRequest,
@@ -136,7 +140,9 @@ async fn ingest_click(
Ok(())
}
+/// List affiliate codes.
#[utoipa::path(
+ tag = "affiliates",
responses((status = OK, body = inline(Vec)))
)]
#[get("")]
@@ -187,7 +193,9 @@ pub struct CreateRequest {
pub source_name: String,
}
+/// Create an affiliate code.
#[utoipa::path(
+ tag = "affiliates",
responses((status = OK, body = inline(AffiliateCode)))
)]
#[put("")]
@@ -263,7 +271,9 @@ async fn create(
Ok(web::Json(AffiliateCode::from(code, is_admin)))
}
+/// Get an affiliate code.
#[utoipa::path(
+ tag = "affiliates",
responses((status = OK, body = inline(AffiliateCode)))
)]
#[get("/{id}")]
@@ -302,7 +312,11 @@ async fn get(
}
}
-#[utoipa::path]
+/// Delete an affiliate code.
+#[utoipa::path(
+ tag = "affiliates",
+ responses((status = NO_CONTENT))
+)]
#[delete("/{id}")]
async fn delete(
req: HttpRequest,
@@ -350,7 +364,11 @@ pub struct PatchRequest {
pub source_name: String,
}
-#[utoipa::path]
+/// Update an affiliate code.
+#[utoipa::path(
+ tag = "affiliates",
+ responses((status = NO_CONTENT))
+)]
#[patch("/{id}")]
async fn patch(
req: HttpRequest,
@@ -399,3 +417,8 @@ async fn patch(
Ok(())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(ingest_click, get_all, create, get, delete, patch,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs
index 7a0f37afcb..3063e08598 100644
--- a/apps/labrinth/src/routes/internal/attribution.rs
+++ b/apps/labrinth/src/routes/internal/attribution.rs
@@ -26,7 +26,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)
@@ -34,7 +34,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,
@@ -45,7 +45,7 @@ struct AttributionGroupResponse {
versions: Vec,
}
-#[derive(Clone, Serialize)]
+#[derive(Clone, Serialize, utoipa::ToSchema)]
struct VersionInfo {
id: VersionId,
name: String,
@@ -53,7 +53,7 @@ struct VersionInfo {
date_created: chrono::DateTime,
}
-#[derive(Serialize)]
+#[derive(Serialize, utoipa::ToSchema)]
struct AttributionFileResponse {
name: String,
sha1: String,
@@ -64,7 +64,7 @@ struct AttributionFileResponse {
moderation_external_license: Option,
}
-#[derive(Clone, Serialize)]
+#[derive(Clone, Serialize, utoipa::ToSchema)]
struct ModerationExternalLicenseResponse {
id: i64,
title: Option,
@@ -89,7 +89,11 @@ struct ScanResponse {
queued_files: u64,
}
-#[utoipa::path]
+/// Queue an attribution scan.
+#[utoipa::path(
+ tag = "attribution",
+ responses((status = OK, body = ScanResponse))
+)]
#[post("/scan")]
async fn scan(
req: HttpRequest,
@@ -201,7 +205,12 @@ async fn scan(
}))
}
-#[utoipa::path]
+/// List project attribution groups.
+#[utoipa::path(
+ tag = "attribution",
+ params(("project_id" = ProjectId, Path)),
+ responses((status = OK, body = inline(Vec)))
+)]
#[get("/{project_id}")]
async fn list(
req: HttpRequest,
@@ -451,7 +460,11 @@ struct UpdateGroupBody {
attribution: AttributionResolution,
}
-#[utoipa::path]
+/// Update an attribution group.
+#[utoipa::path(
+ tag = "attribution",
+ responses((status = NO_CONTENT))
+)]
#[patch("/group/{group_id}")]
async fn update_group(
req: HttpRequest,
@@ -532,7 +545,11 @@ struct AssignBody {
project_id: ProjectId,
}
-#[utoipa::path]
+/// Move a file to an attribution group.
+#[utoipa::path(
+ tag = "attribution",
+ responses((status = NO_CONTENT))
+)]
#[post("/assign")]
async fn assign(
req: HttpRequest,
@@ -688,7 +705,11 @@ struct SplitBody {
project_id: ProjectId,
}
-#[utoipa::path]
+/// Split a file into a new attribution group.
+#[utoipa::path(
+ tag = "attribution",
+ responses((status = NO_CONTENT))
+)]
#[post("/split")]
async fn split(
req: HttpRequest,
@@ -966,3 +987,8 @@ fn hex_to_bytes(hex: &str) -> Option> {
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
.collect()
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(scan, list, update_group, assign, split,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs
index 6d3f8f6ec8..ad06707625 100644
--- a/apps/labrinth/src/routes/internal/billing.rs
+++ b/apps/labrinth/src/routes/internal/billing.rs
@@ -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,17 @@ 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(
+ tag = "billing",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[get("/products")]
pub async fn products(
pool: web::Data,
redis: web::Data,
@@ -99,7 +105,12 @@ struct SubscriptionsQuery {
pub user_id: Option,
}
-#[get("subscriptions")]
+/// List subscriptions.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[get("/subscriptions")]
pub async fn subscriptions(
req: HttpRequest,
pool: web::Data,
@@ -141,7 +152,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 +160,19 @@ pub enum ChargeRefundAmount {
None,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
pub struct ChargeRefund {
#[serde(flatten)]
pub amount: ChargeRefundAmount,
pub unprovision: Option,
}
-#[post("charge/{id}/refund")]
+/// Refund a charge.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = NO_CONTENT))
+)]
+#[post("/charge/{id}/refund")]
#[allow(clippy::too_many_arguments)]
pub async fn refund_charge(
req: HttpRequest,
@@ -419,7 +435,12 @@ pub async fn refund_charge(
Ok(HttpResponse::NoContent().finish())
}
-#[post("charge/{id}/tax/reprocess")]
+/// Reprocess tax for a charge.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = NO_CONTENT))
+)]
+#[post("/charge/{id}/tax/reprocess")]
pub async fn reprocess_charge_tax(
req: HttpRequest,
pool: web::Data,
@@ -587,8 +608,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,
pub payment_method: Option,
pub cancelled: Option,
@@ -601,7 +623,15 @@ pub struct SubscriptionEditQuery {
pub dry: Option,
}
-#[patch("subscription/{id}")]
+/// Update a subscription.
+#[utoipa::path(
+ tag = "billing",
+ 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 +1121,12 @@ pub async fn edit_subscription(
}
}
-#[get("customer")]
+/// Get the current customer.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[get("/customer")]
pub async fn user_customer(
req: HttpRequest,
pool: web::Data,
@@ -1129,7 +1164,12 @@ pub struct ChargesQuery {
pub user_id: Option,
}
-#[get("payments")]
+/// List payments.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[get("/payments")]
pub async fn charges(
req: HttpRequest,
pool: web::Data,
@@ -1188,7 +1228,12 @@ pub async fn charges(
))
}
-#[post("payment_method")]
+/// Start a payment method flow.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[post("/payment_method")]
pub async fn add_payment_method_flow(
req: HttpRequest,
pool: web::Data,
@@ -1241,7 +1286,12 @@ pub struct EditPaymentMethod {
pub primary: bool,
}
-#[patch("payment_method/{id}")]
+/// Update a payment method.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = NO_CONTENT))
+)]
+#[patch("/payment_method/{id}")]
pub async fn edit_payment_method(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1305,7 +1355,12 @@ pub async fn edit_payment_method(
}
}
-#[delete("payment_method/{id}")]
+/// Remove a payment method.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = NO_CONTENT))
+)]
+#[delete("/payment_method/{id}")]
pub async fn remove_payment_method(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1388,7 +1443,15 @@ pub async fn remove_payment_method(
}
}
-#[get("payment_methods")]
+/// List payment methods.
+#[utoipa::path(
+ 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,
@@ -1432,7 +1495,22 @@ pub struct ActiveServersQuery {
pub subscription_status: Option,
}
-#[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,
+}
+
+/// List active servers.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = OK, body = inline(Vec)))
+)]
+#[get("/active_servers")]
pub async fn active_servers(
req: HttpRequest,
pool: web::Data,
@@ -1457,21 +1535,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,
- }
-
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 +1551,12 @@ pub async fn active_servers(
SubscriptionMetadata::Medal { .. } => None,
})
})
- .collect::>();
+ .collect::>();
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 +1576,7 @@ impl PaymentRequestType {
}
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChargeRequestType {
Existing {
@@ -1515,11 +1584,12 @@ pub enum ChargeRequestType {
},
New {
product_id: crate::models::ids::ProductId,
+ #[schema(value_type = String)]
interval: Option,
},
}
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct PaymentRequestMetadata {
#[serde(flatten)]
@@ -1527,7 +1597,7 @@ pub struct PaymentRequestMetadata {
pub affiliate_code: Option,
}
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PaymentRequestMetadataKind {
Pyro {
@@ -1537,16 +1607,22 @@ 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,
pub metadata: Option,
}
-#[post("payment")]
+/// Initiate a payment.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[post("/payment")]
pub async fn initiate_payment(
req: HttpRequest,
pool: web::Data,
@@ -1610,7 +1686,12 @@ pub async fn initiate_payment(
}
}
-#[post("_stripe")]
+/// Receive a Stripe webhook.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = NO_CONTENT))
+)]
+#[post("/_stripe")]
pub async fn stripe_webhook(
req: HttpRequest,
payload: String,
@@ -2503,7 +2584,7 @@ async fn apply_credit_many(
Ok(())
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
pub struct CreditRequest {
#[serde(flatten)]
pub target: CreditTarget,
@@ -2512,7 +2593,7 @@ pub struct CreditRequest {
pub message: String,
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
#[serde(untagged)]
pub enum CreditTarget {
Subscriptions {
@@ -2526,7 +2607,12 @@ pub enum CreditTarget {
},
}
-#[post("credit")]
+/// Credit subscriptions.
+#[utoipa::path(
+ tag = "billing",
+ responses((status = NO_CONTENT))
+)]
+#[post("/credit")]
pub async fn credit(
req: HttpRequest,
pool: web::Data,
@@ -2649,3 +2735,24 @@ pub async fn credit(
}
pub mod payments;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ products,
+ subscriptions,
+ refund_charge,
+ reprocess_charge_tax,
+ edit_subscription,
+ user_customer,
+ charges,
+ add_payment_method_flow,
+ edit_payment_method,
+ remove_payment_method,
+ payment_methods,
+ active_servers,
+ initiate_payment,
+ stripe_webhook,
+ credit,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/campaign.rs b/apps/labrinth/src/routes/internal/campaign.rs
index 6bac2eb4cf..2d25486cdb 100644
--- a/apps/labrinth/src/routes/internal/campaign.rs
+++ b/apps/labrinth/src/routes/internal/campaign.rs
@@ -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,11 @@ impl CampaignDonation {
}
}
-#[utoipa::path]
+/// Receive a Tiltify webhook.
+#[utoipa::path(
+ tag = "campaigns",
+ responses((status = NO_CONTENT))
+)]
#[post("/webhook")]
pub async fn tiltify_webhook(
req: HttpRequest,
@@ -301,7 +305,11 @@ fn verify_tiltify_webhook_signature(
Ok(())
}
-#[utoipa::path]
+/// Get Pride campaign data.
+#[utoipa::path(
+ tag = "campaigns",
+ responses((status = OK, body = CampaignInfo))
+)]
#[get("/pride-26")]
pub async fn pride_26(
http: web::Data,
@@ -472,3 +480,8 @@ async fn amount_raised_usd(
Ok(amount.value / usd_to_currency)
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(tiltify_webhook, pride_26,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs
index 5a9b403428..fb1e3659c5 100644
--- a/apps/labrinth/src/routes/internal/delphi/mod.rs
+++ b/apps/labrinth/src/routes/internal/delphi/mod.rs
@@ -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,7 +141,12 @@ pub struct DelphiRunParameters {
pub file_id: crate::models::ids::FileId,
}
-#[post("ingest", guard = "admin_key_guard")]
+/// Ingest a Delphi report.
+#[utoipa::path(
+ tag = "delphi",
+ responses((status = NO_CONTENT))
+)]
+#[post("/ingest", guard = "admin_key_guard")]
async fn ingest_report(
pool: web::Data,
redis: web::Data,
@@ -466,7 +471,12 @@ pub async fn send_tech_review_exit_file_deleted_message_if_exited(
Ok(())
}
-#[post("run")]
+/// Run Delphi.
+#[utoipa::path(
+ tag = "delphi",
+ responses((status = NO_CONTENT))
+)]
+#[post("/run")]
async fn _run(
req: HttpRequest,
pool: web::Data,
@@ -487,7 +497,12 @@ async fn _run(
run(&**pool, run_parameters.into_inner(), &http).await
}
-#[get("version")]
+/// Get the Delphi version.
+#[utoipa::path(
+ tag = "delphi",
+ responses((status = OK, body = inline(Option)))
+)]
+#[get("/version")]
async fn version(
req: HttpRequest,
pool: web::Data,
@@ -510,7 +525,12 @@ async fn version(
))
}
-#[get("issue_type/schema")]
+/// Get the Delphi issue type schema.
+#[utoipa::path(
+ tag = "delphi",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[get("/issue_type/schema")]
async fn issue_type_schema(
req: HttpRequest,
pool: web::Data,
@@ -556,3 +576,8 @@ async fn issue_type_schema(
)),
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(ingest_report, _run, version, issue_type_schema,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs
index 942af86166..c674df43c3 100644
--- a/apps/labrinth/src/routes/internal/external_notifications.rs
+++ b/apps/labrinth/src/routes/internal/external_notifications.rs
@@ -26,20 +26,26 @@ use eyre::eyre;
use lettre::message::Mailbox;
use serde::Deserialize;
-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)]
+#[derive(Deserialize, utoipa::ToSchema)]
struct CreateNotification {
+ #[schema(value_type = serde_json::Value)]
pub body: NotificationBody,
pub user_ids: Vec,
}
-#[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,
redis: web::Data,
@@ -74,13 +80,21 @@ pub async fn create(
Ok(HttpResponse::Accepted().finish())
}
-/// Inserts notifications for all users and tries to send emails immediately.
+/// 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)),
+ (status = 207, body = inline(Vec)),
+ )
+)]
#[post(
- "external_notifications/email-sync",
+ "/external_notifications/email-sync",
guard = "external_notification_key_guard"
)]
pub async fn create_email_sync(
@@ -178,14 +192,19 @@ pub async fn create_email_sync(
Ok(web::Json(failed).customize().with_status(status))
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
struct NotificationFilter {
pub user_ids: Vec,
#[serde(flatten)]
pub body: serde_json::Map,
}
-#[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,
redis: web::Data,
@@ -225,7 +244,7 @@ pub async fn remove(
Ok(HttpResponse::NoContent().finish())
}
-#[derive(Deserialize)]
+#[derive(Deserialize, utoipa::ToSchema)]
struct SendEmail {
pub users: Vec,
pub key: String,
@@ -233,7 +252,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,
@@ -339,3 +363,8 @@ async fn broadcast_notifications(
}
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(create, create_email_sync, remove, send_custom_email,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs
index 8a82b3dc8d..fd51ded61f 100644
--- a/apps/labrinth/src/routes/internal/flows.rs
+++ b/apps/labrinth/src/routes/internal/flows.rs
@@ -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)
@@ -1067,14 +1067,7 @@ 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")
- )
-)]
+#[utoipa::path(tag = "auth", responses((status = TEMPORARY_REDIRECT), (status = OK)))]
#[get("/init")]
pub async fn init(
req: HttpRequest,
@@ -1165,14 +1158,7 @@ pub async fn init(
.json(serde_json::json!({ "url": url })))
}
-#[utoipa::path(
- get,
- operation_id = "authCallback",
- responses(
- (status = 307, description = "Redirect with auth code"),
- (status = 401, description = "Authentication failed")
- )
-)]
+#[utoipa::path(tag = "auth", responses((status = OK)))]
#[get("/callback")]
pub async fn auth_callback(
req: HttpRequest,
@@ -1441,13 +1427,15 @@ struct NewOAuthAccount {
pub sign_up_newsletter: bool,
}
+/// Create account with OAuth.
#[utoipa::path(
+ 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(
@@ -1525,7 +1513,9 @@ struct DiscordCommunityHandoffPayload {
nonce: String,
}
+/// Link Discord community.
#[utoipa::path(
+ tag = "auth",
operation_id = "discordCommunityLink",
responses(
(status = 200, description = "Discord community bot handoff URL", body = DiscordCommunityLinkResponse),
@@ -1599,7 +1589,9 @@ pub async fn discord_community_link(
Ok(web::Json(DiscordCommunityLinkResponse { url }))
}
+/// Remove an auth provider.
#[utoipa::path(
+ tag = "auth",
delete,
operation_id = "deleteAuthProvider",
responses(
@@ -1939,11 +1931,13 @@ impl ReadyAccountRegisterFlow {
}
}
+/// Validate password account creation.
#[utoipa::path(
+ 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 +1958,15 @@ pub async fn validate_create_account_with_password(
Ok(())
}
+/// Create account with a password.
#[utoipa::path(
+ 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 +2006,15 @@ pub struct Login {
pub challenge: String,
}
+/// Log in with a password.
#[utoipa::path(
+ 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 +2166,15 @@ async fn validate_2fa_code(
}
}
+/// Complete login with 2FA.
#[utoipa::path(
+ 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 +2225,15 @@ pub async fn login_2fa(
}
}
+/// Start 2FA setup.
#[utoipa::path(
+ 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 +2275,15 @@ pub async fn begin_2fa_flow(
}
}
+/// Finish 2FA setup.
#[utoipa::path(
+ 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 +2409,9 @@ pub struct Remove2FA {
pub code: String,
}
+/// Remove 2FA.
#[utoipa::path(
+ tag = "auth",
delete,
operation_id = "remove2fa",
responses(
@@ -2502,7 +2508,9 @@ pub struct ResetPassword {
pub challenge: String,
}
+/// Start password reset.
#[utoipa::path(
+ tag = "auth",
post,
operation_id = "resetPasswordBegin",
responses(
@@ -2605,7 +2613,9 @@ pub struct ChangePassword {
pub new_password: Option,
}
+/// Change password.
#[utoipa::path(
+ tag = "auth",
patch,
operation_id = "changePassword",
responses(
@@ -2768,7 +2778,9 @@ pub struct SetEmail {
pub email: String,
}
+/// Set email address.
#[utoipa::path(
+ tag = "auth",
patch,
operation_id = "setEmail",
responses(
@@ -2887,7 +2899,9 @@ pub async fn set_email(
Ok(HttpResponse::Ok().finish())
}
+/// Resend verification email.
#[utoipa::path(
+ tag = "auth",
post,
operation_id = "resendVerifyEmail",
responses(
@@ -2959,7 +2973,9 @@ pub struct VerifyEmail {
pub flow: String,
}
+/// Verify email address.
#[utoipa::path(
+ tag = "auth",
post,
operation_id = "verifyEmail",
responses(
@@ -3022,7 +3038,9 @@ pub async fn verify_email(
}
}
+/// Subscribe to the newsletter.
#[utoipa::path(
+ tag = "auth",
post,
operation_id = "subscribeNewsletter",
responses(
@@ -3068,13 +3086,15 @@ pub async fn subscribe_newsletter(
Ok(HttpResponse::NoContent().finish())
}
+/// Get newsletter subscription status.
#[utoipa::path(
+ 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 +3135,9 @@ pub struct RegisterPasskeyResponse {
pub flow: String,
}
+/// Start passkey registration.
#[utoipa::path(
+ tag = "auth",
post,
operation_id = "registerPasskeyStart",
responses(
@@ -3212,7 +3234,9 @@ pub struct PasskeyResponse {
pub last_used: Option>,
}
+/// Finish passkey registration.
#[utoipa::path(
+ tag = "auth",
post,
operation_id = "registerPasskeyFinish",
responses(
@@ -3318,7 +3342,9 @@ pub struct AuthenticatePasskeyResponse {
pub flow: String,
}
+/// Start passkey authentication.
#[utoipa::path(
+ tag = "auth",
post,
operation_id = "authenticatePasskeyStart",
responses(
@@ -3358,13 +3384,15 @@ pub struct AuthenticatePasskeyFinish {
pub credential: PublicKeyCredential,
}
+/// Finish passkey authentication.
#[utoipa::path(
+ 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 +3500,9 @@ pub async fn authenticate_passkey_finish(
}
}
+/// List passkeys.
#[utoipa::path(
+ tag = "auth",
get,
operation_id = "listPasskeys",
responses(
@@ -3519,7 +3549,9 @@ pub struct RenamePasskey {
pub name: String,
}
+/// Rename a passkey.
#[utoipa::path(
+ tag = "auth",
patch,
operation_id = "renamePasskey",
responses(
@@ -3571,7 +3603,9 @@ pub async fn rename_passkey(
Ok(HttpResponse::NoContent().finish())
}
+/// Delete a passkey.
#[utoipa::path(
+ tag = "auth",
delete,
operation_id = "deletePasskey",
responses(
@@ -3616,3 +3650,43 @@ pub async fn delete_passkey(
transaction.commit().await?;
Ok(HttpResponse::NoContent().finish())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ init,
+ auth_callback,
+ create_oauth_account,
+ discord_community_link,
+ delete_auth_provider,
+ validate_create_account_with_password,
+ create_account_with_password,
+ login_password,
+ login_2fa,
+ begin_2fa_flow,
+ finish_2fa_flow,
+ remove_2fa,
+ reset_password_begin,
+ change_password,
+ set_email,
+ resend_verify_email,
+ verify_email,
+ subscribe_newsletter,
+ get_newsletter_subscription_status,
+ register_passkey_start,
+ register_passkey_finish,
+ authenticate_passkey_start,
+ authenticate_passkey_finish,
+ list_passkeys,
+ rename_passkey,
+ delete_passkey,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ nest(
+ (path = "/auth", api = RouteDoc),
+ )
+)]
+pub(crate) struct ApiDoc;
diff --git a/apps/labrinth/src/routes/internal/gdpr.rs b/apps/labrinth/src/routes/internal/gdpr.rs
index 2412cf33ef..e8f0082ec1 100644
--- a/apps/labrinth/src/routes/internal/gdpr.rs
+++ b/apps/labrinth/src/routes/internal/gdpr.rs
@@ -6,10 +6,15 @@ 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(
+ tag = "GDPR",
+ responses((status = OK, body = serde_json::Value))
+)]
#[post("/export")]
pub async fn export(
req: HttpRequest,
@@ -214,3 +219,8 @@ pub async fn export(
"subscriptions": subscriptions,
})))
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(export,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/globals.rs b/apps/labrinth/src/routes/internal/globals.rs
index 6dbb0c931d..c5b047493d 100644
--- a/apps/labrinth/src/routes/internal/globals.rs
+++ b/apps/labrinth/src/routes/internal/globals.rs
@@ -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,11 @@ 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(
+ tag = "globals",
+ responses((status = OK, body = Globals))
+)]
#[get("")]
pub async fn get_globals() -> web::Json {
web::Json(GLOBALS.clone())
@@ -118,3 +121,8 @@ mod tests {
assert_eq!(second, Some(Decimal::from(2000_u64)));
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(get_globals,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/gotenberg.rs b/apps/labrinth/src/routes/internal/gotenberg.rs
index 149b254535..8955344b81 100644
--- a/apps/labrinth/src/routes/internal/gotenberg.rs
+++ b/apps/labrinth/src/routes/internal/gotenberg.rs
@@ -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,
+ 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,
pub message: Option,
@@ -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,
@@ -227,3 +238,8 @@ impl header::Header for ModrinthPaymentId {
.map(|id| Self(PayoutId(id)))
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(success_callback, error_callback,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/medal.rs b/apps/labrinth/src/routes/internal/medal.rs
index 9c534a4823..02f92954e6 100644
--- a/apps/labrinth/src/routes/internal/medal.rs
+++ b/apps/labrinth/src/routes/internal/medal.rs
@@ -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,18 @@ 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(
+ tag = "medal",
+ responses((status = OK, body = VerifyResponse))
+)]
+#[post("/verify", guard = "medal_key_guard")]
pub async fn verify(
pool: web::Data,
web::Query(MedalQuery { username }): web::Query,
@@ -35,12 +46,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 +55,12 @@ pub async fn verify(
}
}
-#[post("redeem", guard = "medal_key_guard")]
+/// Redeem Medal credit.
+#[utoipa::path(
+ tag = "medal",
+ responses((status = ACCEPTED), (status = CREATED))
+)]
+#[post("/redeem", guard = "medal_key_guard")]
pub async fn redeem(
pool: web::Data,
redis: web::Data,
@@ -107,3 +117,8 @@ pub async fn redeem(
Ok(HttpResponse::Created().finish())
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(verify, redeem,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs
index 5bc6531856..b7244d9af3 100644
--- a/apps/labrinth/src/routes/internal/mod.rs
+++ b/apps/labrinth/src/routes/internal/mod.rs
@@ -21,97 +21,64 @@ pub mod statuses;
pub use super::ApiError;
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),
- );
-}
-
-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),
+ .configure(statuses::config),
)
.service(
- utoipa_actix_web::scope("/_internal/attribution")
+ web::scope("/v3/analytics-event")
.wrap(default_cors())
- .configure(attribution::config),
+ .configure(super::v3::analytics_event::config),
);
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ nest(
+ (path = "/_internal", api = admin::ApiDoc),
+ (path = "/_internal", api = session::ApiDoc),
+ (path = "/_internal", api = flows::ApiDoc),
+ (path = "/_internal", api = pats::RouteDoc),
+ (path = "/_internal", api = oauth_clients::ApiDoc),
+ (path = "/_internal/moderation", api = moderation::ApiDoc),
+ (path = "/_internal/affiliate", api = affiliate::RouteDoc),
+ (path = "/_internal/campaign", api = campaign::RouteDoc),
+ (path = "/_internal/search-management", api = search::RouteDoc),
+ (path = "/_internal/globals", api = globals::RouteDoc),
+ (path = "/_internal/server-ping", api = server_ping::RouteDoc),
+ (path = "/_internal/attribution", api = attribution::RouteDoc),
+ (path = "/_internal/billing", api = billing::RouteDoc),
+ (path = "/_internal/delphi", api = delphi::RouteDoc),
+ (path = "/_internal", api = external_notifications::RouteDoc),
+ (path = "/_internal/gdpr", api = gdpr::RouteDoc),
+ (path = "/_internal", api = gotenberg::RouteDoc),
+ (path = "/_internal/medal", api = medal::RouteDoc),
+ (path = "/_internal", api = mural::RouteDoc),
+ (path = "/_internal", api = statuses::RouteDoc),
+ (path = "/v3/analytics-event", api = super::v3::analytics_event::RouteDoc),
+ )
+)]
+pub struct ApiDoc;
diff --git a/apps/labrinth/src/routes/internal/moderation/external_license.rs b/apps/labrinth/src/routes/internal/moderation/external_license.rs
index 6ef567ee34..7c5167fe53 100644
--- a/apps/labrinth/src/routes/internal/moderation/external_license.rs
+++ b/apps/labrinth/src/routes/internal/moderation/external_license.rs
@@ -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,7 +329,11 @@ async fn fetch_by_flame_ids(
Ok(results)
}
-#[utoipa::path]
+/// Search external licenses.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = OK, body = inline(Vec)))
+)]
#[post("/search")]
async fn search(
req: HttpRequest,
@@ -393,7 +397,11 @@ async fn search(
Ok(web::Json(results))
}
-#[utoipa::path]
+/// Look up external license metadata.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = OK, body = ExternalLicenseLookupResponse))
+)]
#[post("/lookup")]
async fn lookup(
req: HttpRequest,
@@ -422,7 +430,11 @@ async fn lookup(
}))
}
-#[utoipa::path]
+/// Get external license by SHA-1.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = OK, body = ExternalProject))
+)]
#[get("/by-sha1/{sha1}")]
async fn get_by_sha1(
req: HttpRequest,
@@ -448,7 +460,11 @@ async fn get_by_sha1(
Ok(web::Json(result))
}
-#[utoipa::path]
+/// Get external licenses by SHA-1.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = OK, body = inline(HashMap)))
+)]
#[post("/by-sha1")]
async fn get_by_sha1_bulk(
req: HttpRequest,
@@ -472,7 +488,11 @@ async fn get_by_sha1_bulk(
Ok(web::Json(results))
}
-#[utoipa::path]
+/// Add an external license file.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = OK, body = ExternalProject))
+)]
#[post("/file")]
async fn add_file(
req: HttpRequest,
@@ -484,7 +504,11 @@ async fn add_file(
upsert_file_license(req, pool, redis, session_queue, body).await
}
-#[utoipa::path]
+/// Reassign an external license file.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = OK, body = ExternalProject))
+)]
#[post("/file/reassign")]
async fn reassign_file(
req: HttpRequest,
@@ -584,7 +608,11 @@ async fn upsert_file_license(
))
}
-#[utoipa::path]
+/// Update an external license.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = OK, body = ExternalProject))
+)]
#[patch("/{id}")]
async fn update_license(
req: HttpRequest,
@@ -654,3 +682,16 @@ async fn update_license(
.into_external_project(linked_files),
))
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ search,
+ lookup,
+ get_by_sha1,
+ get_by_sha1_bulk,
+ add_file,
+ reassign_file,
+ update_license,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs
index ddcc775a7e..ee04e9b962 100644
--- a/apps/labrinth/src/routes/internal/moderation/mod.rs
+++ b/apps/labrinth/src/routes/internal/moderation/mod.rs
@@ -25,7 +25,7 @@ mod external_license;
mod ownership;
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,8 +158,14 @@ pub struct DeleteAllLocksResponse {
pub deleted_count: u64,
}
-/// Fetch all projects which are in the moderation queue.
+/// List projects in the moderation queue.
#[utoipa::path(
+ tag = "moderation",
+ params(
+ ("count" = Option, Query),
+ ("offset" = Option, Query),
+ ("has_external_dependencies" = Option, Query)
+ ),
responses((status = OK, body = inline(Vec)))
)]
#[get("/projects")]
@@ -291,9 +293,10 @@ 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)))
+ tag = "moderation",
+ responses((status = OK, body = MissingMetadata))
)]
#[get("/project/{id}")]
async fn get_project_meta(
@@ -447,8 +450,11 @@ pub enum Judgement {
},
}
-/// Update moderation judgements for projects in the review queue.
-#[utoipa::path]
+/// Update project moderation judgements.
+#[utoipa::path(
+ tag = "moderation",
+ responses((status = NO_CONTENT))
+)]
#[post("/project")]
async fn set_project_meta(
req: HttpRequest,
@@ -536,9 +542,10 @@ 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(
+ tag = "moderation",
responses(
(status = OK, body = LockAcquireResponse),
(status = NOT_FOUND, description = "Project not found")
@@ -594,8 +601,9 @@ async fn acquire_lock(
}
}
-/// Force-acquire a moderation lock on a project (moderator override).
+/// Override a moderation lock.
#[utoipa::path(
+ tag = "moderation",
responses(
(status = OK, body = LockAcquireResponse),
(status = NOT_FOUND, description = "Project not found")
@@ -639,8 +647,9 @@ async fn override_lock(
}))
}
-/// Check the lock status for a project
+/// Get moderation lock status.
#[utoipa::path(
+ tag = "moderation",
responses(
(status = OK, body = LockStatusResponse),
(status = NOT_FOUND, description = "Project not found")
@@ -699,8 +708,9 @@ async fn get_lock_status(
}
}
-/// Release a moderation lock on a project
+/// Release a moderation lock.
#[utoipa::path(
+ tag = "moderation",
responses(
(status = OK, body = LockReleaseResponse),
(status = NOT_FOUND, description = "Project not found")
@@ -740,12 +750,13 @@ 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(
+ tag = "moderation",
request_body(
content = String,
description = "Token value (same as Authorization header)",
@@ -811,8 +822,9 @@ async fn release_lock_beacon(
Ok(web::Json(LockReleaseResponse { success: released }))
}
-/// Delete all moderation locks (admin only)
+/// Delete all moderation locks.
#[utoipa::path(
+ tag = "moderation",
responses(
(status = OK, body = DeleteAllLocksResponse),
(status = UNAUTHORIZED, description = "Not an admin")
@@ -845,3 +857,29 @@ async fn delete_all_locks(
Ok(web::Json(DeleteAllLocksResponse { deleted_count }))
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ get_projects,
+ get_project_meta,
+ set_project_meta,
+ acquire_lock,
+ override_lock,
+ get_lock_status,
+ release_lock,
+ release_lock_beacon,
+ delete_all_locks,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
+
+pub(crate) struct ApiDoc;
+
+impl utoipa::OpenApi for ApiDoc {
+ fn openapi() -> utoipa::openapi::OpenApi {
+ let openapi = RouteDoc::openapi();
+ openapi
+ .nest("/tech-review", tech_review::RouteDoc::openapi())
+ .nest("/external-license", external_license::RouteDoc::openapi())
+ }
+}
diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs
index cfa609a98e..eee7f1ab94 100644
--- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs
@@ -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,8 +193,9 @@ pub enum FlagReason {
Delphi,
}
-/// Get info on an issue in a Delphi report.
+/// Get a Delphi report issue.
#[utoipa::path(
+ tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(FileIssue)))
)]
@@ -252,8 +253,9 @@ 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(
+ tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(FileReport)))
)]
@@ -662,10 +664,11 @@ async fn fetch_project_reports(
Ok(project_reports)
}
-/// Searches all projects which are awaiting technical review.
+/// Search projects awaiting technical review.
#[utoipa::path(
+ tag = "moderation",
security(("bearer_auth" = [])),
- responses((status = OK, body = inline(Vec)))
+ responses((status = OK, body = SearchResponse))
)]
#[post("/search")]
async fn search_projects(
@@ -872,8 +875,9 @@ async fn search_projects(
}))
}
-/// Gets the technical review report for a specific project.
+/// Get a project technical review report.
#[utoipa::path(
+ tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(ProjectReportResponse)))
)]
@@ -963,13 +967,14 @@ pub struct SubmitReport {
pub message: Option,
}
-/// 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(
+ tag = "moderation",
security(("bearer_auth" = [])),
responses((status = NO_CONTENT))
)]
@@ -1163,11 +1168,12 @@ 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(
+ tag = "moderation",
security(("bearer_auth" = [])),
responses((status = NO_CONTENT))
)]
@@ -1273,9 +1279,12 @@ 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(
+ tag = "moderation",
+ responses((status = OK, body = DelphiReportId))
+)]
#[put("/report")]
async fn add_report(
req: HttpRequest,
@@ -1337,3 +1346,16 @@ async fn add_report(
Ok(web::Json(report_id))
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ get_issue,
+ get_report,
+ search_projects,
+ get_project_report,
+ submit_report,
+ update_issue_details,
+ add_report,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/mural.rs b/apps/labrinth/src/routes/internal/mural.rs
index d10ff4e90c..3cf8c7d2ca 100644
--- a/apps/labrinth/src/routes/internal/mural.rs
+++ b/apps/labrinth/src/routes/internal/mural.rs
@@ -6,10 +6,15 @@ 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(
payouts_queue: web::Data,
@@ -26,3 +31,8 @@ async fn get_bank_details(
.wrap_internal_err("failed to fetch bank details")?;
Ok(web::Json(details))
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(get_bank_details,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/pats.rs b/apps/labrinth/src/routes/internal/pats.rs
index 253c9f7254..e708653bec 100644
--- a/apps/labrinth/src/routes/internal/pats.rs
+++ b/apps/labrinth/src/routes/internal/pats.rs
@@ -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,
}
+/// 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,7 +189,9 @@ pub struct ModifyPersonalAccessToken {
pub expires: Option>,
}
+/// Update a personal access token.
#[utoipa::path(
+ tag = "personal access tokens",
patch,
operation_id = "editPat",
params(("id" = String, Path, description = "The PAT ID")),
@@ -293,7 +299,9 @@ 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")),
@@ -345,3 +353,8 @@ pub async fn delete_pat(
Ok(HttpResponse::NoContent().finish())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(get_pats, create_pat, edit_pat, delete_pat,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/search.rs b/apps/labrinth/src/routes/internal/search.rs
index 86a3615d55..7a993a3371 100644
--- a/apps/labrinth/src/routes/internal/search.rs
+++ b/apps/labrinth/src/routes/internal/search.rs
@@ -5,20 +5,28 @@ 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(
+ tag = "search",
+ responses((status = OK, body = serde_json::Value))
+)]
+#[get("/tasks", guard = "admin_key_guard")]
pub async fn tasks(
search: web::Data,
) -> Result, ApiError> {
Ok(web::Json(search.tasks().await.map_err(ApiError::Internal)?))
}
-#[utoipa::path]
-#[delete("tasks", guard = "admin_key_guard")]
+/// Cancel search tasks.
+#[utoipa::path(
+ tag = "search",
+ responses((status = NO_CONTENT))
+)]
+#[delete("/tasks", guard = "admin_key_guard")]
pub async fn tasks_cancel(
search: web::Data,
body: web::Json,
@@ -29,3 +37,8 @@ pub async fn tasks_cancel(
.map_err(ApiError::Internal)?;
Ok(())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(tasks, tasks_cancel,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/server_ping.rs b/apps/labrinth/src/routes/internal/server_ping.rs
index 6f2d7e147f..a48c50c591 100644
--- a/apps/labrinth/src/routes/internal/server_ping.rs
+++ b/apps/labrinth/src/routes/internal/server_ping.rs
@@ -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,11 @@ pub struct PingRequest {
pub timeout_ms: Option,
}
-#[utoipa::path]
+/// Ping Minecraft server.
+#[utoipa::path(
+ tag = "server ping",
+ responses((status = NO_CONTENT))
+)]
#[post("/minecraft-java")]
pub async fn ping_minecraft_java(
req: HttpRequest,
@@ -47,3 +51,8 @@ pub async fn ping_minecraft_java(
Ok(())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(ping_minecraft_java,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs
index 405a1781d3..62503703f7 100644
--- a/apps/labrinth/src/routes/internal/session.rs
+++ b/apps/labrinth/src/routes/internal/session.rs
@@ -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,15 @@ pub async fn issue_session(
Ok(session)
}
+/// List sessions.
#[utoipa::path(
+ 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,7 +180,9 @@ pub async fn list(
Ok(HttpResponse::Ok().json(sessions))
}
+/// Delete a session.
#[utoipa::path(
+ tag = "sessions",
delete,
operation_id = "deleteSession",
params(("id" = String, Path, description = "The session ID")),
@@ -228,13 +232,15 @@ pub async fn delete(
Ok(HttpResponse::NoContent().body(""))
}
+/// Refresh a session.
#[utoipa::path(
+ 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(
@@ -310,3 +316,16 @@ pub async fn refresh(
))
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(list, delete, refresh,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ nest(
+ (path = "/session", api = RouteDoc),
+ )
+)]
+pub(crate) struct ApiDoc;
diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs
index 9a589e5013..8f10172f4d 100644
--- a/apps/labrinth/src/routes/internal/statuses.rs
+++ b/apps/labrinth/src/routes/internal/statuses.rs
@@ -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,
@@ -554,3 +559,8 @@ pub async fn close_socket(
Ok(())
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(ws_init,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/maven.rs b/apps/labrinth/src/routes/maven.rs
index 41757641cc..9259d1436a 100644
--- a/apps/labrinth/src/routes/maven.rs
+++ b/apps/labrinth/src/routes/maven.rs
@@ -18,7 +18,7 @@ use quick_xml::escape::escape;
use std::collections::HashSet;
use yaserde::YaSerialize;
-pub fn config(cfg: &mut web::ServiceConfig) {
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(maven_metadata);
cfg.service(version_file_sha512);
cfg.service(version_file_sha1);
@@ -70,7 +70,11 @@ pub struct MavenPom {
description: String,
}
-#[get("maven/modrinth/{id}/maven-metadata.xml")]
+#[utoipa::path(
+ tag = "maven",
+ responses((status = OK, body = String, content_type = "text/xml"))
+)]
+#[get("/maven/modrinth/{id}/maven-metadata.xml")]
pub async fn maven_metadata(
req: HttpRequest,
params: web::Path<(String,)>,
@@ -279,8 +283,15 @@ fn find_file<'a>(
None
}
+#[utoipa::path(
+ tag = "maven",
+ responses(
+ (status = OK, body = String, content_type = "text/xml"),
+ (status = TEMPORARY_REDIRECT)
+ )
+)]
#[route(
- "maven/modrinth/{id}/{versionnum}/{file}",
+ "/maven/modrinth/{id}/{versionnum}/{file}",
method = "GET",
method = "HEAD"
)]
@@ -349,7 +360,8 @@ pub async fn version_file(
Err(ApiError::NotFound)
}
-#[get("maven/modrinth/{id}/{versionnum}/{file}.sha1")]
+#[utoipa::path(tag = "maven", responses((status = OK, body = String)))]
+#[get("/maven/modrinth/{id}/{versionnum}/{file}.sha1")]
pub async fn version_file_sha1(
req: HttpRequest,
params: web::Path<(String, String, String)>,
@@ -396,7 +408,8 @@ pub async fn version_file_sha1(
))
}
-#[get("maven/modrinth/{id}/{versionnum}/{file}.sha512")]
+#[utoipa::path(tag = "maven", responses((status = OK, body = String)))]
+#[get("/maven/modrinth/{id}/{versionnum}/{file}.sha512")]
pub async fn version_file_sha512(
req: HttpRequest,
params: web::Path<(String, String, String)>,
@@ -442,3 +455,13 @@ pub async fn version_file_sha512(
|hash_str| HttpResponse::Ok().body(hash_str.clone()),
))
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ maven_metadata,
+ version_file,
+ version_file_sha1,
+ version_file_sha512,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs
index f5cfdc1ad9..bb01a0bf53 100644
--- a/apps/labrinth/src/routes/mod.rs
+++ b/apps/labrinth/src/routes/mod.rs
@@ -24,6 +24,28 @@ mod updates;
pub use self::not_found::not_found;
pub fn root_config(cfg: &mut web::ServiceConfig) {
+ cfg.service(
+ web::scope("/api/v1")
+ .wrap(default_cors())
+ .wrap_fn(|req, _srv| {
+ async {
+ Ok(req.into_response(
+ HttpResponse::Gone()
+ .content_type("application/json")
+ .body(r#"{"error":"api_deprecated","description":"You are using an application that uses an outdated version of Modrinth's API. Please either update it or switch to another application. For developers: https://docs.modrinth.com/api/#versioning"}"#)
+ ))
+ }.boxed_local()
+ })
+ );
+ cfg.service(
+ web::scope("")
+ .wrap(default_cors())
+ .service(index::index_get)
+ .service(Files::new("/", "assets/")),
+ );
+}
+
+pub fn public_config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/maven")
.wrap(default_cors())
@@ -58,27 +80,18 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
)
.configure(analytics::config),
);
- cfg.service(
- web::scope("/api/v1")
- .wrap(default_cors())
- .wrap_fn(|req, _srv| {
- async {
- Ok(req.into_response(
- HttpResponse::Gone()
- .content_type("application/json")
- .body(r#"{"error":"api_deprecated","description":"You are using an application that uses an outdated version of Modrinth's API. Please either update it or switch to another application. For developers: https://docs.modrinth.com/api/#versioning"}"#)
- ))
- }.boxed_local()
- })
- );
- cfg.service(
- web::scope("")
- .wrap(default_cors())
- .service(index::index_get)
- .service(Files::new("/", "assets/")),
- );
}
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ nest(
+ (path = "/maven", api = maven::RouteDoc),
+ (path = "/updates", api = updates::RouteDoc),
+ (path = "/analytics", api = analytics::RouteDoc),
+ )
+)]
+pub struct PublicApiDoc;
+
/// Error when calling an HTTP endpoint.
#[derive(thiserror::Error, Debug)]
pub enum ApiError {
diff --git a/apps/labrinth/src/routes/updates.rs b/apps/labrinth/src/routes/updates.rs
index 70c826ed64..a1f7bc1521 100644
--- a/apps/labrinth/src/routes/updates.rs
+++ b/apps/labrinth/src/routes/updates.rs
@@ -17,11 +17,11 @@ use crate::queue::session::AuthQueue;
use super::ApiError;
-pub fn config(cfg: &mut web::ServiceConfig) {
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(forge_updates);
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct NeoForge {
#[serde(default = "default_neoforge")]
pub neoforge: String,
@@ -31,7 +31,8 @@ fn default_neoforge() -> String {
"none".into()
}
-#[get("{id}/forge_updates.json")]
+#[utoipa::path(tag = "updates", responses((status = OK)))]
+#[get("/{id}/forge_updates.json")]
pub async fn forge_updates(
req: HttpRequest,
web::Query(neo): web::Query,
@@ -134,3 +135,8 @@ pub async fn forge_updates(
Ok(HttpResponse::Ok().json(response))
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(forge_updates,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/v2/mod.rs b/apps/labrinth/src/routes/v2/mod.rs
index dbae422da2..d000426e05 100644
--- a/apps/labrinth/src/routes/v2/mod.rs
+++ b/apps/labrinth/src/routes/v2/mod.rs
@@ -1,4 +1,4 @@
-mod moderation;
+pub(crate) mod moderation;
mod notifications;
pub(crate) mod project_creation;
mod projects;
@@ -14,17 +14,16 @@ mod versions;
pub use super::ApiError;
use crate::util::cors::default_cors;
+use actix_web::web;
-pub fn utoipa_config(
- cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
-) {
+pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
- utoipa_actix_web::scope("/v2")
+ web::scope("/v2")
.wrap(default_cors())
- .configure(super::internal::admin::config)
.configure(super::internal::session::config)
.configure(super::internal::flows::config)
.configure(super::internal::pats::config)
+ .configure(super::internal::admin::config)
.configure(moderation::config)
.configure(notifications::config)
.configure(project_creation::config)
@@ -39,3 +38,26 @@ pub fn utoipa_config(
.configure(versions::config),
);
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ nest(
+ (path = "/v2", api = super::internal::session::ApiDoc),
+ (path = "/v2", api = super::internal::flows::ApiDoc),
+ (path = "/v2", api = super::internal::pats::RouteDoc),
+ (path = "/v2", api = super::internal::admin::ApiDoc),
+ (path = "/v2", api = moderation::ApiDoc),
+ (path = "/v2", api = notifications::ApiDoc),
+ (path = "/v2", api = project_creation::RouteDoc),
+ (path = "/v2", api = projects::ApiDoc),
+ (path = "/v2", api = reports::RouteDoc),
+ (path = "/v2", api = statistics::RouteDoc),
+ (path = "/v2", api = tags::ApiDoc),
+ (path = "/v2", api = teams::ApiDoc),
+ (path = "/v2", api = threads::ApiDoc),
+ (path = "/v2", api = users::ApiDoc),
+ (path = "/v2", api = version_file::ApiDoc),
+ (path = "/v2", api = versions::ApiDoc),
+ )
+)]
+pub struct ApiDoc;
diff --git a/apps/labrinth/src/routes/v2/moderation.rs b/apps/labrinth/src/routes/v2/moderation.rs
index b79a6b7bd6..7b0280ff2a 100644
--- a/apps/labrinth/src/routes/v2/moderation.rs
+++ b/apps/labrinth/src/routes/v2/moderation.rs
@@ -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 utoipa_actix_web::service_config::ServiceConfig) {
- cfg.service(utoipa_actix_web::scope("/moderation").service(get_projects));
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
+ cfg.service(web::scope("/moderation").service(get_projects));
}
#[derive(Deserialize)]
@@ -22,8 +22,9 @@ fn default_count() -> u16 {
100
}
-/// Get projects in the moderation queue.
+/// List projects in the moderation queue.
#[utoipa::path(
+ tag = "v2 moderation",
get,
operation_id = "getModerationProjects",
params(
@@ -34,7 +35,7 @@ fn default_count() -> u16 {
)
),
responses(
- (status = 200, description = "Expected response to a valid request"),
+ (status = 200, description = "Expected response to a valid request", body = Vec),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -79,3 +80,16 @@ pub async fn get_projects(
Err(response) => Ok(response),
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(get_projects,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(
+ nest(
+ (path = "/moderation", api = RouteDoc),
+ )
+)]
+pub(crate) struct ApiDoc;
diff --git a/apps/labrinth/src/routes/v2/notifications.rs b/apps/labrinth/src/routes/v2/notifications.rs
index 0bd52c0c02..f65554a0c4 100644
--- a/apps/labrinth/src/routes/v2/notifications.rs
+++ b/apps/labrinth/src/routes/v2/notifications.rs
@@ -10,12 +10,12 @@ use crate::routes::v3;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
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(notifications_get);
cfg.service(notifications_delete);
cfg.service(notifications_read);
cfg.service(
- utoipa_actix_web::scope("/notification")
+ web::scope("/notification")
.service(notification_get)
.service(notification_read)
.service(notification_delete),
@@ -27,8 +27,9 @@ pub struct NotificationIds {
pub ids: String,
}
-/// Get multiple notifications by ID.
+/// Get multiple notifications by ID.
#[utoipa::path(
+ tag = "notifications",
get,
operation_id = "getNotifications",
params(
@@ -39,7 +40,7 @@ pub struct NotificationIds {
)
),
responses(
- (status = 200, description = "Expected response to a valid request"),
+ (status = 200, description = "Expected response to a valid request", body = Vec),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -80,13 +81,14 @@ pub async fn notifications_get(
}
}
-/// Get a notification by ID.
+/// Get a notification by ID.
#[utoipa::path(
+ tag = "notifications",
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 = 200, description = "Expected response to a valid request", body = LegacyNotification),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -124,8 +126,9 @@ pub async fn notification_get(
}
}
-/// Mark a notification as read.
+/// Mark a notification as read.
#[utoipa::path(
+ tag = "notifications",
patch,
operation_id = "readNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
@@ -156,8 +159,9 @@ pub async fn notification_read(
.or_else(v2_reroute::flatten_404_error)
}
-/// Delete a notification by ID.
+/// Delete a notification by ID.
#[utoipa::path(
+ tag = "notifications",
delete,
operation_id = "deleteNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
@@ -194,8 +198,9 @@ pub async fn notification_delete(
.or_else(v2_reroute::flatten_404_error)
}
-/// Mark multiple notifications as read.
+/// Mark multiple notifications as read.
#[utoipa::path(
+ tag = "notifications",
patch,
operation_id = "readNotifications",
params(
@@ -238,8 +243,9 @@ pub async fn notifications_read(
.or_else(v2_reroute::flatten_404_error)
}
-/// Delete multiple notifications by ID.
+/// Delete multiple notifications by ID.
#[utoipa::path(
+ tag = "notifications",
delete,
operation_id = "deleteNotifications",
params(
@@ -281,3 +287,32 @@ pub async fn notifications_delete(
.await
.or_else(v2_reroute::flatten_404_error)
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(
+ notifications_get,
+ notification_get,
+ notification_read,
+ notification_delete,
+ notifications_read,
+ notifications_delete,
+))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(notifications_get, notifications_read, notifications_delete,))]
+pub(crate) struct RootRoutesDoc;
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(notification_get, notification_read, notification_delete,))]
+pub(crate) struct NotificationRoutesDoc;
+
+pub(crate) struct ApiDoc;
+
+impl utoipa::OpenApi for ApiDoc {
+ fn openapi() -> utoipa::openapi::OpenApi {
+ let openapi = RootRoutesDoc::openapi();
+ openapi.nest("/notification", NotificationRoutesDoc::openapi())
+ }
+}
diff --git a/apps/labrinth/src/routes/v2/project_creation.rs b/apps/labrinth/src/routes/v2/project_creation.rs
index d6afd30cc8..3e2bf3b4c8 100644
--- a/apps/labrinth/src/routes/v2/project_creation.rs
+++ b/apps/labrinth/src/routes/v2/project_creation.rs
@@ -25,7 +25,7 @@ use validator::Validate;
use super::version_creation::InitialVersionData;
-pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
+pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(project_create);
}
@@ -134,8 +134,9 @@ struct ProjectCreateData {
pub organization_id: Option,
}
-/// Create a new project with initial versions.
+/// Create a new project with initial versions.
#[utoipa::path(
+ tag = "project creation",
post,
operation_id = "createProject",
request_body(
@@ -143,7 +144,7 @@ struct ProjectCreateData {
description = "Multipart payload containing `data` and uploaded files"
),
responses(
- (status = 200, description = "Expected response to a valid request"),
+ (status = 200, description = "Expected response to a valid request", body = LegacyProject),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
@@ -304,3 +305,8 @@ pub async fn project_create(
Err(response) => Ok(response),
}
}
+
+#[derive(utoipa::OpenApi)]
+#[openapi(paths(project_create,))]
+#[allow(dead_code)]
+pub(crate) struct RouteDoc;
diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs
index ded7348367..9add6a6abe 100644
--- a/apps/labrinth/src/routes/v2/projects.rs
+++ b/apps/labrinth/src/routes/v2/projects.rs
@@ -20,13 +20,13 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
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(project_search);
cfg.service(projects_get);
cfg.service(projects_edit);
cfg.service(random_projects_get);
cfg.service(
- utoipa_actix_web::scope("/project")
+ web::scope("/project")
.service(project_get)
.service(project_get_check)
.service(project_delete)
@@ -40,7 +40,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
.service(project_unfollow)
.service(super::teams::team_members_get_project)
.service(
- utoipa_actix_web::scope("/{project_id}")
+ web::scope("/{project_id}")
.service(super::versions::version_list)
.service(super::versions::version_project_get)
.service(dependency_list),
@@ -48,8 +48,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
);
}
-/// Search projects.
+/// Search projects.
#[utoipa::path(
+ tag = "search",
get,
operation_id = "searchProjects",
params(
@@ -77,10 +78,35 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
"limit" = Option,
Query,
description = "Maximum number of search results"
+ ),
+ (
+ "show_metadata" = Option,
+ Query,
+ description = "Whether to include search metadata"
+ ),
+ (
+ "typesense_config" = Option,
+ Query,
+ description = "Typesense request configuration"
+ ),
+ (
+ "new_filters" = Option,
+ Query,
+ description = "Search filters"
+ ),
+ (
+ "filters" = Option,
+ Query,
+ description = "Legacy search filters"
+ ),
+ (
+ "version" = Option,
+ Query,
+ description = "Legacy search version"
)
),
responses(
- (status = 200, description = "Expected response to a valid request"),
+ (status = 200, description = "Expected response to a valid request", body = LegacySearchResults),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -181,8 +207,9 @@ pub struct RandomProjects {
pub count: u32,
}
-/// Get random projects.
+/// Get random projects.
#[utoipa::path(
+ tag = "projects",
get,
operation_id = "randomProjects",
params(
@@ -193,7 +220,7 @@ pub struct RandomProjects {
)
),
responses(
- (status = 200, description = "Expected response to a valid request"),
+ (status = 200, description = "Expected response to a valid request", body = Vec),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -224,8 +251,9 @@ pub async fn random_projects_get(
}
}
-/// Get multiple projects by ID or slug.
+/// Get multiple projects by ID or slug.
#[utoipa::path(
+ tag = "projects",
get,
operation_id = "getProjects",
params(
@@ -235,7 +263,7 @@ pub async fn random_projects_get(
description = "The JSON array of project IDs or slugs"
)
),
- responses((status = 200, description = "Expected response to a valid request"))
+ responses((status = 200, description = "Expected response to a valid request", body = Vec))
)]
#[get("/projects")]
pub async fn projects_get(
@@ -268,13 +296,14 @@ pub async fn projects_get(
}
}
-/// Get a project by ID or slug.
+/// Get a project by ID or slug.
#[utoipa::path(
+ tag = "projects",
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 = 200, description = "Expected response to a valid request", body = LegacyProject),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
@@ -317,13 +346,14 @@ pub async fn project_get(
}
//checks the validity of a project id or slug
-/// Check that a project ID or slug exists.
+/// Check that a project ID or slug exists.
#[utoipa::path(
+ tag = "projects",
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 = 200, description = "Expected response to a valid request", body = v3::projects::ProjectCheckResponse),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
@@ -348,13 +378,14 @@ struct DependencyInfo {
pub versions: Vec,
}
-/// Get dependency projects and versions for a project.
+/// Get dependency projects and versions for a project.
#[utoipa::path(
+ tag = "projects",
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 = 200, description = "Expected response to a valid request", body = DependencyInfo),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
@@ -508,14 +539,15 @@ pub struct EditProject {
pub monetization_status: Option,
}
-/// Modify a project.
+/// Update a project.
#[utoipa::path(
+ tag = "projects",
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 = NO_CONTENT, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -766,8 +798,9 @@ pub struct BulkEditProject {
pub discord_url: Option