From 4f6777526133568b49bec8b85881e1a316d8ad0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Wed, 1 Jul 2026 15:41:34 -0400 Subject: [PATCH 1/7] feat: docs --- Cargo.lock | 199 ++++++++++++----- Cargo.toml | 2 +- apps/labrinth/Cargo.toml | 2 +- apps/labrinth/src/api_v2_description.md | 117 ++++++++++ apps/labrinth/src/lib.rs | 31 ++- apps/labrinth/src/main.rs | 185 ++++++++++++++-- apps/labrinth/src/routes/debug/pprof.rs | 6 +- apps/labrinth/src/routes/internal/admin.rs | 6 + .../labrinth/src/routes/internal/affiliate.rs | 15 +- .../src/routes/internal/attribution.rs | 15 +- apps/labrinth/src/routes/internal/billing.rs | 76 ++++++- apps/labrinth/src/routes/internal/campaign.rs | 6 +- .../src/routes/internal/delphi/mod.rs | 20 ++ .../routes/internal/external_notifications.rs | 29 ++- apps/labrinth/src/routes/internal/flows.rs | 66 ++++-- apps/labrinth/src/routes/internal/gdpr.rs | 8 + apps/labrinth/src/routes/internal/globals.rs | 4 +- .../labrinth/src/routes/internal/gotenberg.rs | 16 +- apps/labrinth/src/routes/internal/medal.rs | 14 ++ apps/labrinth/src/routes/internal/mod.rs | 21 +- .../internal/moderation/external_license.rs | 21 +- .../src/routes/internal/moderation/mod.rs | 28 ++- .../routes/internal/moderation/tech_review.rs | 22 +- apps/labrinth/src/routes/internal/mural.rs | 10 + apps/labrinth/src/routes/internal/pats.rs | 8 + apps/labrinth/src/routes/internal/search.rs | 6 +- .../src/routes/internal/server_ping.rs | 3 +- apps/labrinth/src/routes/internal/session.rs | 6 + apps/labrinth/src/routes/internal/statuses.rs | 8 + apps/labrinth/src/routes/v2/mod.rs | 4 +- apps/labrinth/src/routes/v2/moderation.rs | 3 +- apps/labrinth/src/routes/v2/notifications.rs | 18 +- .../src/routes/v2/project_creation.rs | 3 +- apps/labrinth/src/routes/v2/projects.rs | 48 ++-- apps/labrinth/src/routes/v2/reports.rs | 18 +- apps/labrinth/src/routes/v2/statistics.rs | 3 +- apps/labrinth/src/routes/v2/tags.rs | 27 ++- apps/labrinth/src/routes/v2/teams.rs | 24 +- apps/labrinth/src/routes/v2/threads.rs | 12 +- apps/labrinth/src/routes/v2/users.rs | 30 ++- .../src/routes/v2/version_creation.rs | 6 +- apps/labrinth/src/routes/v2/version_file.rs | 27 ++- apps/labrinth/src/routes/v2/versions.rs | 18 +- .../labrinth/src/routes/v3/analytics_event.rs | 16 +- .../src/routes/v3/analytics_get/facets/mod.rs | 2 + .../src/routes/v3/analytics_get/mod.rs | 3 +- .../src/routes/v3/analytics_get/old.rs | 23 +- apps/labrinth/src/routes/v3/content/mod.rs | 10 +- apps/labrinth/src/routes/v3/friends.rs | 12 +- apps/labrinth/src/routes/v3/mod.rs | 25 ++- apps/labrinth/src/routes/v3/oauth_clients.rs | 22 +- apps/labrinth/src/routes/v3/payouts.rs | 37 ++-- .../src/routes/v3/project_creation.rs | 7 +- .../src/routes/v3/project_creation/new.rs | 4 +- apps/labrinth/src/routes/v3/projects.rs | 96 ++++++-- apps/labrinth/src/routes/v3/teams.rs | 3 +- apps/labrinth/src/routes/v3/version_file.rs | 206 +++++++++++++++++- apps/labrinth/src/routes/v3/versions.rs | 6 +- packages/ariadne/src/ids.rs | 25 ++- 59 files changed, 1382 insertions(+), 306 deletions(-) create mode 100644 apps/labrinth/src/api_v2_description.md diff --git a/Cargo.lock b/Cargo.lock index 19f6627e78..7e9d6fdad6 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", @@ -5412,7 +5457,6 @@ dependencies = [ "urlencoding", "utoipa", "utoipa-actix-web", - "utoipa-scalar", "uuid 1.23.3", "validator", "webauthn-rs", @@ -5822,7 +5866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -8494,8 +8538,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 +8552,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 +8905,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 +9342,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 +9545,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 +9573,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -9505,7 +9606,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 +9883,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -9803,7 +9904,7 @@ dependencies = [ "rsa", "rust_decimal", "serde", - "sha1", + "sha1 0.10.6", "sha2", "smallvec", "sqlx-core", @@ -11290,7 +11391,7 @@ dependencies = [ "constant_time_eq", "hmac", "rand 0.9.2", - "sha1", + "sha1 0.10.6", "sha2", ] @@ -11505,7 +11606,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 +11625,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" @@ -11782,18 +11883,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 +13539,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..d58e2af285 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -218,7 +218,7 @@ 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 } +scalar_api_reference = { version = "0.2.2", 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..526708ecfa 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -129,7 +129,7 @@ url = { workspace = true } urlencoding = { workspace = true } utoipa = { workspace = true, features = ["url"] } utoipa-actix-web = { workspace = true } -utoipa-scalar = { workspace = true, features = ["actix-web"] } +scalar_api_reference = { 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/lib.rs b/apps/labrinth/src/lib.rs index 076a19dd4a..a012735776 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -402,12 +402,12 @@ pub fn app_config( .configure(routes::v3::config) .configure(routes::internal::config) .configure(routes::root_config) - .default_service(web::get().wrap(default_cors()).to(routes::not_found)); + .default_service(web::get().to(routes::not_found).wrap(default_cors())); } pub fn utoipa_app_config( cfg: &mut utoipa_actix_web::service_config::ServiceConfig, - _labrinth_config: LabrinthConfig, + labrinth_config: LabrinthConfig, ) { cfg.configure({ #[cfg(target_os = "linux")] @@ -419,7 +419,28 @@ pub fn utoipa_app_config( |_cfg| () } }) - .configure(routes::v2::utoipa_config) - .configure(routes::v3::utoipa_config) - .configure(routes::internal::utoipa_config); + .configure(|cfg| utoipa_app_config_v2(cfg, labrinth_config.clone())) + .configure(|cfg| utoipa_app_config_v3(cfg, labrinth_config.clone())) + .configure(|cfg| utoipa_app_config_internal(cfg, labrinth_config)); +} + +pub fn utoipa_app_config_v2( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, + _labrinth_config: LabrinthConfig, +) { + cfg.configure(routes::v2::utoipa_config); +} + +pub fn utoipa_app_config_v3( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, + _labrinth_config: LabrinthConfig, +) { + cfg.configure(routes::v3::utoipa_config); +} + +pub fn utoipa_app_config_internal( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, + _labrinth_config: LabrinthConfig, +) { + cfg.configure(routes::internal::utoipa_config); } diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index 8b636f485e..8010ede9f7 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,21 @@ 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::{clickhouse, database, file_hosting}; +use labrinth::{ + utoipa_app_config_internal, utoipa_app_config_v2, utoipa_app_config_v3, +}; +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::openapi::extensions::ExtensionsBuilder; +use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}; use utoipa_actix_web::AppExt; -use utoipa_scalar::Servable; #[cfg(target_os = "linux")] #[global_allocator] @@ -223,7 +227,7 @@ async fn app() -> std::io::Result<()> { info!("Starting Actix HTTP server!"); HttpServer::new(move || { - App::new() + let (app, docs_v2) = App::new() .wrap(TracingLogger::default()) .wrap_fn(|req, srv| { // We capture the same fields as `tracing-actix-web`'s `RootSpanBuilder`. @@ -260,11 +264,85 @@ 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() + .openapi(DocsV2::openapi()) + .configure(|cfg| utoipa_app_config_v2(cfg, labrinth_config.clone())) + .split_for_parts(); + let (app, docs_v3) = app + .into_utoipa_app() + .openapi(DocsV3::openapi()) + .configure(|cfg| utoipa_app_config_v3(cfg, labrinth_config.clone())) + .split_for_parts(); + let (app, docs_internal) = app + .into_utoipa_app() + .openapi(DocsInternal::openapi()) + .configure(|cfg| { + utoipa_app_config_internal(cfg, labrinth_config.clone()) + }) + .split_for_parts(); + + 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(|cfg| app_config(cfg, labrinth_config.clone())) }) .bind(&ENV.BIND_ADDR)? @@ -273,19 +351,98 @@ 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( + paths( + labrinth::routes::v3::version_file::get_version_from_hash, + labrinth::routes::v3::version_file::get_update_from_hash, + labrinth::routes::v3::version_file::get_versions_from_hashes, + labrinth::routes::v3::version_file::get_projects_from_hashes, + labrinth::routes::v3::version_file::update_files_many, + labrinth::routes::v3::version_file::update_files, + labrinth::routes::v3::version_file::update_individual_files, + labrinth::routes::v3::version_file::delete_file, + labrinth::routes::v3::version_file::download_version + ), + 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/routes/debug/pprof.rs b/apps/labrinth/src/routes/debug/pprof.rs index 5b583af383..6641da8db6 100644 --- a/apps/labrinth/src/routes/debug/pprof.rs +++ b/apps/labrinth/src/routes/debug/pprof.rs @@ -9,7 +9,8 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(heap).service(flame_graph); } -#[utoipa::path] +/// Get a heap profile. +#[utoipa::path(tag = "debug")] #[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 +24,8 @@ pub async fn heap() -> Result { .body(pprof)) } -#[utoipa::path] +/// Get a heap flame graph. +#[utoipa::path(tag = "debug")] #[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; diff --git a/apps/labrinth/src/routes/internal/admin.rs b/apps/labrinth/src/routes/internal/admin.rs index 00eb793568..f298480d7e 100644 --- a/apps/labrinth/src/routes/internal/admin.rs +++ b/apps/labrinth/src/routes/internal/admin.rs @@ -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( diff --git a/apps/labrinth/src/routes/internal/affiliate.rs b/apps/labrinth/src/routes/internal/affiliate.rs index 5dca27b38f..91dded0f73 100644 --- a/apps/labrinth/src/routes/internal/affiliate.rs +++ b/apps/labrinth/src/routes/internal/affiliate.rs @@ -40,7 +40,8 @@ pub struct IngestClick { pub affiliate_code_id: AffiliateCodeId, } -#[utoipa::path] +/// Ingest an affiliate click. +#[utoipa::path(tag = "affiliates")] #[post("/ingest-click")] async fn ingest_click( req: HttpRequest, @@ -136,7 +137,9 @@ async fn ingest_click( Ok(()) } +/// List affiliate codes. #[utoipa::path( + tag = "affiliates", responses((status = OK, body = inline(Vec))) )] #[get("")] @@ -187,7 +190,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 +268,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 +309,8 @@ async fn get( } } -#[utoipa::path] +/// Delete an affiliate code. +#[utoipa::path(tag = "affiliates")] #[delete("/{id}")] async fn delete( req: HttpRequest, @@ -350,7 +358,8 @@ pub struct PatchRequest { pub source_name: String, } -#[utoipa::path] +/// Update an affiliate code. +#[utoipa::path(tag = "affiliates")] #[patch("/{id}")] async fn patch( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs index 7a0f37afcb..5ddda2906d 100644 --- a/apps/labrinth/src/routes/internal/attribution.rs +++ b/apps/labrinth/src/routes/internal/attribution.rs @@ -89,7 +89,8 @@ struct ScanResponse { queued_files: u64, } -#[utoipa::path] +/// Queue an attribution scan. +#[utoipa::path(tag = "attribution")] #[post("/scan")] async fn scan( req: HttpRequest, @@ -201,7 +202,8 @@ async fn scan( })) } -#[utoipa::path] +/// List project attribution groups. +#[utoipa::path(tag = "attribution")] #[get("/{project_id}")] async fn list( req: HttpRequest, @@ -451,7 +453,8 @@ struct UpdateGroupBody { attribution: AttributionResolution, } -#[utoipa::path] +/// Update an attribution group. +#[utoipa::path(tag = "attribution")] #[patch("/group/{group_id}")] async fn update_group( req: HttpRequest, @@ -532,7 +535,8 @@ struct AssignBody { project_id: ProjectId, } -#[utoipa::path] +/// Move a file to an attribution group. +#[utoipa::path(tag = "attribution")] #[post("/assign")] async fn assign( req: HttpRequest, @@ -688,7 +692,8 @@ struct SplitBody { project_id: ProjectId, } -#[utoipa::path] +/// Split a file into a new attribution group. +#[utoipa::path(tag = "attribution")] #[post("/split")] async fn split( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index 6d3f8f6ec8..f82b46688c 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -62,6 +62,31 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service( + utoipa_actix_web::scope("/_internal/billing") + .service(products) + .service(subscriptions) + .service(user_customer) + .service(edit_subscription) + .service(payment_methods) + .service(add_payment_method_flow) + .service(edit_payment_method) + .service(remove_payment_method) + .service(charges) + .service(credit) + .service(active_servers) + .service(initiate_payment) + .service(stripe_webhook) + .service(refund_charge) + .service(reprocess_charge_tax), + ); +} + +/// List products. +#[utoipa::path(tag = "billing")] #[get("products")] pub async fn products( pool: web::Data, @@ -99,6 +124,8 @@ struct SubscriptionsQuery { pub user_id: Option, } +/// List subscriptions. +#[utoipa::path(tag = "billing")] #[get("subscriptions")] pub async fn subscriptions( req: HttpRequest, @@ -141,7 +168,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,13 +176,15 @@ pub enum ChargeRefundAmount { None, } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct ChargeRefund { #[serde(flatten)] pub amount: ChargeRefundAmount, pub unprovision: Option, } +/// Refund a charge. +#[utoipa::path(tag = "billing")] #[post("charge/{id}/refund")] #[allow(clippy::too_many_arguments)] pub async fn refund_charge( @@ -419,6 +448,8 @@ pub async fn refund_charge( Ok(HttpResponse::NoContent().finish()) } +/// Reprocess tax for a charge. +#[utoipa::path(tag = "billing")] #[post("charge/{id}/tax/reprocess")] pub async fn reprocess_charge_tax( req: HttpRequest, @@ -587,8 +618,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,6 +633,8 @@ pub struct SubscriptionEditQuery { pub dry: Option, } +/// Update a subscription. +#[utoipa::path(tag = "billing")] #[patch("subscription/{id}")] #[allow(clippy::too_many_arguments)] pub async fn edit_subscription( @@ -1091,6 +1125,8 @@ pub async fn edit_subscription( } } +/// Get the current customer. +#[utoipa::path(tag = "billing")] #[get("customer")] pub async fn user_customer( req: HttpRequest, @@ -1129,6 +1165,8 @@ pub struct ChargesQuery { pub user_id: Option, } +/// List payments. +#[utoipa::path(tag = "billing")] #[get("payments")] pub async fn charges( req: HttpRequest, @@ -1188,6 +1226,8 @@ pub async fn charges( )) } +/// Start a payment method flow. +#[utoipa::path(tag = "billing")] #[post("payment_method")] pub async fn add_payment_method_flow( req: HttpRequest, @@ -1241,6 +1281,8 @@ pub struct EditPaymentMethod { pub primary: bool, } +/// Update a payment method. +#[utoipa::path(tag = "billing")] #[patch("payment_method/{id}")] pub async fn edit_payment_method( req: HttpRequest, @@ -1305,6 +1347,8 @@ pub async fn edit_payment_method( } } +/// Remove a payment method. +#[utoipa::path(tag = "billing")] #[delete("payment_method/{id}")] pub async fn remove_payment_method( req: HttpRequest, @@ -1388,6 +1432,8 @@ pub async fn remove_payment_method( } } +/// List payment methods. +#[utoipa::path(tag = "billing")] #[get("payment_methods")] pub async fn payment_methods( req: HttpRequest, @@ -1432,6 +1478,8 @@ pub struct ActiveServersQuery { pub subscription_status: Option, } +/// List active servers. +#[utoipa::path(tag = "billing")] #[get("active_servers")] pub async fn active_servers( req: HttpRequest, @@ -1487,7 +1535,7 @@ pub async fn active_servers( 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 +1555,7 @@ impl PaymentRequestType { } } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ChargeRequestType { Existing { @@ -1515,11 +1563,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 +1576,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,15 +1586,18 @@ 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, } +/// Initiate a payment. +#[utoipa::path(tag = "billing")] #[post("payment")] pub async fn initiate_payment( req: HttpRequest, @@ -1610,6 +1662,8 @@ pub async fn initiate_payment( } } +/// Receive a Stripe webhook. +#[utoipa::path(tag = "billing")] #[post("_stripe")] pub async fn stripe_webhook( req: HttpRequest, @@ -2503,7 +2557,7 @@ async fn apply_credit_many( Ok(()) } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct CreditRequest { #[serde(flatten)] pub target: CreditTarget, @@ -2512,7 +2566,7 @@ pub struct CreditRequest { pub message: String, } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] #[serde(untagged)] pub enum CreditTarget { Subscriptions { @@ -2526,6 +2580,8 @@ pub enum CreditTarget { }, } +/// Credit subscriptions. +#[utoipa::path(tag = "billing")] #[post("credit")] pub async fn credit( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/campaign.rs b/apps/labrinth/src/routes/internal/campaign.rs index 6bac2eb4cf..d2ec29dca6 100644 --- a/apps/labrinth/src/routes/internal/campaign.rs +++ b/apps/labrinth/src/routes/internal/campaign.rs @@ -143,7 +143,8 @@ impl CampaignDonation { } } -#[utoipa::path] +/// Receive a Tiltify webhook. +#[utoipa::path(tag = "campaigns")] #[post("/webhook")] pub async fn tiltify_webhook( req: HttpRequest, @@ -301,7 +302,8 @@ fn verify_tiltify_webhook_signature( Ok(()) } -#[utoipa::path] +/// Get Pride campaign data. +#[utoipa::path(tag = "campaigns")] #[get("/pride-26")] pub async fn pride_26( http: web::Data, diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs index 5a9b403428..d934001828 100644 --- a/apps/labrinth/src/routes/internal/delphi/mod.rs +++ b/apps/labrinth/src/routes/internal/delphi/mod.rs @@ -45,6 +45,18 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service( + utoipa_actix_web::scope("/_internal/delphi") + .service(ingest_report) + .service(_run) + .service(version) + .service(issue_type_schema), + ); +} + /// Type of [`DelphiReportIssueDetails::key`]. /// /// Delphi may provide `null` for the key, but we require a key for storing @@ -141,6 +153,8 @@ pub struct DelphiRunParameters { pub file_id: crate::models::ids::FileId, } +/// Ingest a Delphi report. +#[utoipa::path(tag = "delphi")] #[post("ingest", guard = "admin_key_guard")] async fn ingest_report( pool: web::Data, @@ -466,6 +480,8 @@ pub async fn send_tech_review_exit_file_deleted_message_if_exited( Ok(()) } +/// Run Delphi. +#[utoipa::path(tag = "delphi")] #[post("run")] async fn _run( req: HttpRequest, @@ -487,6 +503,8 @@ async fn _run( run(&**pool, run_parameters.into_inner(), &http).await } +/// Get the Delphi version. +#[utoipa::path(tag = "delphi")] #[get("version")] async fn version( req: HttpRequest, @@ -510,6 +528,8 @@ async fn version( )) } +/// Get the Delphi issue type schema. +#[utoipa::path(tag = "delphi")] #[get("issue_type/schema")] async fn issue_type_schema( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs index 942af86166..27931caa2a 100644 --- a/apps/labrinth/src/routes/internal/external_notifications.rs +++ b/apps/labrinth/src/routes/internal/external_notifications.rs @@ -33,12 +33,27 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(send_custom_email); } -#[derive(Deserialize)] +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service( + utoipa_actix_web::scope("/_internal") + .service(create) + .service(create_email_sync) + .service(remove) + .service(send_custom_email), + ); +} + +#[derive(Deserialize, utoipa::ToSchema)] struct CreateNotification { + #[schema(value_type = serde_json::Value)] pub body: NotificationBody, pub user_ids: Vec, } +/// Create external notifications. +#[utoipa::path(tag = "external notifications")] #[post("external_notifications", guard = "external_notification_key_guard")] pub async fn create( pool: web::Data, @@ -74,11 +89,13 @@ 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")] #[post( "external_notifications/email-sync", guard = "external_notification_key_guard" @@ -178,13 +195,15 @@ 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, } +/// Remove external notifications. +#[utoipa::path(tag = "external notifications")] #[delete("external_notifications", guard = "external_notification_key_guard")] pub async fn remove( pool: 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,6 +252,8 @@ struct SendEmail { pub title: String, } +/// Send a custom email. +#[utoipa::path(tag = "external notifications")] #[post("external_notifications/send_custom_email")] pub async fn send_custom_email( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 8a82b3dc8d..1e1a2b7859 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -63,8 +63,6 @@ use zxcvbn::Score; pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service( utoipa_actix_web::scope("/auth") - .service(init) - .service(auth_callback) .service(delete_auth_provider) .service(create_oauth_account) .service(validate_create_account_with_password) @@ -1067,14 +1065,6 @@ 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") - ) -)] #[get("/init")] pub async fn init( req: HttpRequest, @@ -1165,14 +1155,6 @@ 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") - ) -)] #[get("/callback")] pub async fn auth_callback( req: HttpRequest, @@ -1441,7 +1423,9 @@ struct NewOAuthAccount { pub sign_up_newsletter: bool, } +/// Create account with OAuth. #[utoipa::path( + tag = "auth", post, operation_id = "createOAuthAccount", responses( @@ -1525,7 +1509,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 +1585,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,7 +1927,9 @@ impl ReadyAccountRegisterFlow { } } +/// Validate password account creation. #[utoipa::path( + tag = "auth", post, operation_id = "validateCreateAccountWithPassword", responses( @@ -1964,7 +1954,9 @@ pub async fn validate_create_account_with_password( Ok(()) } +/// Create account with a password. #[utoipa::path( + tag = "auth", post, operation_id = "createAccountPassword", responses( @@ -2010,7 +2002,9 @@ pub struct Login { pub challenge: String, } +/// Log in with a password. #[utoipa::path( + tag = "auth", post, operation_id = "loginPassword", responses( @@ -2168,7 +2162,9 @@ async fn validate_2fa_code( } } +/// Complete login with 2FA. #[utoipa::path( + tag = "auth", post, operation_id = "login2fa", responses( @@ -2225,7 +2221,9 @@ pub async fn login_2fa( } } +/// Start 2FA setup. #[utoipa::path( + tag = "auth", post, operation_id = "begin2faFlow", responses( @@ -2273,7 +2271,9 @@ pub async fn begin_2fa_flow( } } +/// Finish 2FA setup. #[utoipa::path( + tag = "auth", post, operation_id = "finish2faFlow", responses( @@ -2405,7 +2405,9 @@ pub struct Remove2FA { pub code: String, } +/// Remove 2FA. #[utoipa::path( + tag = "auth", delete, operation_id = "remove2fa", responses( @@ -2502,7 +2504,9 @@ pub struct ResetPassword { pub challenge: String, } +/// Start password reset. #[utoipa::path( + tag = "auth", post, operation_id = "resetPasswordBegin", responses( @@ -2605,7 +2609,9 @@ pub struct ChangePassword { pub new_password: Option, } +/// Change password. #[utoipa::path( + tag = "auth", patch, operation_id = "changePassword", responses( @@ -2768,7 +2774,9 @@ pub struct SetEmail { pub email: String, } +/// Set email address. #[utoipa::path( + tag = "auth", patch, operation_id = "setEmail", responses( @@ -2887,7 +2895,9 @@ pub async fn set_email( Ok(HttpResponse::Ok().finish()) } +/// Resend verification email. #[utoipa::path( + tag = "auth", post, operation_id = "resendVerifyEmail", responses( @@ -2959,7 +2969,9 @@ pub struct VerifyEmail { pub flow: String, } +/// Verify email address. #[utoipa::path( + tag = "auth", post, operation_id = "verifyEmail", responses( @@ -3022,7 +3034,9 @@ pub async fn verify_email( } } +/// Subscribe to the newsletter. #[utoipa::path( + tag = "auth", post, operation_id = "subscribeNewsletter", responses( @@ -3068,7 +3082,9 @@ pub async fn subscribe_newsletter( Ok(HttpResponse::NoContent().finish()) } +/// Get newsletter subscription status. #[utoipa::path( + tag = "auth", get, operation_id = "getNewsletterSubscriptionStatus", responses( @@ -3115,7 +3131,9 @@ pub struct RegisterPasskeyResponse { pub flow: String, } +/// Start passkey registration. #[utoipa::path( + tag = "auth", post, operation_id = "registerPasskeyStart", responses( @@ -3212,7 +3230,9 @@ pub struct PasskeyResponse { pub last_used: Option>, } +/// Finish passkey registration. #[utoipa::path( + tag = "auth", post, operation_id = "registerPasskeyFinish", responses( @@ -3318,7 +3338,9 @@ pub struct AuthenticatePasskeyResponse { pub flow: String, } +/// Start passkey authentication. #[utoipa::path( + tag = "auth", post, operation_id = "authenticatePasskeyStart", responses( @@ -3358,7 +3380,9 @@ pub struct AuthenticatePasskeyFinish { pub credential: PublicKeyCredential, } +/// Finish passkey authentication. #[utoipa::path( + tag = "auth", post, operation_id = "authenticatePasskeyFinish", responses( @@ -3472,7 +3496,9 @@ pub async fn authenticate_passkey_finish( } } +/// List passkeys. #[utoipa::path( + tag = "auth", get, operation_id = "listPasskeys", responses( @@ -3519,7 +3545,9 @@ pub struct RenamePasskey { pub name: String, } +/// Rename a passkey. #[utoipa::path( + tag = "auth", patch, operation_id = "renamePasskey", responses( @@ -3571,7 +3599,9 @@ pub async fn rename_passkey( Ok(HttpResponse::NoContent().finish()) } +/// Delete a passkey. #[utoipa::path( + tag = "auth", delete, operation_id = "deletePasskey", responses( diff --git a/apps/labrinth/src/routes/internal/gdpr.rs b/apps/labrinth/src/routes/internal/gdpr.rs index 2412cf33ef..b52c1dbbf2 100644 --- a/apps/labrinth/src/routes/internal/gdpr.rs +++ b/apps/labrinth/src/routes/internal/gdpr.rs @@ -10,6 +10,14 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(web::scope("/gdpr").service(export)); } +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service(utoipa_actix_web::scope("/_internal/gdpr").service(export)); +} + +/// Export GDPR data. +#[utoipa::path(tag = "GDPR")] #[post("/export")] pub async fn export( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/globals.rs b/apps/labrinth/src/routes/internal/globals.rs index 6dbb0c931d..846476a5df 100644 --- a/apps/labrinth/src/routes/internal/globals.rs +++ b/apps/labrinth/src/routes/internal/globals.rs @@ -89,8 +89,8 @@ 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")] #[get("")] pub async fn get_globals() -> web::Json { web::Json(GLOBALS.clone()) diff --git a/apps/labrinth/src/routes/internal/gotenberg.rs b/apps/labrinth/src/routes/internal/gotenberg.rs index 149b254535..f893b7c27f 100644 --- a/apps/labrinth/src/routes/internal/gotenberg.rs +++ b/apps/labrinth/src/routes/internal/gotenberg.rs @@ -28,6 +28,18 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(success_callback).service(error_callback); } +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service( + utoipa_actix_web::scope("/_internal") + .service(success_callback) + .service(error_callback), + ); +} + +/// Receive a Gotenberg success callback. +#[utoipa::path(tag = "gotenberg", request_body = Vec)] #[post("/gotenberg/success", guard = "internal_network_guard")] pub async fn success_callback( web::Header(header::ContentDisposition { @@ -82,7 +94,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 +113,8 @@ impl fmt::Display for GotenbergError { } } +/// Receive a Gotenberg error callback. +#[utoipa::path(tag = "gotenberg")] #[post("/gotenberg/error", guard = "internal_network_guard")] pub async fn error_callback( web::Header(GotenbergTrace(trace)): web::Header, diff --git a/apps/labrinth/src/routes/internal/medal.rs b/apps/labrinth/src/routes/internal/medal.rs index 9c534a4823..ac584e2a51 100644 --- a/apps/labrinth/src/routes/internal/medal.rs +++ b/apps/labrinth/src/routes/internal/medal.rs @@ -17,11 +17,23 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(web::scope("/medal").service(verify).service(redeem)); } +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service( + utoipa_actix_web::scope("/_internal/medal") + .service(verify) + .service(redeem), + ); +} + #[derive(Deserialize)] struct MedalQuery { username: String, } +/// Verify Medal credentials. +#[utoipa::path(tag = "medal")] #[post("verify", guard = "medal_key_guard")] pub async fn verify( pool: web::Data, @@ -50,6 +62,8 @@ pub async fn verify( } } +/// Redeem Medal credit. +#[utoipa::path(tag = "medal")] #[post("redeem", guard = "medal_key_guard")] pub async fn redeem( pool: web::Data, diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index 5bc6531856..08314324be 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -113,5 +113,24 @@ pub fn utoipa_config( utoipa_actix_web::scope("/_internal/attribution") .wrap(default_cors()) .configure(attribution::config), - ); + ) + .service( + utoipa_actix_web::scope("/v2") + .wrap(default_cors()) + .configure(admin::config) + .configure(super::v2::moderation::config), + ) + .service( + utoipa_actix_web::scope("/v3/analytics-event") + .wrap(default_cors()) + .configure(super::v3::analytics_event::config), + ) + .configure(billing::utoipa_config) + .configure(delphi::utoipa_config) + .configure(external_notifications::utoipa_config) + .configure(gdpr::utoipa_config) + .configure(gotenberg::utoipa_config) + .configure(medal::utoipa_config) + .configure(mural::utoipa_config) + .configure(statuses::utoipa_config); } diff --git a/apps/labrinth/src/routes/internal/moderation/external_license.rs b/apps/labrinth/src/routes/internal/moderation/external_license.rs index 6ef567ee34..f762d7542b 100644 --- a/apps/labrinth/src/routes/internal/moderation/external_license.rs +++ b/apps/labrinth/src/routes/internal/moderation/external_license.rs @@ -329,7 +329,8 @@ async fn fetch_by_flame_ids( Ok(results) } -#[utoipa::path] +/// Search external licenses. +#[utoipa::path(tag = "moderation")] #[post("/search")] async fn search( req: HttpRequest, @@ -393,7 +394,8 @@ async fn search( Ok(web::Json(results)) } -#[utoipa::path] +/// Look up external license metadata. +#[utoipa::path(tag = "moderation")] #[post("/lookup")] async fn lookup( req: HttpRequest, @@ -422,7 +424,8 @@ async fn lookup( })) } -#[utoipa::path] +/// Get external license by SHA-1. +#[utoipa::path(tag = "moderation")] #[get("/by-sha1/{sha1}")] async fn get_by_sha1( req: HttpRequest, @@ -448,7 +451,8 @@ async fn get_by_sha1( Ok(web::Json(result)) } -#[utoipa::path] +/// Get external licenses by SHA-1. +#[utoipa::path(tag = "moderation")] #[post("/by-sha1")] async fn get_by_sha1_bulk( req: HttpRequest, @@ -472,7 +476,8 @@ async fn get_by_sha1_bulk( Ok(web::Json(results)) } -#[utoipa::path] +/// Add an external license file. +#[utoipa::path(tag = "moderation")] #[post("/file")] async fn add_file( req: HttpRequest, @@ -484,7 +489,8 @@ 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")] #[post("/file/reassign")] async fn reassign_file( req: HttpRequest, @@ -584,7 +590,8 @@ async fn upsert_file_license( )) } -#[utoipa::path] +/// Update an external license. +#[utoipa::path(tag = "moderation")] #[patch("/{id}")] async fn update_license( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index ddcc775a7e..ddddd7e0af 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -162,8 +162,9 @@ 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", responses((status = OK, body = inline(Vec))) )] #[get("/projects")] @@ -291,8 +292,9 @@ pub async fn get_projects_internal( Ok(web::Json(projects)) } -/// Fetch moderation metadata for a specific project. +/// Get project moderation metadata. #[utoipa::path( + tag = "moderation", responses((status = OK, body = inline(Vec))) )] #[get("/project/{id}")] @@ -447,8 +449,8 @@ pub enum Judgement { }, } -/// Update moderation judgements for projects in the review queue. -#[utoipa::path] +/// Update project moderation judgements. +#[utoipa::path(tag = "moderation")] #[post("/project")] async fn set_project_meta( req: HttpRequest, @@ -536,9 +538,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 +597,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 +643,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 +704,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 +746,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 +818,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") diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index cfa609a98e..a0442d07c2 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -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,8 +664,9 @@ 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))) )] @@ -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,9 @@ 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")] #[put("/report")] async fn add_report( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/mural.rs b/apps/labrinth/src/routes/internal/mural.rs index d10ff4e90c..7805c1417d 100644 --- a/apps/labrinth/src/routes/internal/mural.rs +++ b/apps/labrinth/src/routes/internal/mural.rs @@ -10,6 +10,16 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(get_bank_details); } +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service( + utoipa_actix_web::scope("/_internal").service(get_bank_details), + ); +} + +/// Get bank details. +#[utoipa::path(tag = "mural")] #[get("/mural/bank-details")] async fn get_bank_details( payouts_queue: web::Data, diff --git a/apps/labrinth/src/routes/internal/pats.rs b/apps/labrinth/src/routes/internal/pats.rs index 253c9f7254..c5c8a8e342 100644 --- a/apps/labrinth/src/routes/internal/pats.rs +++ b/apps/labrinth/src/routes/internal/pats.rs @@ -29,7 +29,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(delete_pat); } +/// List personal access tokens. #[utoipa::path( + tag = "personal access tokens", get, operation_id = "getPats", responses( @@ -82,7 +84,9 @@ pub struct NewPersonalAccessToken { pub expires: DateTime, } +/// Create a personal access token. #[utoipa::path( + tag = "personal access tokens", post, operation_id = "createPat", responses( @@ -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")), diff --git a/apps/labrinth/src/routes/internal/search.rs b/apps/labrinth/src/routes/internal/search.rs index 86a3615d55..e8a0fb6403 100644 --- a/apps/labrinth/src/routes/internal/search.rs +++ b/apps/labrinth/src/routes/internal/search.rs @@ -9,7 +9,8 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(tasks).service(tasks_cancel); } -#[utoipa::path] +/// List search tasks. +#[utoipa::path(tag = "search")] #[get("tasks", guard = "admin_key_guard")] pub async fn tasks( search: web::Data, @@ -17,7 +18,8 @@ pub async fn tasks( Ok(web::Json(search.tasks().await.map_err(ApiError::Internal)?)) } -#[utoipa::path] +/// Cancel search tasks. +#[utoipa::path(tag = "search")] #[delete("tasks", guard = "admin_key_guard")] pub async fn tasks_cancel( search: web::Data, diff --git a/apps/labrinth/src/routes/internal/server_ping.rs b/apps/labrinth/src/routes/internal/server_ping.rs index 6f2d7e147f..14a86d1d20 100644 --- a/apps/labrinth/src/routes/internal/server_ping.rs +++ b/apps/labrinth/src/routes/internal/server_ping.rs @@ -22,7 +22,8 @@ pub struct PingRequest { pub timeout_ms: Option, } -#[utoipa::path] +/// Ping Minecraft server. +#[utoipa::path(tag = "server ping")] #[post("/minecraft-java")] pub async fn ping_minecraft_java( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs index 405a1781d3..dded095001 100644 --- a/apps/labrinth/src/routes/internal/session.rs +++ b/apps/labrinth/src/routes/internal/session.rs @@ -133,7 +133,9 @@ pub async fn issue_session( Ok(session) } +/// List sessions. #[utoipa::path( + tag = "sessions", get, operation_id = "listSessions", responses( @@ -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,7 +232,9 @@ pub async fn delete( Ok(HttpResponse::NoContent().body("")) } +/// Refresh a session. #[utoipa::path( + tag = "sessions", post, operation_id = "refreshSession", responses( diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs index 9a589e5013..1be778a12a 100644 --- a/apps/labrinth/src/routes/internal/statuses.rs +++ b/apps/labrinth/src/routes/internal/statuses.rs @@ -39,12 +39,20 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(ws_init); } +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service(utoipa_actix_web::scope("/_internal").service(ws_init)); +} + #[derive(Deserialize)] struct LauncherHeartbeatInit { code: String, } // TODO: Move launcher-specific tunnel traffic to a proper launcher websocket endpoint. +/// Start launcher socket. +#[utoipa::path(tag = "statuses")] #[get("launcher_socket")] pub async fn ws_init( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v2/mod.rs b/apps/labrinth/src/routes/v2/mod.rs index dbae422da2..f9bc20865b 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; @@ -21,11 +21,9 @@ pub fn utoipa_config( cfg.service( utoipa_actix_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(moderation::config) .configure(notifications::config) .configure(project_creation::config) .configure(projects::config) diff --git a/apps/labrinth/src/routes/v2/moderation.rs b/apps/labrinth/src/routes/v2/moderation.rs index b79a6b7bd6..c133dc8453 100644 --- a/apps/labrinth/src/routes/v2/moderation.rs +++ b/apps/labrinth/src/routes/v2/moderation.rs @@ -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( diff --git a/apps/labrinth/src/routes/v2/notifications.rs b/apps/labrinth/src/routes/v2/notifications.rs index 0bd52c0c02..7580d69113 100644 --- a/apps/labrinth/src/routes/v2/notifications.rs +++ b/apps/labrinth/src/routes/v2/notifications.rs @@ -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( @@ -80,8 +81,9 @@ 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")), @@ -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( diff --git a/apps/labrinth/src/routes/v2/project_creation.rs b/apps/labrinth/src/routes/v2/project_creation.rs index d6afd30cc8..d85c407446 100644 --- a/apps/labrinth/src/routes/v2/project_creation.rs +++ b/apps/labrinth/src/routes/v2/project_creation.rs @@ -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( diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs index ded7348367..511c451610 100644 --- a/apps/labrinth/src/routes/v2/projects.rs +++ b/apps/labrinth/src/routes/v2/projects.rs @@ -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( @@ -181,8 +182,9 @@ pub struct RandomProjects { pub count: u32, } -/// Get random projects. +/// Get random projects. #[utoipa::path( + tag = "projects", get, operation_id = "randomProjects", params( @@ -224,8 +226,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( @@ -268,8 +271,9 @@ 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")), @@ -317,8 +321,9 @@ 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")), @@ -348,8 +353,9 @@ 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")), @@ -508,8 +514,9 @@ 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")), @@ -766,8 +773,9 @@ pub struct BulkEditProject { pub discord_url: Option>, } -/// Bulk-edit multiple projects. +/// Bulk-edit multiple projects. #[utoipa::path( + tag = "projects", patch, operation_id = "patchProjects", params( @@ -892,8 +900,9 @@ pub struct Extension { pub ext: String, } -/// Change a project's icon. +/// Change a project's icon. #[utoipa::path( + tag = "projects", patch, operation_id = "changeProjectIcon", params( @@ -949,8 +958,9 @@ pub async fn project_icon_edit( .or_else(v2_reroute::flatten_404_error) } -/// Delete a project's icon. +/// Delete a project's icon. #[utoipa::path( + tag = "projects", delete, operation_id = "deleteProjectIcon", params(("id" = String, Path, description = "The ID or slug of the project")), @@ -998,8 +1008,9 @@ pub struct GalleryCreateQuery { pub ordering: Option, } -/// Add a gallery image to a project. +/// Add a gallery image to a project. #[utoipa::path( + tag = "projects", post, operation_id = "addGalleryImage", params( @@ -1112,8 +1123,9 @@ pub struct GalleryEditQuery { pub ordering: Option, } -/// Modify a gallery image. +/// Update a gallery image. #[utoipa::path( + tag = "projects", patch, operation_id = "modifyGalleryImage", params( @@ -1186,8 +1198,9 @@ pub struct GalleryDeleteQuery { pub url: String, } -/// Delete a gallery image. +/// Delete a gallery image. #[utoipa::path( + tag = "projects", delete, operation_id = "deleteGalleryImage", params( @@ -1228,8 +1241,9 @@ pub async fn delete_gallery_item( .or_else(v2_reroute::flatten_404_error) } -/// Delete a project by ID or slug. +/// Delete a project by ID or slug. #[utoipa::path( + tag = "projects", delete, operation_id = "deleteProject", params(("id" = String, Path, description = "The ID or slug of the project")), @@ -1266,8 +1280,9 @@ pub async fn project_delete( .or_else(v2_reroute::flatten_404_error) } -/// Follow a project. +/// Follow a project. #[utoipa::path( + tag = "projects", post, operation_id = "followProject", params(("id" = String, Path, description = "The ID or slug of the project")), @@ -1295,8 +1310,9 @@ pub async fn project_follow( .or_else(v2_reroute::flatten_404_error) } -/// Unfollow a project. +/// Unfollow a project. #[utoipa::path( + tag = "projects", delete, operation_id = "unfollowProject", params(("id" = String, Path, description = "The ID or slug of the project")), diff --git a/apps/labrinth/src/routes/v2/reports.rs b/apps/labrinth/src/routes/v2/reports.rs index 2193ec61b4..9f5348643b 100644 --- a/apps/labrinth/src/routes/v2/reports.rs +++ b/apps/labrinth/src/routes/v2/reports.rs @@ -17,8 +17,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(report_get); } -/// Create a report for a project, version, or user. +/// Create a report for a project, version, or user. #[utoipa::path( + tag = "reports", post, operation_id = "submitReport", responses( @@ -69,8 +70,9 @@ fn default_all() -> bool { true } -/// Get open reports for the current user. +/// Get open reports for the current user. #[utoipa::path( + tag = "reports", get, operation_id = "getOpenReports", params( @@ -131,8 +133,9 @@ pub struct ReportIds { pub ids: String, } -/// Get multiple reports by ID. +/// Get multiple reports by ID. #[utoipa::path( + tag = "reports", get, operation_id = "getReports", params( @@ -184,8 +187,9 @@ pub async fn reports_get( } } -/// Get a report by ID. +/// Get a report by ID. #[utoipa::path( + tag = "reports", get, operation_id = "getReport", params(("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")), @@ -232,8 +236,9 @@ pub struct EditReport { pub closed: Option, } -/// Modify a report. +/// Update a report. #[utoipa::path( + tag = "reports", patch, operation_id = "modifyReport", params(("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")), @@ -278,8 +283,9 @@ pub async fn report_edit( .or_else(v2_reroute::flatten_404_error) } -/// Delete a report by ID. +/// Delete a report by ID. #[utoipa::path( + tag = "reports", delete, operation_id = "deleteReport", params(("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")), diff --git a/apps/labrinth/src/routes/v2/statistics.rs b/apps/labrinth/src/routes/v2/statistics.rs index 89045851aa..8ef30247b6 100644 --- a/apps/labrinth/src/routes/v2/statistics.rs +++ b/apps/labrinth/src/routes/v2/statistics.rs @@ -17,8 +17,9 @@ pub struct V2Stats { pub files: Option, } -/// Get aggregate instance statistics. +/// Get aggregate instance statistics. #[utoipa::path( + tag = "statistics", get, operation_id = "statistics", responses( diff --git a/apps/labrinth/src/routes/v2/tags.rs b/apps/labrinth/src/routes/v2/tags.rs index ee81a80578..5d63cfa461 100644 --- a/apps/labrinth/src/routes/v2/tags.rs +++ b/apps/labrinth/src/routes/v2/tags.rs @@ -35,8 +35,9 @@ pub struct CategoryData { pub header: String, } -/// Get the list of project categories. +/// List project categories. #[utoipa::path( + tag = "tags", get, operation_id = "categoryList", responses( @@ -81,8 +82,9 @@ pub struct LoaderData { pub supported_project_types: Vec, } -/// Get the list of loaders. +/// List loaders. #[utoipa::path( + tag = "tags", get, operation_id = "loaderList", responses( @@ -155,8 +157,9 @@ pub struct GameVersionQuery { major: Option, } -/// Get the list of game versions. +/// List game versions. #[utoipa::path( + tag = "tags", get, operation_id = "versionList", params( @@ -239,8 +242,9 @@ pub struct License { pub name: String, } -/// Get SPDX license identifiers and names. +/// List SPDX license identifiers and names. #[utoipa::path( + tag = "tags", get, operation_id = "licenseList", responses( @@ -278,8 +282,9 @@ pub struct LicenseText { pub body: String, } -/// Get full license text by SPDX ID. +/// Get full license text by SPDX ID. #[utoipa::path( + tag = "tags", get, operation_id = "licenseText", params(("id" = String, Path, description = "The license ID to get the text for")), @@ -325,8 +330,9 @@ pub struct DonationPlatformQueryData { pub name: String, } -/// Get available donation platforms. +/// List donation platforms. #[utoipa::path( + tag = "tags", get, operation_id = "donationPlatformList", responses( @@ -383,8 +389,9 @@ pub async fn donation_platform_list( .or_else(v2_reroute::flatten_404_error) } -/// Get valid report types. +/// List valid report types. #[utoipa::path( + tag = "tags", get, operation_id = "reportTypeList", responses( @@ -406,8 +413,9 @@ pub async fn report_type_list( .or_else(v2_reroute::flatten_404_error) } -/// Get valid project types. +/// List valid project types. #[utoipa::path( + tag = "tags", get, operation_id = "projectTypeList", responses( @@ -429,8 +437,9 @@ pub async fn project_type_list( .or_else(v2_reroute::flatten_404_error) } -/// Get valid side-type values. +/// List valid side-type values. #[utoipa::path( + tag = "tags", get, operation_id = "sideTypeList", responses( diff --git a/apps/labrinth/src/routes/v2/teams.rs b/apps/labrinth/src/routes/v2/teams.rs index 21bc0c0c36..08951095bb 100644 --- a/apps/labrinth/src/routes/v2/teams.rs +++ b/apps/labrinth/src/routes/v2/teams.rs @@ -30,8 +30,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { // also the members of the organization's team if the project is associated with an organization // (Unlike team_members_get_project, which only returns the members of the project's team) // They can be differentiated by the "organization_permissions" field being null or not -/// Get a project's team members. +/// Get a project's team members. #[utoipa::path( + tag = "teams", get, operation_id = "getProjectTeamMembers", params(("id" = String, Path, description = "The ID or slug of the project")), @@ -74,8 +75,9 @@ pub async fn team_members_get_project( } // Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project) -/// Get a team's members. +/// Get a team's members. #[utoipa::path( + tag = "teams", get, operation_id = "getTeamMembers", params(("id" = TeamId, Path, description = "The ID of the team")), @@ -112,8 +114,9 @@ pub struct TeamIds { pub ids: String, } -/// Get the members of multiple teams. +/// Get the members of multiple teams. #[utoipa::path( + tag = "teams", get, operation_id = "getTeams", params(("ids" = String, Query, description = "The JSON array of team IDs")), @@ -154,8 +157,9 @@ pub async fn teams_get( } } -/// Join a team with a pending invite. +/// Join a team with a pending invite. #[utoipa::path( + tag = "teams", post, operation_id = "joinTeam", params(("id" = TeamId, Path, description = "The ID of the team")), @@ -210,8 +214,9 @@ pub struct NewTeamMember { pub ordering: i64, } -/// Add a member to a team. +/// Add a member to a team. #[utoipa::path( + tag = "teams", post, operation_id = "addTeamMember", params(("id" = TeamId, Path, description = "The ID of the team")), @@ -267,8 +272,9 @@ pub struct EditTeamMember { pub ordering: Option, } -/// Modify a team member. +/// Update a team member. #[utoipa::path( + tag = "teams", patch, operation_id = "modifyTeamMember", params( @@ -326,8 +332,9 @@ pub struct TransferOwnership { pub user_id: UserId, } -/// Transfer team ownership. +/// Transfer team ownership. #[utoipa::path( + tag = "teams", patch, operation_id = "transferTeamOwnership", params(("id" = TeamId, Path, description = "The ID of the team")), @@ -369,8 +376,9 @@ pub async fn transfer_ownership( .or_else(v2_reroute::flatten_404_error) } -/// Remove a member from a team. +/// Remove a member from a team. #[utoipa::path( + tag = "teams", delete, operation_id = "deleteTeamMember", params( diff --git a/apps/labrinth/src/routes/v2/threads.rs b/apps/labrinth/src/routes/v2/threads.rs index e04a369cc5..581a4053a6 100644 --- a/apps/labrinth/src/routes/v2/threads.rs +++ b/apps/labrinth/src/routes/v2/threads.rs @@ -19,8 +19,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(threads_get); } -/// Get a thread by ID. +/// Get a thread by ID. #[utoipa::path( + tag = "threads", get, operation_id = "getThread", params(("id" = ThreadId, Path, description = "The ID of the thread")), @@ -51,8 +52,9 @@ pub struct ThreadIds { pub ids: String, } -/// Get multiple threads by ID. +/// Get multiple threads by ID. #[utoipa::path( + tag = "threads", get, operation_id = "getThreads", params(("ids" = String, Query, description = "The JSON array of thread IDs")), @@ -101,8 +103,9 @@ pub struct NewThreadMessage { pub body: MessageBody, } -/// Send a message to a thread. +/// Send a message to a thread. #[utoipa::path( + tag = "threads", post, operation_id = "sendThreadMessage", params(("id" = ThreadId, Path, description = "The ID of the thread")), @@ -142,8 +145,9 @@ pub async fn thread_send_message( .or_else(v2_reroute::flatten_404_error) } -/// Delete a thread message by ID. +/// Delete a thread message by ID. #[utoipa::path( + tag = "threads", delete, operation_id = "deleteThreadMessage", params(("id" = ThreadMessageId, Path, description = "The ID of the message")), diff --git a/apps/labrinth/src/routes/v2/users.rs b/apps/labrinth/src/routes/v2/users.rs index 21e80adfbe..58388d45c0 100644 --- a/apps/labrinth/src/routes/v2/users.rs +++ b/apps/labrinth/src/routes/v2/users.rs @@ -29,8 +29,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { ); } -/// Get the current user from the authorization header. +/// Get the current user. #[utoipa::path( + tag = "users", get, operation_id = "getUserFromAuth", responses( @@ -68,8 +69,9 @@ pub struct UserIds { pub ids: String, } -/// Get multiple users by ID. +/// Get multiple users by ID. #[utoipa::path( + tag = "users", get, operation_id = "getUsers", params(("ids" = String, Query, description = "The JSON array of user IDs")), @@ -104,8 +106,9 @@ pub async fn users_get( } } -/// Get a user by ID or username. +/// Get a user by ID or username. #[utoipa::path( + tag = "users", get, operation_id = "getUser", params(("id" = String, Path, description = "The ID or username of the user")), @@ -139,8 +142,9 @@ pub async fn user_get( } } -/// Get a user's projects. +/// Get a user's projects. #[utoipa::path( + tag = "users", get, operation_id = "getUserProjects", params(("user_id" = String, Path, description = "The ID or username of the user")), @@ -204,8 +208,9 @@ pub struct EditUser { pub allow_friend_requests: Option, } -/// Modify a user. +/// Update a user. #[utoipa::path( + tag = "users", patch, operation_id = "modifyUser", params(("id" = String, Path, description = "The ID or username of the user")), @@ -258,8 +263,9 @@ pub struct Extension { pub ext: String, } -/// Change a user's avatar. +/// Change a user's avatar. #[utoipa::path( + tag = "users", patch, operation_id = "changeUserIcon", params( @@ -317,8 +323,9 @@ pub async fn user_icon_edit( .or_else(v2_reroute::flatten_404_error) } -/// Remove a user's avatar. +/// Remove a user's avatar. #[utoipa::path( + tag = "users", delete, operation_id = "deleteUserIcon", params(("id" = String, Path, description = "The ID or username of the user")), @@ -354,8 +361,9 @@ pub async fn user_icon_delete( .or_else(v2_reroute::flatten_404_error) } -/// Delete a user by ID or username. +/// Delete a user by ID or username. #[utoipa::path( + tag = "users", delete, operation_id = "deleteUser", params(("id" = String, Path, description = "The ID or username of the user")), @@ -387,8 +395,9 @@ pub async fn user_delete( .or_else(v2_reroute::flatten_404_error) } -/// Get projects followed by a user. +/// Get projects followed by a user. #[utoipa::path( + tag = "users", get, operation_id = "getFollowedProjects", params(("id" = String, Path, description = "The ID or username of the user")), @@ -434,8 +443,9 @@ pub async fn user_follows( } } -/// Get notifications for a user. +/// Get notifications for a user. #[utoipa::path( + tag = "users", get, operation_id = "getUserNotifications", params(("id" = String, Path, description = "The ID or username of the user")), diff --git a/apps/labrinth/src/routes/v2/version_creation.rs b/apps/labrinth/src/routes/v2/version_creation.rs index 72da4004e0..11975a448b 100644 --- a/apps/labrinth/src/routes/v2/version_creation.rs +++ b/apps/labrinth/src/routes/v2/version_creation.rs @@ -76,8 +76,9 @@ pub struct InitialVersionData { } // under `/api/v1/version` -/// Create a version on an existing project. +/// Create a version on an existing project. #[utoipa::path( + tag = "version creation", post, operation_id = "createVersion", request_body( @@ -305,8 +306,9 @@ async fn get_example_version_fields( } // under /api/v1/version/{version_id} -/// Add files to an existing version. +/// Add files to an existing version. #[utoipa::path( + tag = "version creation", post, operation_id = "addFilesToVersion", params(("version_id" = VersionId, Path, description = "The ID of the version")), diff --git a/apps/labrinth/src/routes/v2/version_file.rs b/apps/labrinth/src/routes/v2/version_file.rs index ff8aa5edf3..6dfd4b9eea 100644 --- a/apps/labrinth/src/routes/v2/version_file.rs +++ b/apps/labrinth/src/routes/v2/version_file.rs @@ -31,8 +31,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { } // under /api/v1/version_file/{hash} -/// Get version metadata by file hash. +/// Get version metadata by file hash. #[utoipa::path( + tag = "version file", get, operation_id = "versionFromHash", params( @@ -91,8 +92,9 @@ pub async fn get_version_from_hash( } // under /api/v1/version_file/{hash}/download -/// Download a file by hash. +/// Download a file by hash. #[utoipa::path( + tag = "version file", get, operation_id = "downloadVersionFromHash", params( @@ -143,8 +145,9 @@ pub async fn download_version( } // under /api/v1/version_file/{hash} -/// Delete a file by hash. +/// Delete a file by hash. #[utoipa::path( + tag = "version file", delete, operation_id = "deleteFileFromHash", params( @@ -206,8 +209,9 @@ pub struct UpdateData { pub version_types: Option>, } -/// Get the latest compatible version from a file hash. +/// Get the latest compatible version from a file hash. #[utoipa::path( + tag = "version file", post, operation_id = "getLatestVersionFromHash", params( @@ -292,8 +296,9 @@ pub struct FileHashes { } // under /api/v2/version_files -/// Get versions from file hashes. +/// Get versions from file hashes. #[utoipa::path( + tag = "version file", post, operation_id = "versionsFromHashes", request_body = FileHashes, @@ -343,8 +348,9 @@ pub async fn get_versions_from_hashes( } } -/// Get projects from file hashes. +/// Get projects from file hashes. #[utoipa::path( + tag = "version file", post, operation_id = "projectsFromHashes", request_body = FileHashes, @@ -420,8 +426,9 @@ pub struct ManyUpdateData { pub version_types: Option>, } -/// Get latest compatible versions for multiple hashes. +/// Get latest compatible versions for multiple hashes. #[utoipa::path( + tag = "version file", post, operation_id = "getLatestVersionsFromHashes", request_body = ManyUpdateData, @@ -469,8 +476,9 @@ pub async fn update_files( Ok(HttpResponse::Ok().json(v3_versions)) } -/// Get all latest compatible versions for multiple hashes. +/// Get all latest compatible versions for multiple hashes. #[utoipa::path( + tag = "version file", post, operation_id = "getLatestVersionsFromHashesMany", request_body = ManyUpdateData, @@ -535,8 +543,9 @@ pub struct ManyFileUpdateData { pub hashes: Vec, } -/// Get latest versions with per-hash filters. +/// Get latest versions with per-hash filters. #[utoipa::path( + tag = "version file", post, operation_id = "getLatestVersionsFromHashesIndividual", request_body = ManyFileUpdateData, diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs index d444bf8ed2..88ee07ab10 100644 --- a/apps/labrinth/src/routes/v2/versions.rs +++ b/apps/labrinth/src/routes/v2/versions.rs @@ -44,8 +44,9 @@ fn default_true() -> bool { true } -/// List versions for a project. +/// List versions for a project. #[utoipa::path( + tag = "versions", get, operation_id = "getProjectVersions", params( @@ -169,8 +170,9 @@ pub async fn version_list( } // Given a project ID/slug and a version slug -/// Get a project version by ID or version number. +/// Get a project version by ID or version number. #[utoipa::path( + tag = "versions", get, operation_id = "getVersionFromIdOrNumber", params( @@ -230,8 +232,9 @@ pub struct VersionIds { pub include_changelog: bool, } -/// Get multiple versions by ID. +/// Get multiple versions by ID. #[utoipa::path( + tag = "versions", get, operation_id = "getVersions", params(("ids" = String, Query, description = "The JSON array of version IDs")), @@ -274,8 +277,9 @@ pub async fn versions_get( } } -/// Get a version by ID. +/// Get a version by ID. #[utoipa::path( + tag = "versions", get, operation_id = "getVersion", params(("version_id" = models::ids::VersionId, Path, description = "The ID of the version")), @@ -353,8 +357,9 @@ pub struct EditVersionFileType { pub file_type: Option, } -/// Modify an existing version. +/// Update an existing version. #[utoipa::path( + tag = "versions", patch, operation_id = "modifyVersion", params(("id" = VersionId, Path, description = "The ID of the version")), @@ -468,8 +473,9 @@ pub async fn version_edit( Ok(response) } -/// Delete a version by ID. +/// Delete a version by ID. #[utoipa::path( + tag = "versions", delete, operation_id = "deleteVersion", params(("version_id" = VersionId, Path, description = "The ID of the version")), diff --git a/apps/labrinth/src/routes/v3/analytics_event.rs b/apps/labrinth/src/routes/v3/analytics_event.rs index 9f2f4998b3..d35159e12e 100644 --- a/apps/labrinth/src/routes/v3/analytics_event.rs +++ b/apps/labrinth/src/routes/v3/analytics_event.rs @@ -37,8 +37,8 @@ pub struct AnalyticsEventUpsert { pub ends: DateTime, } -/// Fetches all analytics events. -#[utoipa::path(responses((status = OK, body = Vec)))] +/// List analytics events. +#[utoipa::path(tag = "v3 analytics", responses((status = OK, body = Vec)))] #[get("")] pub async fn analytics_events_get( pool: web::Data, @@ -54,8 +54,8 @@ pub async fn analytics_events_get( Ok(web::Json(events)) } -/// Creates an analytics event. -#[utoipa::path(responses((status = OK, body = AnalyticsEvent)))] +/// Create an analytics event. +#[utoipa::path(tag = "v3 analytics", responses((status = OK, body = AnalyticsEvent)))] #[post("")] pub async fn analytics_event_create( req: HttpRequest, @@ -110,8 +110,8 @@ pub async fn analytics_event_create( Ok(web::Json(event.into())) } -/// Edits an analytics event. -#[utoipa::path(responses((status = OK, body = AnalyticsEvent)))] +/// Update an analytics event. +#[utoipa::path(tag = "v3 analytics", responses((status = OK, body = AnalyticsEvent)))] #[patch("/{id}")] pub async fn analytics_event_edit( req: HttpRequest, @@ -158,8 +158,8 @@ pub async fn analytics_event_edit( Ok(web::Json(event.into())) } -/// Deletes an analytics event. -#[utoipa::path(responses((status = NO_CONTENT)))] +/// Delete an analytics event. +#[utoipa::path(tag = "v3 analytics", responses((status = NO_CONTENT)))] #[delete("/{id}")] pub async fn analytics_event_delete( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs index 145384e897..8755374832 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs @@ -58,7 +58,9 @@ pub struct ProjectPlaytimeFacets { pub country: Vec, } +/// Get analytics facets. #[utoipa::path( + tag = "analytics", responses((status = OK, body = inline(FacetsResponse))), )] #[post("/facets")] diff --git a/apps/labrinth/src/routes/v3/analytics_get/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/mod.rs index 0df8fedf8d..d43e05fded 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/mod.rs @@ -172,8 +172,9 @@ pub enum ProjectAnalyticsEventKind { // logic -/// Fetches analytics data for the authorized user's projects. +/// Fetch analytics data. #[utoipa::path( + tag = "analytics", responses((status = OK, body = inline(GetResponse))), )] #[post("")] diff --git a/apps/labrinth/src/routes/v3/analytics_get/old.rs b/apps/labrinth/src/routes/v3/analytics_get/old.rs index 7a6c19b7b9..ea348597a2 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/old.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/old.rs @@ -48,7 +48,8 @@ pub struct GetData { pub resolution_minutes: Option, // defaults to 1 day. Ignored in routes that do not aggregate over a resolution (eg: /countries) } -#[utoipa::path] +/// Get playtime data. +#[utoipa::path(tag = "analytics")] #[get("/playtime")] pub async fn playtimes_get( req: HttpRequest, @@ -110,7 +111,7 @@ pub async fn playtimes_get( Ok(HttpResponse::Ok().json(hm)) } -/// Get view data for a set of projects or versions. +/// Get view data. /// /// Data is returned as a hashmap of project/version ids to a hashmap of days to views /// eg: @@ -120,7 +121,7 @@ pub async fn playtimes_get( /// } ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -#[utoipa::path] +#[utoipa::path(tag = "analytics")] #[get("/views")] pub async fn views_get( req: HttpRequest, @@ -182,7 +183,7 @@ pub async fn views_get( Ok(HttpResponse::Ok().json(hm)) } -/// Get download data for a set of projects or versions. +/// Get download data. /// /// Data is returned as a hashmap of project/version ids to a hashmap of days to downloads /// eg: @@ -192,7 +193,7 @@ pub async fn views_get( /// } ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -#[utoipa::path] +#[utoipa::path(tag = "analytics")] #[get("/downloads")] pub async fn downloads_get( req: HttpRequest, @@ -255,7 +256,7 @@ pub async fn downloads_get( Ok(HttpResponse::Ok().json(hm)) } -/// Get payout data for a set of projects. +/// Get payout data. /// /// Data is returned as a hashmap of project ids to a hashmap of days to amount earned per day /// eg: @@ -265,7 +266,7 @@ pub async fn downloads_get( /// } ///} /// ONLY project IDs can be used. Unauthorized projects will be filtered out. -#[utoipa::path] +#[utoipa::path(tag = "analytics")] #[get("/revenue")] pub async fn revenue_get( req: HttpRequest, @@ -394,7 +395,7 @@ pub async fn revenue_get( Ok(HttpResponse::Ok().json(hm)) } -/// Get country data for a set of projects or versions. +/// Get download country data. /// /// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to downloads. /// Unknown countries are labeled "". @@ -407,7 +408,7 @@ pub async fn revenue_get( ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. /// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch -#[utoipa::path] +#[utoipa::path(tag = "analytics")] #[get("/countries/downloads")] pub async fn countries_downloads_get( req: HttpRequest, @@ -470,7 +471,7 @@ pub async fn countries_downloads_get( Ok(HttpResponse::Ok().json(hm)) } -/// Get country data for a set of projects or versions. +/// Get view country data. /// /// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to views. /// Unknown countries are labeled "". @@ -483,7 +484,7 @@ pub async fn countries_downloads_get( ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. /// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch -#[utoipa::path] +#[utoipa::path(tag = "analytics")] #[get("/countries/views")] pub async fn countries_views_get( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/content/mod.rs b/apps/labrinth/src/routes/v3/content/mod.rs index 766495013b..11b73fb018 100644 --- a/apps/labrinth/src/routes/v3/content/mod.rs +++ b/apps/labrinth/src/routes/v3/content/mod.rs @@ -31,7 +31,15 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(resolve_content); } -#[post("content/resolve")] +pub fn utoipa_config( + cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +) { + cfg.service(utoipa_actix_web::scope("/v3").service(resolve_content)); +} + +/// Resolve content. +#[utoipa::path(tag = "content", request_body = serde_json::Value)] +#[post("/content/resolve")] async fn resolve_content( req: HttpRequest, request: web::Json, diff --git a/apps/labrinth/src/routes/v3/friends.rs b/apps/labrinth/src/routes/v3/friends.rs index 359b21c603..b621d3cf8d 100644 --- a/apps/labrinth/src/routes/v3/friends.rs +++ b/apps/labrinth/src/routes/v3/friends.rs @@ -23,7 +23,9 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(friends); } -#[post("friend/{id}")] +/// Add a friend. +#[utoipa::path(tag = "friends")] +#[post("/friend/{id}")] pub async fn add_friend( req: HttpRequest, info: web::Path<(String,)>, @@ -133,7 +135,9 @@ pub async fn add_friend( Ok(HttpResponse::NoContent().body("")) } -#[delete("friend/{id}")] +/// Remove a friend. +#[utoipa::path(tag = "friends")] +#[delete("/friend/{id}")] pub async fn remove_friend( req: HttpRequest, info: web::Path<(String,)>, @@ -175,7 +179,9 @@ pub async fn remove_friend( } } -#[get("friends")] +/// List friends. +#[utoipa::path(tag = "friends")] +#[get("/friends")] pub async fn friends( req: HttpRequest, pool: web::Data, diff --git a/apps/labrinth/src/routes/v3/mod.rs b/apps/labrinth/src/routes/v3/mod.rs index 247a2b654d..87ce0070be 100644 --- a/apps/labrinth/src/routes/v3/mod.rs +++ b/apps/labrinth/src/routes/v3/mod.rs @@ -39,6 +39,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .configure(images::config) .configure(notifications::config) .configure(organizations::config) + .configure(payouts::webhook_config) .configure(projects::config) .configure(reports::config) .configure(shared_instance_version_creation::config) @@ -62,11 +63,6 @@ pub fn utoipa_config( .wrap(default_cors()) .configure(analytics_get::config), ); - cfg.service( - utoipa_actix_web::scope("/v3/analytics-event") - .wrap(default_cors()) - .configure(analytics_event::config), - ); cfg.service( utoipa_actix_web::scope("/v3/payout") .wrap(default_cors()) @@ -78,6 +74,25 @@ pub fn utoipa_config( .configure(projects::utoipa_config) .configure(project_creation::config), ); + cfg.service( + utoipa_actix_web::scope("/v3") + .wrap(default_cors()) + .service(friends::add_friend) + .service(friends::remove_friend) + .service(friends::friends) + .service(projects::project_search) + .service(projects::project_search_post) + .service(oauth_clients::get_client) + .service(oauth_clients::get_clients) + .service(oauth_clients::oauth_client_create) + .service(oauth_clients::oauth_client_delete) + .service(oauth_clients::oauth_client_edit) + .service(oauth_clients::oauth_client_icon_edit) + .service(oauth_clients::oauth_client_icon_delete) + .service(oauth_clients::get_user_oauth_authorizations) + .service(oauth_clients::revoke_oauth_authorization), + ); + cfg.configure(content::utoipa_config); } pub async fn hello_world() -> Result { diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index 7c16ea141d..f19e7d5d3e 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -100,6 +100,8 @@ pub async fn get_user_clients( } } +/// Get an OAuth client. +#[utoipa::path(tag = "oauth clients")] #[get("app/{id}")] pub async fn get_client( id: web::Path, @@ -113,6 +115,8 @@ pub async fn get_client( } } +/// List OAuth clients. +#[utoipa::path(tag = "oauth clients")] #[get("apps")] pub async fn get_clients( info: web::Query, @@ -129,7 +133,7 @@ pub async fn get_clients( Ok(HttpResponse::Ok().json(clients)) } -#[derive(Deserialize, Validate)] +#[derive(Deserialize, Validate, utoipa::ToSchema)] pub struct NewOAuthApp { #[validate( custom(function = "crate::util::validate::validate_name"), @@ -154,6 +158,8 @@ pub struct NewOAuthApp { pub description: Option, } +/// Create an OAuth client. +#[utoipa::path(tag = "oauth clients")] #[post("app")] pub async fn oauth_client_create( req: HttpRequest, @@ -215,6 +221,8 @@ pub async fn oauth_client_create( })) } +/// Delete an OAuth client. +#[utoipa::path(tag = "oauth clients")] #[delete("app/{id}")] pub async fn oauth_client_delete( req: HttpRequest, @@ -245,7 +253,7 @@ pub async fn oauth_client_delete( } } -#[derive(Serialize, Deserialize, Validate)] +#[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)] pub struct OAuthClientEdit { #[validate( custom(function = "crate::util::validate::validate_name"), @@ -271,6 +279,8 @@ pub struct OAuthClientEdit { pub description: Option>, } +/// Update an OAuth client. +#[utoipa::path(tag = "oauth clients")] #[patch("app/{id}")] pub async fn oauth_client_edit( req: HttpRequest, @@ -346,6 +356,8 @@ pub struct Extension { pub ext: String, } +/// Update an OAuth client icon. +#[utoipa::path(tag = "oauth clients")] #[patch("app/{id}/icon")] #[allow(clippy::too_many_arguments)] pub async fn oauth_client_icon_edit( @@ -418,6 +430,8 @@ pub async fn oauth_client_icon_edit( Ok(HttpResponse::NoContent().body("")) } +/// Delete an OAuth client icon. +#[utoipa::path(tag = "oauth clients")] #[delete("app/{id}/icon")] pub async fn oauth_client_icon_delete( req: HttpRequest, @@ -468,6 +482,8 @@ pub async fn oauth_client_icon_delete( Ok(HttpResponse::NoContent().body("")) } +/// List OAuth authorizations. +#[utoipa::path(tag = "oauth clients")] #[get("authorizations")] pub async fn get_user_oauth_authorizations( req: HttpRequest, @@ -497,6 +513,8 @@ pub async fn get_user_oauth_authorizations( Ok(HttpResponse::Ok().json(mapped)) } +/// Revoke OAuth authorization. +#[utoipa::path(tag = "oauth clients")] #[delete("authorizations")] pub async fn revoke_oauth_authorization( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index 77df4167ba..f131fba4f1 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -32,9 +32,7 @@ const COMPLIANCE_CHECK_DEBOUNCE: chrono::Duration = chrono::Duration::seconds(15); pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { - cfg.service(paypal_webhook) - .service(tremendous_webhook) - .service(transaction_history) + cfg.service(transaction_history) .service(calculate_fees) .service(create_payout) .service(cancel_payout) @@ -44,12 +42,17 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { .service(post_compliance_form); } +pub fn webhook_config(cfg: &mut web::ServiceConfig) { + cfg.service(paypal_webhook).service(tremendous_webhook); +} + #[derive(Deserialize, utoipa::ToSchema)] pub struct RequestForm { form_type: users_compliance::FormType, } -#[utoipa::path] +/// Submit a compliance form. +#[utoipa::path(tag = "payouts")] #[post("/compliance")] pub async fn post_compliance_form( req: HttpRequest, @@ -148,7 +151,7 @@ pub async fn post_compliance_form( } } -#[utoipa::path] +/// Receive PayPal webhook. #[post("/_paypal")] pub async fn paypal_webhook( req: HttpRequest, @@ -306,7 +309,7 @@ pub async fn paypal_webhook( Ok(HttpResponse::NoContent().finish()) } -#[utoipa::path] +/// Receive Tremendous webhook. #[post("/_tremendous")] pub async fn tremendous_webhook( req: HttpRequest, @@ -424,7 +427,8 @@ pub struct WithdrawalFees { pub exchange_rate: Option, } -#[utoipa::path] +/// Calculate payout fees. +#[utoipa::path(tag = "payouts")] #[post("/fees")] pub async fn calculate_fees( req: HttpRequest, @@ -457,7 +461,8 @@ pub async fn calculate_fees( })) } -#[utoipa::path] +/// Create a payout. +#[utoipa::path(tag = "payouts")] #[post("")] pub async fn create_payout( req: HttpRequest, @@ -670,9 +675,9 @@ pub enum PayoutSource { Affilites, } -/// Get the history of when the authorized user got payouts available, and when +/// Get transaction history. /// the user withdrew their payouts. -#[utoipa::path(responses((status = OK, body = Vec)))] +#[utoipa::path(tag = "payouts", responses((status = OK, body = Vec)))] #[get("/history")] pub async fn transaction_history( req: HttpRequest, @@ -754,7 +759,8 @@ pub async fn transaction_history( Ok(web::Json(txn_items)) } -#[utoipa::path] +/// Cancel a payout. +#[utoipa::path(tag = "payouts")] #[delete("/{id}")] pub async fn cancel_payout( info: web::Path<(PayoutId,)>, @@ -873,7 +879,8 @@ pub enum FormCompletionStatus { Complete, } -#[utoipa::path] +/// List payment methods. +#[utoipa::path(tag = "payouts")] #[get("/methods")] pub async fn payment_methods( payouts_queue: web::Data, @@ -906,7 +913,8 @@ pub struct UserBalance { pub dates: HashMap, Decimal>, } -#[utoipa::path] +/// Get account balance. +#[utoipa::path(tag = "payouts")] #[get("/balance")] pub async fn get_balance( req: HttpRequest, @@ -1137,7 +1145,8 @@ pub struct RevenueData { pub creator_revenue: Decimal, } -#[utoipa::path] +/// Get platform revenue. +#[utoipa::path(tag = "payouts")] #[get("/platform_revenue")] pub async fn platform_revenue( query: web::Query, diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 1088302d2f..626d22d872 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -283,7 +283,8 @@ pub async fn undo_uploads( Ok(()) } -#[utoipa::path] +/// Create a project. +#[utoipa::path(tag = "projects")] #[post("")] pub async fn project_create( req: HttpRequest, @@ -361,10 +362,10 @@ pub async fn project_create_internal( result } -/// Allows creating a project with a specific ID. +/// Create a project with a specific ID. /// /// This is a testing endpoint only accessible behind an admin key. -#[utoipa::path] +#[utoipa::path(tag = "projects")] #[post("/{id}", guard = "admin_key_guard")] pub async fn project_create_with_id( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/project_creation/new.rs b/apps/labrinth/src/routes/v3/project_creation/new.rs index 877ad9a70d..0974983e29 100644 --- a/apps/labrinth/src/routes/v3/project_creation/new.rs +++ b/apps/labrinth/src/routes/v3/project_creation/new.rs @@ -110,11 +110,11 @@ pub struct ProjectCreate { pub components: exp::ProjectEdit, } -/// Creates a new project with the given components. +/// Create a project from components. /// /// Components must include `base` ([`exp::base::Project`]), and at least one /// other component. -#[utoipa::path] +#[utoipa::path(tag = "projects")] #[put("")] pub async fn create( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 1606919089..5886f577c7 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -47,7 +47,7 @@ use serde_json::json; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("search", web::get().to(project_search)); + cfg.service(project_search); cfg.service(project_search_post); cfg.route("projects", web::get().to(projects_get)); cfg.route("projects", web::patch().to(projects_edit)); @@ -175,7 +175,8 @@ pub async fn projects_get( Ok(HttpResponse::Ok().json(projects)) } -#[utoipa::path] +/// Get a project. +#[utoipa::path(tag = "projects")] #[get("/{id}")] async fn project_get( req: HttpRequest, @@ -306,7 +307,8 @@ pub struct EditProject { } #[allow(clippy::too_many_arguments)] -#[utoipa::path] +/// Update a project. +#[utoipa::path(tag = "projects")] #[patch("/{id}")] async fn project_edit( req: HttpRequest, @@ -1213,6 +1215,59 @@ pub async fn edit_project_categories( // pub total_hits: usize, // } +/// Search projects. +#[utoipa::path( + tag = "search", + get, + operation_id = "v3SearchProjects", + params( + ( + "query" = Option, + Query, + description = "The query to search for" + ), + ( + "facets" = Option, + Query, + description = "Search facets JSON" + ), + ( + "filters" = Option, + Query, + description = "Search filters JSON" + ), + ( + "new_filters" = Option, + Query, + description = "Search filters JSON" + ), + ( + "index" = Option, + Query, + description = "Search index to use" + ), + ( + "offset" = Option, + Query, + description = "Search result offset" + ), + ( + "limit" = Option, + Query, + description = "Maximum number of search results" + ), + ( + "version" = Option, + Query, + description = "Game version to filter for" + ) + ), + responses( + (status = 200, description = "Expected response to a valid request"), + (status = 400, description = "Request was invalid, see given error") + ) +)] +#[get("/search")] pub async fn project_search( web::Query(info): web::Query, search_backend: web::Data, @@ -1238,6 +1293,8 @@ pub async fn project_search( } // for more complicated search queries +/// Search projects. +#[utoipa::path(tag = "search", request_body = serde_json::Value)] #[post("/search")] pub async fn project_search_post( web::Json(info): web::Json, @@ -1249,7 +1306,8 @@ pub async fn project_search_post( } //checks the validity of a project id or slug -#[utoipa::path] +/// Check project availability. +#[utoipa::path(tag = "projects")] #[get("/{id}/check")] async fn project_get_check( info: web::Path<(String,)>, @@ -1284,7 +1342,8 @@ pub struct DependencyInfo { pub versions: Vec, } -#[utoipa::path] +/// List project dependencies. +#[utoipa::path(tag = "projects")] #[get("/{project_id}/dependencies")] pub async fn dependency_list( req: HttpRequest, @@ -1730,7 +1789,8 @@ pub struct Extension { } #[allow(clippy::too_many_arguments)] -#[utoipa::path] +/// Update a project icon. +#[utoipa::path(tag = "projects")] #[patch("/{id}/icon")] async fn project_icon_edit( web::Query(ext): web::Query, @@ -1874,7 +1934,8 @@ pub async fn project_icon_edit_internal( Ok(HttpResponse::NoContent().body("")) } -#[utoipa::path] +/// Delete a project icon. +#[utoipa::path(tag = "projects")] #[delete("/{id}/icon")] async fn delete_project_icon( req: HttpRequest, @@ -2000,7 +2061,8 @@ pub struct GalleryCreateQuery { } #[allow(clippy::too_many_arguments)] -#[utoipa::path] +/// Add a gallery item. +#[utoipa::path(tag = "projects")] #[post("/{id}/gallery")] pub async fn add_gallery_item( web::Query(ext): web::Query, @@ -2198,7 +2260,8 @@ pub struct GalleryEditQuery { pub ordering: Option, } -#[utoipa::path] +/// Update a gallery item. +#[utoipa::path(tag = "projects")] #[patch("/{id}/gallery")] async fn edit_gallery_item( req: HttpRequest, @@ -2387,7 +2450,8 @@ pub struct GalleryDeleteQuery { pub url: String, } -#[utoipa::path] +/// Delete a gallery item. +#[utoipa::path(tag = "projects")] #[delete("/{id}/gallery")] async fn delete_gallery_item( req: HttpRequest, @@ -2522,7 +2586,8 @@ pub async fn delete_gallery_item_internal( Ok(HttpResponse::NoContent().body("")) } -#[utoipa::path] +/// Delete a project. +#[utoipa::path(tag = "projects")] #[delete("/{id}")] async fn project_delete( req: HttpRequest, @@ -2680,7 +2745,8 @@ pub async fn project_delete_internal( } } -#[utoipa::path] +/// Follow a project. +#[utoipa::path(tag = "projects")] #[post("/{id}/follow")] async fn project_follow( req: HttpRequest, @@ -2772,7 +2838,8 @@ pub async fn project_follow_internal( } } -#[utoipa::path] +/// Unfollow a project. +#[utoipa::path(tag = "projects")] #[delete("/{id}/follow")] async fn project_unfollow( req: HttpRequest, @@ -2860,7 +2927,8 @@ pub async fn project_unfollow_internal( } } -#[utoipa::path] +/// Get a project's organization. +#[utoipa::path(tag = "projects")] #[get("/{id}/organization")] pub async fn project_get_organization( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index 785e1df07c..43e29f300a 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -40,7 +40,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { // also the members of the organization's team if the project is associated with an organization // (Unlike team_members_get_project, which only returns the members of the project's team) // They can be differentiated by the "organization_permissions" field being null or not -#[utoipa::path] +/// Get a project's team members. +#[utoipa::path(tag = "teams")] #[get("/{project_id}/members")] async fn team_members_get_project( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index 04c0d6f184..2318da25eb 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -38,6 +38,37 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } +/// Get version metadata by file hash. +#[utoipa::path( + tag = "version files", + get, + path = "/v3/version_file/{version_id}", + operation_id = "v3VersionFromHash", + params( + ( + "version_id" = String, + Path, + description = "The hexadecimal file hash" + ), + ( + "algorithm" = Option, + Query, + description = "Hash algorithm to use (sha1 or sha512)" + ), + ( + "version_id" = Option, + Query, + description = "Optional version ID when hash maps to multiple files" + ) + ), + responses( + (status = 200, description = "Expected response to a valid request", body = models::projects::Version), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] pub async fn get_version_from_hash( req: HttpRequest, info: web::Path<(String,)>, @@ -89,7 +120,7 @@ pub async fn get_version_from_hash( } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct HashQuery { pub algorithm: Option, // Defaults to calculation based on size of hash pub version_id: Option, @@ -110,7 +141,7 @@ pub fn default_algorithm_from_hashes(hashes: &[String]) -> String { "sha1".into() } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct UpdateData { pub loaders: Option>, pub version_types: Option>, @@ -123,6 +154,38 @@ pub struct UpdateData { pub loader_fields: Option>>, } +/// Get the latest matching version by file hash. +#[utoipa::path( + tag = "version files", + post, + path = "/v3/version_file/{version_id}/update", + operation_id = "v3UpdateFromHash", + params( + ( + "version_id" = String, + Path, + description = "The hexadecimal file hash" + ), + ( + "algorithm" = Option, + Query, + description = "Hash algorithm to use (sha1 or sha512)" + ), + ( + "version_id" = Option, + Query, + description = "Optional version ID when hash maps to multiple files" + ) + ), + request_body = UpdateData, + responses( + (status = 200, description = "Expected response to a valid request", body = models::projects::Version), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] pub async fn get_update_from_hash( req: HttpRequest, info: web::Path<(String,)>, @@ -208,12 +271,27 @@ pub async fn get_update_from_hash( } // Requests above with multiple versions below -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct FileHashes { pub algorithm: Option, // Defaults to calculation based on size of hash pub hashes: Vec, } +/// Get versions by file hashes. +#[utoipa::path( + tag = "version files", + post, + path = "/v3/version_files", + operation_id = "v3VersionsFromHashes", + request_body = FileHashes, + responses( + (status = 200, description = "Expected response to a valid request", body = HashMap), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] pub async fn get_versions_from_hashes( req: HttpRequest, pool: web::Data, @@ -269,6 +347,21 @@ pub async fn get_versions_from_hashes( Ok(HttpResponse::Ok().json(response)) } +/// Get projects by file hashes. +#[utoipa::path( + tag = "version files", + post, + path = "/v3/version_file/project", + operation_id = "v3ProjectsFromHashes", + request_body = FileHashes, + responses( + (status = 200, description = "Expected response to a valid request", body = HashMap), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] pub async fn get_projects_from_hashes( req: HttpRequest, pool: web::Data, @@ -327,7 +420,7 @@ pub async fn get_projects_from_hashes( Ok(HttpResponse::Ok().json(response)) } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct ManyUpdateData { pub algorithm: Option, // Defaults to calculation based on size of hash pub hashes: Vec, @@ -336,6 +429,17 @@ pub struct ManyUpdateData { pub version_types: Option>, } +/// Get latest matching versions by file hashes. +#[utoipa::path( + tag = "version files", + post, + path = "/v3/version_files/update_many", + operation_id = "v3UpdateFilesMany", + request_body = ManyUpdateData, + responses( + (status = 200, description = "Expected response to a valid request", body = HashMap>) + ) +)] pub async fn update_files_many( pool: web::Data, redis: web::Data, @@ -368,6 +472,17 @@ pub async fn update_files_many( // This endpoint is kept for backwards compat, since it still works in 99% of // cases where H only maps to a single version, and for older clients. This // endpoint will only take the first version for each file hash. +/// Get the latest matching version by file hash. +#[utoipa::path( + tag = "version files", + post, + path = "/v3/version_files/update", + operation_id = "v3UpdateFiles", + request_body = ManyUpdateData, + responses( + (status = 200, description = "Expected response to a valid request", body = HashMap) + ) +)] pub async fn update_files( pool: web::Data, redis: web::Data, @@ -468,7 +583,7 @@ async fn update_files_internal( Ok(response) } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct FileUpdateData { pub hash: String, pub loaders: Option>, @@ -476,12 +591,23 @@ pub struct FileUpdateData { pub version_types: Option>, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct ManyFileUpdateData { pub algorithm: Option, // Defaults to calculation based on size of hash pub hashes: Vec, } +/// Get latest matching versions by individual file filters. +#[utoipa::path( + tag = "version files", + post, + path = "/v3/version_files/update_individual", + operation_id = "v3UpdateIndividualFiles", + request_body = ManyFileUpdateData, + responses( + (status = 200, description = "Expected response to a valid request", body = HashMap) + ) +)] pub async fn update_individual_files( req: HttpRequest, pool: web::Data, @@ -603,6 +729,41 @@ pub async fn update_individual_files( } // under /api/v1/version_file/{hash} +/// Delete a file by hash. +#[utoipa::path( + tag = "version files", + delete, + path = "/v3/version_file/{version_id}", + operation_id = "v3DeleteFileFromHash", + params( + ( + "version_id" = String, + Path, + description = "The hexadecimal file hash" + ), + ( + "algorithm" = Option, + Query, + description = "Hash algorithm to use (sha1 or sha512)" + ), + ( + "version_id" = Option, + Query, + description = "Optional version ID to delete from" + ) + ), + responses( + (status = 204, description = "Expected response to a valid request"), + ( + status = 401, + description = "Incorrect token scopes or no authorization to access the requested item(s)" + ), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] pub async fn delete_file( req: HttpRequest, info: web::Path<(String,)>, @@ -740,12 +901,43 @@ pub async fn delete_file( } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct DownloadRedirect { pub url: String, } // under /api/v1/version_file/{hash}/download +/// Download a file by hash. +#[utoipa::path( + tag = "version files", + get, + path = "/v3/version_file/{version_id}/download", + operation_id = "v3DownloadVersionFromHash", + params( + ( + "version_id" = String, + Path, + description = "The hexadecimal file hash" + ), + ( + "algorithm" = Option, + Query, + description = "Hash algorithm to use (sha1 or sha512)" + ), + ( + "version_id" = Option, + Query, + description = "Optional version ID when hash maps to multiple files" + ) + ), + responses( + (status = 302, description = "Temporary redirect to file URL"), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] pub async fn download_version( req: HttpRequest, info: web::Path<(String,)>, diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 0e322cde51..bfb1c400b8 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -57,7 +57,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { } // Given a project ID/slug and a version slug -#[utoipa::path] +/// Get a project version. +#[utoipa::path(tag = "versions")] #[get("/{project_id}/version/{slug}")] pub async fn version_project_get( req: HttpRequest, @@ -798,7 +799,8 @@ pub struct VersionListFilters { pub include_changelog: bool, } -#[utoipa::path] +/// List project versions. +#[utoipa::path(tag = "versions")] #[get("/{project_id}/version")] async fn version_list( req: HttpRequest, diff --git a/packages/ariadne/src/ids.rs b/packages/ariadne/src/ids.rs index 6f91cecdd1..10e6ecb705 100644 --- a/packages/ariadne/src/ids.rs +++ b/packages/ariadne/src/ids.rs @@ -59,6 +59,21 @@ const MULTIPLES: [u64; 12] = [ #[derive(Copy, Clone, PartialEq, Eq)] pub struct Base62Id(pub u64); +impl utoipa::PartialSchema for Base62Id { + fn schema() -> utoipa::openapi::RefOr { + utoipa::openapi::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::String) + .min_length(Some(8)) + .max_length(Some(8)) + .pattern(Some("^[A-Za-z0-9]{8}$")) + .examples([serde_json::json!("ABcd1234")]) + .build() + .into() + } +} + +impl utoipa::ToSchema for Base62Id {} + /// An error decoding a number from base62. #[derive(Error, Debug)] pub enum DecodingError { @@ -94,12 +109,20 @@ macro_rules! base62_id { serde::Deserialize, Debug, Hash, - utoipa::ToSchema, )] #[serde(from = "ariadne::ids::Base62Id")] #[serde(into = "ariadne::ids::Base62Id")] pub struct $struct(pub u64); + impl utoipa::PartialSchema for $struct { + fn schema() + -> utoipa::openapi::RefOr { + <$crate::ids::Base62Id as utoipa::PartialSchema>::schema() + } + } + + impl utoipa::ToSchema for $struct {} + $crate::ids::impl_base62_display!($struct); impl From<$crate::ids::Base62Id> for $struct { From d0e8edbc1888d71f1405553c6734a8ae6f03f9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Wed, 1 Jul 2026 15:48:36 -0400 Subject: [PATCH 2/7] fix tombi --- Cargo.toml | 2 +- apps/labrinth/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d58e2af285..1b87bd1c63 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", @@ -218,7 +219,6 @@ 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" } -scalar_api_reference = { version = "0.2.2", 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 526708ecfa..2226d6f03b 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 } @@ -129,7 +130,6 @@ url = { workspace = true } urlencoding = { workspace = true } utoipa = { workspace = true, features = ["url"] } utoipa-actix-web = { workspace = true } -scalar_api_reference = { workspace = true, features = ["actix-web"] } uuid = { workspace = true, features = ["fast-rng", "serde", "v4", "v7"] } validator = { workspace = true, features = ["derive"] } webauthn-rs = { workspace = true, features = [ From dd3f0ece6d6a4fd71624d8773d90163c57f85d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Wed, 1 Jul 2026 16:05:00 -0400 Subject: [PATCH 3/7] chore: fix some of the routes rendering with missing / --- apps/labrinth/src/routes/analytics.rs | 6 ++-- apps/labrinth/src/routes/internal/billing.rs | 30 +++++++++---------- .../src/routes/internal/delphi/mod.rs | 8 ++--- .../routes/internal/external_notifications.rs | 8 ++--- apps/labrinth/src/routes/internal/medal.rs | 4 +-- apps/labrinth/src/routes/internal/search.rs | 4 +-- apps/labrinth/src/routes/internal/statuses.rs | 2 +- apps/labrinth/src/routes/maven.rs | 6 ++-- apps/labrinth/src/routes/updates.rs | 2 +- apps/labrinth/src/routes/v3/oauth_clients.rs | 18 +++++------ 10 files changed, 44 insertions(+), 44 deletions(-) diff --git a/apps/labrinth/src/routes/analytics.rs b/apps/labrinth/src/routes/analytics.rs index ed25ef45aa..f45c8307e9 100644 --- a/apps/labrinth/src/routes/analytics.rs +++ b/apps/labrinth/src/routes/analytics.rs @@ -58,7 +58,7 @@ pub struct UrlInput { } //this route should be behind the cloudflare WAF to prevent non-browsers from calling it -#[post("view")] +#[post("/view")] async fn page_view_ingest( req: HttpRequest, analytics_queue: web::Data>, @@ -179,7 +179,7 @@ pub struct PlaytimeInput { parent: Option, } -#[post("playtime")] +#[post("/playtime")] async fn playtime_ingest( req: HttpRequest, analytics_queue: web::Data>, @@ -258,7 +258,7 @@ pub struct MinecraftJavaServerPlayInput { pub const MINECRAFT_SERVER_PLAYS: &str = "minecraft_server_plays"; -#[post("minecraft-server-play")] +#[post("/minecraft-server-play")] async fn minecraft_server_play_ingest( req: HttpRequest, analytics_queue: web::Data>, diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index f82b46688c..365651575e 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -87,7 +87,7 @@ pub fn utoipa_config( /// List products. #[utoipa::path(tag = "billing")] -#[get("products")] +#[get("/products")] pub async fn products( pool: web::Data, redis: web::Data, @@ -126,7 +126,7 @@ struct SubscriptionsQuery { /// List subscriptions. #[utoipa::path(tag = "billing")] -#[get("subscriptions")] +#[get("/subscriptions")] pub async fn subscriptions( req: HttpRequest, pool: web::Data, @@ -185,7 +185,7 @@ pub struct ChargeRefund { /// Refund a charge. #[utoipa::path(tag = "billing")] -#[post("charge/{id}/refund")] +#[post("/charge/{id}/refund")] #[allow(clippy::too_many_arguments)] pub async fn refund_charge( req: HttpRequest, @@ -450,7 +450,7 @@ pub async fn refund_charge( /// Reprocess tax for a charge. #[utoipa::path(tag = "billing")] -#[post("charge/{id}/tax/reprocess")] +#[post("/charge/{id}/tax/reprocess")] pub async fn reprocess_charge_tax( req: HttpRequest, pool: web::Data, @@ -635,7 +635,7 @@ pub struct SubscriptionEditQuery { /// Update a subscription. #[utoipa::path(tag = "billing")] -#[patch("subscription/{id}")] +#[patch("/subscription/{id}")] #[allow(clippy::too_many_arguments)] pub async fn edit_subscription( req: HttpRequest, @@ -1127,7 +1127,7 @@ pub async fn edit_subscription( /// Get the current customer. #[utoipa::path(tag = "billing")] -#[get("customer")] +#[get("/customer")] pub async fn user_customer( req: HttpRequest, pool: web::Data, @@ -1167,7 +1167,7 @@ pub struct ChargesQuery { /// List payments. #[utoipa::path(tag = "billing")] -#[get("payments")] +#[get("/payments")] pub async fn charges( req: HttpRequest, pool: web::Data, @@ -1228,7 +1228,7 @@ pub async fn charges( /// Start a payment method flow. #[utoipa::path(tag = "billing")] -#[post("payment_method")] +#[post("/payment_method")] pub async fn add_payment_method_flow( req: HttpRequest, pool: web::Data, @@ -1283,7 +1283,7 @@ pub struct EditPaymentMethod { /// Update a payment method. #[utoipa::path(tag = "billing")] -#[patch("payment_method/{id}")] +#[patch("/payment_method/{id}")] pub async fn edit_payment_method( req: HttpRequest, info: web::Path<(String,)>, @@ -1349,7 +1349,7 @@ pub async fn edit_payment_method( /// Remove a payment method. #[utoipa::path(tag = "billing")] -#[delete("payment_method/{id}")] +#[delete("/payment_method/{id}")] pub async fn remove_payment_method( req: HttpRequest, info: web::Path<(String,)>, @@ -1434,7 +1434,7 @@ pub async fn remove_payment_method( /// List payment methods. #[utoipa::path(tag = "billing")] -#[get("payment_methods")] +#[get("/payment_methods")] pub async fn payment_methods( req: HttpRequest, pool: web::Data, @@ -1480,7 +1480,7 @@ pub struct ActiveServersQuery { /// List active servers. #[utoipa::path(tag = "billing")] -#[get("active_servers")] +#[get("/active_servers")] pub async fn active_servers( req: HttpRequest, pool: web::Data, @@ -1598,7 +1598,7 @@ pub struct PaymentRequest { /// Initiate a payment. #[utoipa::path(tag = "billing")] -#[post("payment")] +#[post("/payment")] pub async fn initiate_payment( req: HttpRequest, pool: web::Data, @@ -1664,7 +1664,7 @@ pub async fn initiate_payment( /// Receive a Stripe webhook. #[utoipa::path(tag = "billing")] -#[post("_stripe")] +#[post("/_stripe")] pub async fn stripe_webhook( req: HttpRequest, payload: String, @@ -2582,7 +2582,7 @@ pub enum CreditTarget { /// Credit subscriptions. #[utoipa::path(tag = "billing")] -#[post("credit")] +#[post("/credit")] pub async fn credit( req: HttpRequest, pool: web::Data, diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs index d934001828..c83cdd7418 100644 --- a/apps/labrinth/src/routes/internal/delphi/mod.rs +++ b/apps/labrinth/src/routes/internal/delphi/mod.rs @@ -155,7 +155,7 @@ pub struct DelphiRunParameters { /// Ingest a Delphi report. #[utoipa::path(tag = "delphi")] -#[post("ingest", guard = "admin_key_guard")] +#[post("/ingest", guard = "admin_key_guard")] async fn ingest_report( pool: web::Data, redis: web::Data, @@ -482,7 +482,7 @@ pub async fn send_tech_review_exit_file_deleted_message_if_exited( /// Run Delphi. #[utoipa::path(tag = "delphi")] -#[post("run")] +#[post("/run")] async fn _run( req: HttpRequest, pool: web::Data, @@ -505,7 +505,7 @@ async fn _run( /// Get the Delphi version. #[utoipa::path(tag = "delphi")] -#[get("version")] +#[get("/version")] async fn version( req: HttpRequest, pool: web::Data, @@ -530,7 +530,7 @@ async fn version( /// Get the Delphi issue type schema. #[utoipa::path(tag = "delphi")] -#[get("issue_type/schema")] +#[get("/issue_type/schema")] async fn issue_type_schema( req: HttpRequest, pool: web::Data, diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs index 27931caa2a..78296522f9 100644 --- a/apps/labrinth/src/routes/internal/external_notifications.rs +++ b/apps/labrinth/src/routes/internal/external_notifications.rs @@ -54,7 +54,7 @@ struct CreateNotification { /// Create external notifications. #[utoipa::path(tag = "external notifications")] -#[post("external_notifications", guard = "external_notification_key_guard")] +#[post("/external_notifications", guard = "external_notification_key_guard")] pub async fn create( pool: web::Data, redis: web::Data, @@ -97,7 +97,7 @@ pub async fn create( /// Create email sync. #[utoipa::path(tag = "external notifications")] #[post( - "external_notifications/email-sync", + "/external_notifications/email-sync", guard = "external_notification_key_guard" )] pub async fn create_email_sync( @@ -204,7 +204,7 @@ struct NotificationFilter { /// Remove external notifications. #[utoipa::path(tag = "external notifications")] -#[delete("external_notifications", guard = "external_notification_key_guard")] +#[delete("/external_notifications", guard = "external_notification_key_guard")] pub async fn remove( pool: web::Data, redis: web::Data, @@ -254,7 +254,7 @@ struct SendEmail { /// Send a custom email. #[utoipa::path(tag = "external notifications")] -#[post("external_notifications/send_custom_email")] +#[post("/external_notifications/send_custom_email")] pub async fn send_custom_email( req: HttpRequest, pool: web::Data, diff --git a/apps/labrinth/src/routes/internal/medal.rs b/apps/labrinth/src/routes/internal/medal.rs index ac584e2a51..9a795f80b2 100644 --- a/apps/labrinth/src/routes/internal/medal.rs +++ b/apps/labrinth/src/routes/internal/medal.rs @@ -34,7 +34,7 @@ struct MedalQuery { /// Verify Medal credentials. #[utoipa::path(tag = "medal")] -#[post("verify", guard = "medal_key_guard")] +#[post("/verify", guard = "medal_key_guard")] pub async fn verify( pool: web::Data, web::Query(MedalQuery { username }): web::Query, @@ -64,7 +64,7 @@ pub async fn verify( /// Redeem Medal credit. #[utoipa::path(tag = "medal")] -#[post("redeem", guard = "medal_key_guard")] +#[post("/redeem", guard = "medal_key_guard")] pub async fn redeem( pool: web::Data, redis: web::Data, diff --git a/apps/labrinth/src/routes/internal/search.rs b/apps/labrinth/src/routes/internal/search.rs index e8a0fb6403..bac8198222 100644 --- a/apps/labrinth/src/routes/internal/search.rs +++ b/apps/labrinth/src/routes/internal/search.rs @@ -11,7 +11,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { /// List search tasks. #[utoipa::path(tag = "search")] -#[get("tasks", guard = "admin_key_guard")] +#[get("/tasks", guard = "admin_key_guard")] pub async fn tasks( search: web::Data, ) -> Result, ApiError> { @@ -20,7 +20,7 @@ pub async fn tasks( /// Cancel search tasks. #[utoipa::path(tag = "search")] -#[delete("tasks", guard = "admin_key_guard")] +#[delete("/tasks", guard = "admin_key_guard")] pub async fn tasks_cancel( search: web::Data, body: web::Json, diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs index 1be778a12a..0ad007f87a 100644 --- a/apps/labrinth/src/routes/internal/statuses.rs +++ b/apps/labrinth/src/routes/internal/statuses.rs @@ -53,7 +53,7 @@ struct LauncherHeartbeatInit { // TODO: Move launcher-specific tunnel traffic to a proper launcher websocket endpoint. /// Start launcher socket. #[utoipa::path(tag = "statuses")] -#[get("launcher_socket")] +#[get("/launcher_socket")] pub async fn ws_init( req: HttpRequest, pool: Data, diff --git a/apps/labrinth/src/routes/maven.rs b/apps/labrinth/src/routes/maven.rs index 41757641cc..d76e34e87e 100644 --- a/apps/labrinth/src/routes/maven.rs +++ b/apps/labrinth/src/routes/maven.rs @@ -70,7 +70,7 @@ pub struct MavenPom { description: String, } -#[get("maven/modrinth/{id}/maven-metadata.xml")] +#[get("/maven/modrinth/{id}/maven-metadata.xml")] pub async fn maven_metadata( req: HttpRequest, params: web::Path<(String,)>, @@ -349,7 +349,7 @@ pub async fn version_file( Err(ApiError::NotFound) } -#[get("maven/modrinth/{id}/{versionnum}/{file}.sha1")] +#[get("/maven/modrinth/{id}/{versionnum}/{file}.sha1")] pub async fn version_file_sha1( req: HttpRequest, params: web::Path<(String, String, String)>, @@ -396,7 +396,7 @@ pub async fn version_file_sha1( )) } -#[get("maven/modrinth/{id}/{versionnum}/{file}.sha512")] +#[get("/maven/modrinth/{id}/{versionnum}/{file}.sha512")] pub async fn version_file_sha512( req: HttpRequest, params: web::Path<(String, String, String)>, diff --git a/apps/labrinth/src/routes/updates.rs b/apps/labrinth/src/routes/updates.rs index 70c826ed64..14786a2d29 100644 --- a/apps/labrinth/src/routes/updates.rs +++ b/apps/labrinth/src/routes/updates.rs @@ -31,7 +31,7 @@ fn default_neoforge() -> String { "none".into() } -#[get("{id}/forge_updates.json")] +#[get("/{id}/forge_updates.json")] pub async fn forge_updates( req: HttpRequest, web::Query(neo): web::Query, diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index f19e7d5d3e..477949f6ae 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -102,7 +102,7 @@ pub async fn get_user_clients( /// Get an OAuth client. #[utoipa::path(tag = "oauth clients")] -#[get("app/{id}")] +#[get("/app/{id}")] pub async fn get_client( id: web::Path, pool: web::Data, @@ -117,7 +117,7 @@ pub async fn get_client( /// List OAuth clients. #[utoipa::path(tag = "oauth clients")] -#[get("apps")] +#[get("/apps")] pub async fn get_clients( info: web::Query, pool: web::Data, @@ -160,7 +160,7 @@ pub struct NewOAuthApp { /// Create an OAuth client. #[utoipa::path(tag = "oauth clients")] -#[post("app")] +#[post("/app")] pub async fn oauth_client_create( req: HttpRequest, new_oauth_app: web::Json, @@ -223,7 +223,7 @@ pub async fn oauth_client_create( /// Delete an OAuth client. #[utoipa::path(tag = "oauth clients")] -#[delete("app/{id}")] +#[delete("/app/{id}")] pub async fn oauth_client_delete( req: HttpRequest, client_id: web::Path, @@ -281,7 +281,7 @@ pub struct OAuthClientEdit { /// Update an OAuth client. #[utoipa::path(tag = "oauth clients")] -#[patch("app/{id}")] +#[patch("/app/{id}")] pub async fn oauth_client_edit( req: HttpRequest, client_id: web::Path, @@ -358,7 +358,7 @@ pub struct Extension { /// Update an OAuth client icon. #[utoipa::path(tag = "oauth clients")] -#[patch("app/{id}/icon")] +#[patch("/app/{id}/icon")] #[allow(clippy::too_many_arguments)] pub async fn oauth_client_icon_edit( web::Query(ext): web::Query, @@ -432,7 +432,7 @@ pub async fn oauth_client_icon_edit( /// Delete an OAuth client icon. #[utoipa::path(tag = "oauth clients")] -#[delete("app/{id}/icon")] +#[delete("/app/{id}/icon")] pub async fn oauth_client_icon_delete( req: HttpRequest, client_id: web::Path, @@ -484,7 +484,7 @@ pub async fn oauth_client_icon_delete( /// List OAuth authorizations. #[utoipa::path(tag = "oauth clients")] -#[get("authorizations")] +#[get("/authorizations")] pub async fn get_user_oauth_authorizations( req: HttpRequest, pool: web::Data, @@ -515,7 +515,7 @@ pub async fn get_user_oauth_authorizations( /// Revoke OAuth authorization. #[utoipa::path(tag = "oauth clients")] -#[delete("authorizations")] +#[delete("/authorizations")] pub async fn revoke_oauth_authorization( req: HttpRequest, info: web::Query, From b0e9c560ba2ba9f9ab0d962fd3350be84569045f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Wed, 1 Jul 2026 16:28:54 -0400 Subject: [PATCH 4/7] response schemas --- apps/labrinth/src/models/v2/notifications.rs | 6 +- apps/labrinth/src/models/v2/reports.rs | 4 +- apps/labrinth/src/models/v2/search.rs | 4 +- apps/labrinth/src/models/v2/teams.rs | 2 +- apps/labrinth/src/models/v2/threads.rs | 8 +- apps/labrinth/src/models/v2/user.rs | 2 +- apps/labrinth/src/models/v3/billing.rs | 4 +- apps/labrinth/src/models/v3/notifications.rs | 2 +- apps/labrinth/src/models/v3/oauth_clients.rs | 8 +- apps/labrinth/src/models/v3/organizations.rs | 2 +- apps/labrinth/src/models/v3/payouts.rs | 17 ++- .../src/models/v3/shared_instances.rs | 2 +- apps/labrinth/src/models/v3/teams.rs | 4 +- apps/labrinth/src/models/v3/users.rs | 2 +- apps/labrinth/src/routes/debug/pprof.rs | 14 +- .../labrinth/src/routes/internal/affiliate.rs | 15 ++- .../src/routes/internal/attribution.rs | 33 +++-- apps/labrinth/src/routes/internal/billing.rs | 126 +++++++++++++----- apps/labrinth/src/routes/internal/campaign.rs | 10 +- .../src/routes/internal/delphi/mod.rs | 20 ++- .../routes/internal/external_notifications.rs | 27 +++- apps/labrinth/src/routes/internal/flows.rs | 82 ++++++------ apps/labrinth/src/routes/internal/gdpr.rs | 5 +- apps/labrinth/src/routes/internal/globals.rs | 7 +- .../labrinth/src/routes/internal/gotenberg.rs | 11 +- apps/labrinth/src/routes/internal/medal.rs | 28 ++-- .../internal/moderation/external_license.rs | 35 ++++- .../src/routes/internal/moderation/mod.rs | 7 +- .../routes/internal/moderation/tech_review.rs | 7 +- apps/labrinth/src/routes/internal/mural.rs | 5 +- apps/labrinth/src/routes/internal/pats.rs | 22 +-- apps/labrinth/src/routes/internal/search.rs | 12 +- .../src/routes/internal/server_ping.rs | 5 +- apps/labrinth/src/routes/internal/session.rs | 20 +-- apps/labrinth/src/routes/internal/statuses.rs | 5 +- apps/labrinth/src/routes/v2/moderation.rs | 2 +- apps/labrinth/src/routes/v2/notifications.rs | 4 +- .../src/routes/v2/project_creation.rs | 2 +- apps/labrinth/src/routes/v2/projects.rs | 32 ++--- apps/labrinth/src/routes/v2/reports.rs | 8 +- apps/labrinth/src/routes/v2/teams.rs | 6 +- apps/labrinth/src/routes/v2/threads.rs | 4 +- apps/labrinth/src/routes/v2/users.rs | 12 +- .../src/routes/v2/version_creation.rs | 4 +- apps/labrinth/src/routes/v2/version_file.rs | 20 +-- apps/labrinth/src/routes/v2/versions.rs | 12 +- .../src/routes/v3/analytics_get/old.rs | 30 ++++- apps/labrinth/src/routes/v3/content/mod.rs | 6 +- apps/labrinth/src/routes/v3/friends.rs | 6 +- apps/labrinth/src/routes/v3/oauth_clients.rs | 30 +++-- apps/labrinth/src/routes/v3/payouts.rs | 61 ++++++--- .../src/routes/v3/project_creation.rs | 6 +- .../src/routes/v3/project_creation/new.rs | 2 +- apps/labrinth/src/routes/v3/projects.rs | 50 ++++--- .../v3/shared_instance_version_creation.rs | 20 +++ apps/labrinth/src/routes/v3/teams.rs | 2 +- .../src/routes/v3/version_creation.rs | 42 ++++++ apps/labrinth/src/routes/v3/version_file.rs | 4 +- apps/labrinth/src/routes/v3/versions.rs | 89 ++++++++++++- apps/labrinth/src/search/mod.rs | 6 +- 60 files changed, 711 insertions(+), 312 deletions(-) diff --git a/apps/labrinth/src/models/v2/notifications.rs b/apps/labrinth/src/models/v2/notifications.rs index 3e60cdbe89..7b41e5949c 100644 --- a/apps/labrinth/src/models/v2/notifications.rs +++ b/apps/labrinth/src/models/v2/notifications.rs @@ -13,7 +13,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 +30,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..c7cd1e1736 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,7 @@ 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..9832c48c6d 100644 --- a/apps/labrinth/src/models/v3/billing.rs +++ b/apps/labrinth/src/models/v3/billing.rs @@ -75,7 +75,9 @@ 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/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..e628f61b71 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/debug/pprof.rs b/apps/labrinth/src/routes/debug/pprof.rs index 6641da8db6..1a4f1953c5 100644 --- a/apps/labrinth/src/routes/debug/pprof.rs +++ b/apps/labrinth/src/routes/debug/pprof.rs @@ -10,7 +10,14 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { } /// Get a heap profile. -#[utoipa::path(tag = "debug")] +#[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; @@ -25,7 +32,10 @@ pub async fn heap() -> Result { } /// Get a heap flame graph. -#[utoipa::path(tag = "debug")] +#[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; diff --git a/apps/labrinth/src/routes/internal/affiliate.rs b/apps/labrinth/src/routes/internal/affiliate.rs index 91dded0f73..a8af09287a 100644 --- a/apps/labrinth/src/routes/internal/affiliate.rs +++ b/apps/labrinth/src/routes/internal/affiliate.rs @@ -41,7 +41,10 @@ pub struct IngestClick { } /// Ingest an affiliate click. -#[utoipa::path(tag = "affiliates")] +#[utoipa::path( + tag = "affiliates", + responses((status = NO_CONTENT)) +)] #[post("/ingest-click")] async fn ingest_click( req: HttpRequest, @@ -310,7 +313,10 @@ async fn get( } /// Delete an affiliate code. -#[utoipa::path(tag = "affiliates")] +#[utoipa::path( + tag = "affiliates", + responses((status = NO_CONTENT)) +)] #[delete("/{id}")] async fn delete( req: HttpRequest, @@ -359,7 +365,10 @@ pub struct PatchRequest { } /// Update an affiliate code. -#[utoipa::path(tag = "affiliates")] +#[utoipa::path( + tag = "affiliates", + responses((status = NO_CONTENT)) +)] #[patch("/{id}")] async fn patch( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs index 5ddda2906d..8e6e3895cf 100644 --- a/apps/labrinth/src/routes/internal/attribution.rs +++ b/apps/labrinth/src/routes/internal/attribution.rs @@ -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, @@ -90,7 +90,10 @@ struct ScanResponse { } /// Queue an attribution scan. -#[utoipa::path(tag = "attribution")] +#[utoipa::path( + tag = "attribution", + responses((status = OK, body = ScanResponse)) +)] #[post("/scan")] async fn scan( req: HttpRequest, @@ -203,7 +206,10 @@ async fn scan( } /// List project attribution groups. -#[utoipa::path(tag = "attribution")] +#[utoipa::path( + tag = "attribution", + responses((status = OK, body = inline(Vec))) +)] #[get("/{project_id}")] async fn list( req: HttpRequest, @@ -454,7 +460,10 @@ struct UpdateGroupBody { } /// Update an attribution group. -#[utoipa::path(tag = "attribution")] +#[utoipa::path( + tag = "attribution", + responses((status = NO_CONTENT)) +)] #[patch("/group/{group_id}")] async fn update_group( req: HttpRequest, @@ -536,7 +545,10 @@ struct AssignBody { } /// Move a file to an attribution group. -#[utoipa::path(tag = "attribution")] +#[utoipa::path( + tag = "attribution", + responses((status = NO_CONTENT)) +)] #[post("/assign")] async fn assign( req: HttpRequest, @@ -693,7 +705,10 @@ struct SplitBody { } /// Split a file into a new attribution group. -#[utoipa::path(tag = "attribution")] +#[utoipa::path( + tag = "attribution", + responses((status = NO_CONTENT)) +)] #[post("/split")] async fn split( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index 365651575e..c031f04e3e 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -86,7 +86,10 @@ pub fn utoipa_config( } /// List products. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = OK, body = serde_json::Value)) +)] #[get("/products")] pub async fn products( pool: web::Data, @@ -125,7 +128,10 @@ struct SubscriptionsQuery { } /// List subscriptions. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = OK, body = serde_json::Value)) +)] #[get("/subscriptions")] pub async fn subscriptions( req: HttpRequest, @@ -184,7 +190,10 @@ pub struct ChargeRefund { } /// Refund a charge. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = NO_CONTENT)) +)] #[post("/charge/{id}/refund")] #[allow(clippy::too_many_arguments)] pub async fn refund_charge( @@ -449,7 +458,10 @@ pub async fn refund_charge( } /// Reprocess tax for a charge. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = NO_CONTENT)) +)] #[post("/charge/{id}/tax/reprocess")] pub async fn reprocess_charge_tax( req: HttpRequest, @@ -634,7 +646,13 @@ pub struct SubscriptionEditQuery { } /// Update a subscription. -#[utoipa::path(tag = "billing")] +#[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( @@ -1126,7 +1144,10 @@ pub async fn edit_subscription( } /// Get the current customer. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = OK, body = serde_json::Value)) +)] #[get("/customer")] pub async fn user_customer( req: HttpRequest, @@ -1166,7 +1187,10 @@ pub struct ChargesQuery { } /// List payments. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = OK, body = serde_json::Value)) +)] #[get("/payments")] pub async fn charges( req: HttpRequest, @@ -1227,7 +1251,10 @@ pub async fn charges( } /// Start a payment method flow. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = OK, body = serde_json::Value)) +)] #[post("/payment_method")] pub async fn add_payment_method_flow( req: HttpRequest, @@ -1282,7 +1309,10 @@ pub struct EditPaymentMethod { } /// Update a payment method. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = NO_CONTENT)) +)] #[patch("/payment_method/{id}")] pub async fn edit_payment_method( req: HttpRequest, @@ -1348,7 +1378,10 @@ pub async fn edit_payment_method( } /// Remove a payment method. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = NO_CONTENT)) +)] #[delete("/payment_method/{id}")] pub async fn remove_payment_method( req: HttpRequest, @@ -1433,7 +1466,13 @@ pub async fn remove_payment_method( } /// List payment methods. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses( + (status = OK, body = serde_json::Value), + (status = NO_CONTENT), + ) +)] #[get("/payment_methods")] pub async fn payment_methods( req: HttpRequest, @@ -1475,11 +1514,24 @@ pub async fn payment_methods( #[derive(Deserialize)] pub struct ActiveServersQuery { - pub subscription_status: Option, + pub subscription_status: Option, +} + +#[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")] +#[utoipa::path( + tag = "billing", + responses((status = OK, body = inline(Vec))) +)] #[get("/active_servers")] pub async fn active_servers( req: HttpRequest, @@ -1505,34 +1557,25 @@ 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 { - user_id: x.user_id.into(), - server_id: id.clone(), - price_id: x.price_id.into(), + let server_ids = servers + .into_iter() + .filter_map(|x| { + x.metadata.as_ref().and_then(|metadata| match metadata { + SubscriptionMetadata::Pyro { id, region } => { + Some(ActiveServerResponse { + user_id: x.user_id.into(), + server_id: id.clone(), + price_id: x.price_id.into(), interval: x.interval, region: region.clone(), }) } SubscriptionMetadata::Medal { .. } => None, }) - }) - .collect::>(); + }) + .collect::>(); - Ok(HttpResponse::Ok().json(server_ids)) + Ok(HttpResponse::Ok().json(server_ids)) } #[derive(Deserialize, utoipa::ToSchema)] @@ -1597,7 +1640,10 @@ pub struct PaymentRequest { } /// Initiate a payment. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = OK, body = serde_json::Value)) +)] #[post("/payment")] pub async fn initiate_payment( req: HttpRequest, @@ -1663,7 +1709,10 @@ pub async fn initiate_payment( } /// Receive a Stripe webhook. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = NO_CONTENT)) +)] #[post("/_stripe")] pub async fn stripe_webhook( req: HttpRequest, @@ -2581,7 +2630,10 @@ pub enum CreditTarget { } /// Credit subscriptions. -#[utoipa::path(tag = "billing")] +#[utoipa::path( + tag = "billing", + responses((status = NO_CONTENT)) +)] #[post("/credit")] pub async fn credit( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/campaign.rs b/apps/labrinth/src/routes/internal/campaign.rs index d2ec29dca6..59621256a0 100644 --- a/apps/labrinth/src/routes/internal/campaign.rs +++ b/apps/labrinth/src/routes/internal/campaign.rs @@ -144,7 +144,10 @@ impl CampaignDonation { } /// Receive a Tiltify webhook. -#[utoipa::path(tag = "campaigns")] +#[utoipa::path( + tag = "campaigns", + responses((status = NO_CONTENT)) +)] #[post("/webhook")] pub async fn tiltify_webhook( req: HttpRequest, @@ -303,7 +306,10 @@ fn verify_tiltify_webhook_signature( } /// Get Pride campaign data. -#[utoipa::path(tag = "campaigns")] +#[utoipa::path( + tag = "campaigns", + responses((status = OK, body = CampaignInfo)) +)] #[get("/pride-26")] pub async fn pride_26( http: web::Data, diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs index c83cdd7418..513f089fb5 100644 --- a/apps/labrinth/src/routes/internal/delphi/mod.rs +++ b/apps/labrinth/src/routes/internal/delphi/mod.rs @@ -154,7 +154,10 @@ pub struct DelphiRunParameters { } /// Ingest a Delphi report. -#[utoipa::path(tag = "delphi")] +#[utoipa::path( + tag = "delphi", + responses((status = NO_CONTENT)) +)] #[post("/ingest", guard = "admin_key_guard")] async fn ingest_report( pool: web::Data, @@ -481,7 +484,10 @@ pub async fn send_tech_review_exit_file_deleted_message_if_exited( } /// Run Delphi. -#[utoipa::path(tag = "delphi")] +#[utoipa::path( + tag = "delphi", + responses((status = NO_CONTENT)) +)] #[post("/run")] async fn _run( req: HttpRequest, @@ -504,7 +510,10 @@ async fn _run( } /// Get the Delphi version. -#[utoipa::path(tag = "delphi")] +#[utoipa::path( + tag = "delphi", + responses((status = OK, body = inline(Option))) +)] #[get("/version")] async fn version( req: HttpRequest, @@ -529,7 +538,10 @@ async fn version( } /// Get the Delphi issue type schema. -#[utoipa::path(tag = "delphi")] +#[utoipa::path( + tag = "delphi", + responses((status = OK, body = serde_json::Value)) +)] #[get("/issue_type/schema")] async fn issue_type_schema( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs index 78296522f9..bb86ee2e0e 100644 --- a/apps/labrinth/src/routes/internal/external_notifications.rs +++ b/apps/labrinth/src/routes/internal/external_notifications.rs @@ -53,7 +53,10 @@ struct CreateNotification { } /// Create external notifications. -#[utoipa::path(tag = "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, @@ -95,10 +98,16 @@ pub async fn create( /// - `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")] +#[utoipa::path( + tag = "external notifications", + responses( + (status = OK, body = inline(Vec)), + (status = 207, body = inline(Vec)), + ) +)] #[post( - "/external_notifications/email-sync", - guard = "external_notification_key_guard" + "/external_notifications/email-sync", + guard = "external_notification_key_guard" )] pub async fn create_email_sync( pool: web::Data, @@ -203,7 +212,10 @@ struct NotificationFilter { } /// Remove external notifications. -#[utoipa::path(tag = "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, @@ -253,7 +265,10 @@ struct SendEmail { } /// Send a custom email. -#[utoipa::path(tag = "external notifications")] +#[utoipa::path( + tag = "external notifications", + responses((status = ACCEPTED)) +)] #[post("/external_notifications/send_custom_email")] pub async fn send_custom_email( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 1e1a2b7859..a69e3e3a24 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1427,11 +1427,11 @@ struct NewOAuthAccount { #[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( @@ -1933,7 +1933,7 @@ impl ReadyAccountRegisterFlow { 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") ) )] @@ -1958,11 +1958,11 @@ pub async fn validate_create_account_with_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( @@ -2006,11 +2006,11 @@ pub struct Login { #[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( @@ -2166,11 +2166,11 @@ async fn validate_2fa_code( #[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,11 +2225,11 @@ pub async fn login_2fa( #[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")] @@ -2275,11 +2275,11 @@ pub async fn begin_2fa_flow( #[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")] @@ -3086,11 +3086,11 @@ pub async fn subscribe_newsletter( #[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")] @@ -3384,11 +3384,11 @@ pub struct AuthenticatePasskeyFinish { #[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( diff --git a/apps/labrinth/src/routes/internal/gdpr.rs b/apps/labrinth/src/routes/internal/gdpr.rs index b52c1dbbf2..5c5de097ad 100644 --- a/apps/labrinth/src/routes/internal/gdpr.rs +++ b/apps/labrinth/src/routes/internal/gdpr.rs @@ -17,7 +17,10 @@ pub fn utoipa_config( } /// Export GDPR data. -#[utoipa::path(tag = "GDPR")] +#[utoipa::path( + tag = "GDPR", + responses((status = OK, body = serde_json::Value)) +)] #[post("/export")] pub async fn export( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/globals.rs b/apps/labrinth/src/routes/internal/globals.rs index 846476a5df..b28ea4145c 100644 --- a/apps/labrinth/src/routes/internal/globals.rs +++ b/apps/labrinth/src/routes/internal/globals.rs @@ -90,10 +90,13 @@ pub fn tax_compliance_payout_threshold_for_year( } /// Get backend globals. -#[utoipa::path(tag = "globals")] +#[utoipa::path( + tag = "globals", + responses((status = OK, body = Globals)) +)] #[get("")] pub async fn get_globals() -> web::Json { - web::Json(GLOBALS.clone()) + web::Json(GLOBALS.clone()) } #[cfg(test)] diff --git a/apps/labrinth/src/routes/internal/gotenberg.rs b/apps/labrinth/src/routes/internal/gotenberg.rs index f893b7c27f..2ec3717c51 100644 --- a/apps/labrinth/src/routes/internal/gotenberg.rs +++ b/apps/labrinth/src/routes/internal/gotenberg.rs @@ -39,7 +39,11 @@ pub fn utoipa_config( } /// Receive a Gotenberg success callback. -#[utoipa::path(tag = "gotenberg", request_body = Vec)] +#[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 { @@ -114,7 +118,10 @@ impl fmt::Display for GotenbergError { } /// Receive a Gotenberg error callback. -#[utoipa::path(tag = "gotenberg")] +#[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, diff --git a/apps/labrinth/src/routes/internal/medal.rs b/apps/labrinth/src/routes/internal/medal.rs index 9a795f80b2..cf02ae6bda 100644 --- a/apps/labrinth/src/routes/internal/medal.rs +++ b/apps/labrinth/src/routes/internal/medal.rs @@ -29,11 +29,20 @@ pub fn utoipa_config( #[derive(Deserialize)] struct MedalQuery { - username: String, + username: String, +} + +#[derive(Serialize, utoipa::ToSchema)] +struct VerifyResponse { + user_id: UserId, + redeemed: bool, } /// Verify Medal credentials. -#[utoipa::path(tag = "medal")] +#[utoipa::path( + tag = "medal", + responses((status = OK, body = VerifyResponse)) +)] #[post("/verify", guard = "medal_key_guard")] pub async fn verify( pool: web::Data, @@ -47,14 +56,8 @@ pub async fn verify( ) .await?; - #[derive(Serialize)] - struct VerifyResponse { - user_id: UserId, - redeemed: bool, - } - - match maybe_fields { - None => Err(ApiError::NotFound), + match maybe_fields { + None => Err(ApiError::NotFound), Some(fields) => Ok(HttpResponse::Ok().json(VerifyResponse { user_id: fields.user_id.into(), redeemed: fields.redeemal_status.is_some(), @@ -63,7 +66,10 @@ pub async fn verify( } /// Redeem Medal credit. -#[utoipa::path(tag = "medal")] +#[utoipa::path( + tag = "medal", + responses((status = ACCEPTED), (status = CREATED)) +)] #[post("/redeem", guard = "medal_key_guard")] pub async fn redeem( pool: web::Data, diff --git a/apps/labrinth/src/routes/internal/moderation/external_license.rs b/apps/labrinth/src/routes/internal/moderation/external_license.rs index f762d7542b..2adaedc439 100644 --- a/apps/labrinth/src/routes/internal/moderation/external_license.rs +++ b/apps/labrinth/src/routes/internal/moderation/external_license.rs @@ -330,7 +330,10 @@ async fn fetch_by_flame_ids( } /// Search external licenses. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = inline(Vec))) +)] #[post("/search")] async fn search( req: HttpRequest, @@ -395,7 +398,10 @@ async fn search( } /// Look up external license metadata. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = ExternalLicenseLookupResponse)) +)] #[post("/lookup")] async fn lookup( req: HttpRequest, @@ -425,7 +431,10 @@ async fn lookup( } /// Get external license by SHA-1. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = ExternalProject)) +)] #[get("/by-sha1/{sha1}")] async fn get_by_sha1( req: HttpRequest, @@ -452,7 +461,10 @@ async fn get_by_sha1( } /// Get external licenses by SHA-1. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = inline(HashMap))) +)] #[post("/by-sha1")] async fn get_by_sha1_bulk( req: HttpRequest, @@ -477,7 +489,10 @@ async fn get_by_sha1_bulk( } /// Add an external license file. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = ExternalProject)) +)] #[post("/file")] async fn add_file( req: HttpRequest, @@ -490,7 +505,10 @@ async fn add_file( } /// Reassign an external license file. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = ExternalProject)) +)] #[post("/file/reassign")] async fn reassign_file( req: HttpRequest, @@ -591,7 +609,10 @@ async fn upsert_file_license( } /// Update an external license. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = ExternalProject)) +)] #[patch("/{id}")] async fn update_license( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index ddddd7e0af..6a6101daa6 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -295,7 +295,7 @@ pub async fn get_projects_internal( /// Get project moderation metadata. #[utoipa::path( tag = "moderation", - responses((status = OK, body = inline(Vec))) + responses((status = OK, body = MissingMetadata)) )] #[get("/project/{id}")] async fn get_project_meta( @@ -450,7 +450,10 @@ pub enum Judgement { } /// Update project moderation judgements. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = NO_CONTENT)) +)] #[post("/project")] async fn set_project_meta( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index a0442d07c2..9c0c3c4a70 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -668,7 +668,7 @@ async fn fetch_project_reports( #[utoipa::path( tag = "moderation", security(("bearer_auth" = [])), - responses((status = OK, body = inline(Vec))) + responses((status = OK, body = SearchResponse)) )] #[post("/search")] async fn search_projects( @@ -1281,7 +1281,10 @@ pub struct AddReport { /// Add a technical review report. /// does not already exist for it. -#[utoipa::path(tag = "moderation")] +#[utoipa::path( + tag = "moderation", + responses((status = OK, body = DelphiReportId)) +)] #[put("/report")] async fn add_report( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/mural.rs b/apps/labrinth/src/routes/internal/mural.rs index 7805c1417d..82bc9c9a0e 100644 --- a/apps/labrinth/src/routes/internal/mural.rs +++ b/apps/labrinth/src/routes/internal/mural.rs @@ -19,7 +19,10 @@ pub fn utoipa_config( } /// Get bank details. -#[utoipa::path(tag = "mural")] +#[utoipa::path( + tag = "mural", + responses((status = OK, body = serde_json::Value)) +)] #[get("/mural/bank-details")] async fn get_bank_details( payouts_queue: web::Data, diff --git a/apps/labrinth/src/routes/internal/pats.rs b/apps/labrinth/src/routes/internal/pats.rs index c5c8a8e342..406a3be1a2 100644 --- a/apps/labrinth/src/routes/internal/pats.rs +++ b/apps/labrinth/src/routes/internal/pats.rs @@ -33,11 +33,11 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { #[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")] @@ -88,12 +88,12 @@ pub struct NewPersonalAccessToken { #[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")] diff --git a/apps/labrinth/src/routes/internal/search.rs b/apps/labrinth/src/routes/internal/search.rs index bac8198222..b2da298349 100644 --- a/apps/labrinth/src/routes/internal/search.rs +++ b/apps/labrinth/src/routes/internal/search.rs @@ -10,16 +10,22 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { } /// List search tasks. -#[utoipa::path(tag = "search")] +#[utoipa::path( + tag = "search", + responses((status = OK, body = serde_json::Value)) +)] #[get("/tasks", guard = "admin_key_guard")] pub async fn tasks( - search: web::Data, + search: web::Data, ) -> Result, ApiError> { Ok(web::Json(search.tasks().await.map_err(ApiError::Internal)?)) } /// Cancel search tasks. -#[utoipa::path(tag = "search")] +#[utoipa::path( + tag = "search", + responses((status = NO_CONTENT)) +)] #[delete("/tasks", guard = "admin_key_guard")] pub async fn tasks_cancel( search: web::Data, diff --git a/apps/labrinth/src/routes/internal/server_ping.rs b/apps/labrinth/src/routes/internal/server_ping.rs index 14a86d1d20..37cfed34c9 100644 --- a/apps/labrinth/src/routes/internal/server_ping.rs +++ b/apps/labrinth/src/routes/internal/server_ping.rs @@ -23,7 +23,10 @@ pub struct PingRequest { } /// Ping Minecraft server. -#[utoipa::path(tag = "server ping")] +#[utoipa::path( + tag = "server ping", + responses((status = NO_CONTENT)) +)] #[post("/minecraft-java")] pub async fn ping_minecraft_java( req: HttpRequest, diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs index dded095001..1fcfbfea24 100644 --- a/apps/labrinth/src/routes/internal/session.rs +++ b/apps/labrinth/src/routes/internal/session.rs @@ -137,11 +137,11 @@ pub async fn issue_session( #[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")] @@ -236,11 +236,11 @@ pub async fn delete( #[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( diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs index 0ad007f87a..0d15916a57 100644 --- a/apps/labrinth/src/routes/internal/statuses.rs +++ b/apps/labrinth/src/routes/internal/statuses.rs @@ -52,7 +52,10 @@ struct LauncherHeartbeatInit { // TODO: Move launcher-specific tunnel traffic to a proper launcher websocket endpoint. /// Start launcher socket. -#[utoipa::path(tag = "statuses")] +#[utoipa::path( + tag = "statuses", + responses((status = 101)) +)] #[get("/launcher_socket")] pub async fn ws_init( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v2/moderation.rs b/apps/labrinth/src/routes/v2/moderation.rs index c133dc8453..3fb68a8d97 100644 --- a/apps/labrinth/src/routes/v2/moderation.rs +++ b/apps/labrinth/src/routes/v2/moderation.rs @@ -35,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)" diff --git a/apps/labrinth/src/routes/v2/notifications.rs b/apps/labrinth/src/routes/v2/notifications.rs index 7580d69113..101bdf9a67 100644 --- a/apps/labrinth/src/routes/v2/notifications.rs +++ b/apps/labrinth/src/routes/v2/notifications.rs @@ -40,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)" @@ -88,7 +88,7 @@ pub async fn 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)" diff --git a/apps/labrinth/src/routes/v2/project_creation.rs b/apps/labrinth/src/routes/v2/project_creation.rs index d85c407446..b7b2e13ed9 100644 --- a/apps/labrinth/src/routes/v2/project_creation.rs +++ b/apps/labrinth/src/routes/v2/project_creation.rs @@ -144,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, diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs index 511c451610..e5cdc3fcdd 100644 --- a/apps/labrinth/src/routes/v2/projects.rs +++ b/apps/labrinth/src/routes/v2/projects.rs @@ -81,7 +81,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { ) ), 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") ) )] @@ -195,7 +195,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") ) )] @@ -238,7 +238,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( @@ -278,7 +278,7 @@ pub async fn 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)" @@ -328,7 +328,7 @@ pub async fn project_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)" @@ -360,7 +360,7 @@ struct DependencyInfo { 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)" @@ -522,7 +522,7 @@ pub struct EditProject { 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)" @@ -787,7 +787,7 @@ pub struct BulkEditProject { ), request_body = BulkEditProject, responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -924,7 +924,7 @@ pub struct Extension { ) ), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error") ), security(("bearer_auth" = ["PROJECT_WRITE"])) @@ -965,7 +965,7 @@ pub async fn project_icon_edit( operation_id = "deleteProjectIcon", params(("id" = String, Path, description = "The ID or slug of the project")), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -1052,7 +1052,7 @@ pub struct GalleryCreateQuery { ) ), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -1153,7 +1153,7 @@ pub struct GalleryEditQuery { ) ), 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)" @@ -1208,7 +1208,7 @@ pub struct GalleryDeleteQuery { ("url" = String, Query, description = "URL of the image to delete") ), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -1248,7 +1248,7 @@ pub async fn delete_gallery_item( operation_id = "deleteProject", params(("id" = String, Path, description = "The ID or slug of the project")), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -1287,7 +1287,7 @@ pub async fn project_delete( operation_id = "followProject", params(("id" = String, Path, description = "The ID or slug of the project")), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -1317,7 +1317,7 @@ pub async fn project_follow( operation_id = "unfollowProject", params(("id" = String, Path, description = "The ID or slug of the project")), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), (status = 400, description = "Request was invalid, see given error"), ( status = 401, diff --git a/apps/labrinth/src/routes/v2/reports.rs b/apps/labrinth/src/routes/v2/reports.rs index 9f5348643b..eca90d142e 100644 --- a/apps/labrinth/src/routes/v2/reports.rs +++ b/apps/labrinth/src/routes/v2/reports.rs @@ -23,7 +23,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { post, operation_id = "submitReport", responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyReport), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -83,7 +83,7 @@ fn default_all() -> bool { ) ), 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)" @@ -146,7 +146,7 @@ pub struct ReportIds { ) ), 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)" @@ -194,7 +194,7 @@ pub async fn reports_get( operation_id = "getReport", params(("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyReport), ( status = 401, description = "Incorrect token scopes or no authorization to access the requested item(s)" diff --git a/apps/labrinth/src/routes/v2/teams.rs b/apps/labrinth/src/routes/v2/teams.rs index 08951095bb..353375ac98 100644 --- a/apps/labrinth/src/routes/v2/teams.rs +++ b/apps/labrinth/src/routes/v2/teams.rs @@ -37,7 +37,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { operation_id = "getProjectTeamMembers", params(("id" = String, Path, description = "The ID or slug of the project")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = Vec), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -81,7 +81,7 @@ pub async fn team_members_get_project( get, operation_id = "getTeamMembers", params(("id" = TeamId, Path, description = "The ID of the team")), - responses((status = 200, description = "Expected response to a valid request")), + responses((status = 200, description = "Expected response to a valid request", body = Vec)), security(("bearer_auth" = ["PROJECT_READ"])) )] #[get("/{id}/members")] @@ -120,7 +120,7 @@ pub struct TeamIds { get, operation_id = "getTeams", params(("ids" = String, Query, description = "The JSON array of team IDs")), - responses((status = 200, description = "Expected response to a valid request")) + responses((status = 200, description = "Expected response to a valid request", body = Vec>)) )] #[get("/teams")] pub async fn teams_get( diff --git a/apps/labrinth/src/routes/v2/threads.rs b/apps/labrinth/src/routes/v2/threads.rs index 581a4053a6..b8da38aa34 100644 --- a/apps/labrinth/src/routes/v2/threads.rs +++ b/apps/labrinth/src/routes/v2/threads.rs @@ -26,7 +26,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { operation_id = "getThread", params(("id" = ThreadId, Path, description = "The ID of the thread")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = Thread), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -59,7 +59,7 @@ pub struct ThreadIds { operation_id = "getThreads", params(("ids" = String, Query, description = "The JSON array of thread IDs")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = Vec), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" diff --git a/apps/labrinth/src/routes/v2/users.rs b/apps/labrinth/src/routes/v2/users.rs index 58388d45c0..7f63046afa 100644 --- a/apps/labrinth/src/routes/v2/users.rs +++ b/apps/labrinth/src/routes/v2/users.rs @@ -35,7 +35,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { get, operation_id = "getUserFromAuth", responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyUser), ( status = 401, description = "Incorrect token scopes or no authorization to access the requested item(s)" @@ -75,7 +75,7 @@ pub struct UserIds { get, operation_id = "getUsers", params(("ids" = String, Query, description = "The JSON array of user IDs")), - responses((status = 200, description = "Expected response to a valid request")) + responses((status = 200, description = "Expected response to a valid request", body = Vec)) )] #[get("/users")] pub async fn users_get( @@ -113,7 +113,7 @@ pub async fn users_get( operation_id = "getUser", params(("id" = String, Path, description = "The ID or username of the user")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyUser), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -149,7 +149,7 @@ pub async fn user_get( operation_id = "getUserProjects", params(("user_id" = String, Path, description = "The ID or username of the user")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = Vec), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -402,7 +402,7 @@ pub async fn user_delete( operation_id = "getFollowedProjects", params(("id" = String, Path, description = "The ID or username of the user")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 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)" @@ -450,7 +450,7 @@ pub async fn user_follows( operation_id = "getUserNotifications", params(("id" = String, Path, description = "The ID or username of the user")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 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)" diff --git a/apps/labrinth/src/routes/v2/version_creation.rs b/apps/labrinth/src/routes/v2/version_creation.rs index 11975a448b..9990f95141 100644 --- a/apps/labrinth/src/routes/v2/version_creation.rs +++ b/apps/labrinth/src/routes/v2/version_creation.rs @@ -86,7 +86,7 @@ pub struct InitialVersionData { 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 = LegacyVersion), (status = 400, description = "Request was invalid, see given error"), ( status = 401, @@ -317,7 +317,7 @@ async fn get_example_version_fields( description = "Multipart payload containing files to upload" ), 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)" diff --git a/apps/labrinth/src/routes/v2/version_file.rs b/apps/labrinth/src/routes/v2/version_file.rs index 6dfd4b9eea..4e0ac19dc1 100644 --- a/apps/labrinth/src/routes/v2/version_file.rs +++ b/apps/labrinth/src/routes/v2/version_file.rs @@ -5,7 +5,7 @@ use crate::database::redis::RedisPool; use crate::models::projects::{Project, Version, VersionType}; use crate::models::v2::projects::{LegacyProject, LegacyVersion}; use crate::queue::session::AuthQueue; -use crate::routes::v3::version_file::HashQuery; +use crate::routes::v3::version_file::{DownloadRedirect, HashQuery}; use crate::routes::{v2_reroute, v3}; use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use serde::{Deserialize, Serialize}; @@ -54,7 +54,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { ) ), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyVersion), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -115,7 +115,7 @@ pub async fn get_version_from_hash( ) ), responses( - (status = 302, description = "Temporary redirect to file URL"), + (status = 302, description = "Temporary redirect to file URL", body = DownloadRedirect), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -168,7 +168,7 @@ pub async fn download_version( ) ), 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)" @@ -233,7 +233,7 @@ pub struct UpdateData { ), request_body = UpdateData, responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyVersion), (status = 400, description = "Request was invalid, see given error"), ( status = 404, @@ -303,7 +303,7 @@ pub struct FileHashes { operation_id = "versionsFromHashes", request_body = FileHashes, responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = HashMap), (status = 400, description = "Request was invalid, see given error") ) )] @@ -355,7 +355,7 @@ pub async fn get_versions_from_hashes( operation_id = "projectsFromHashes", request_body = FileHashes, responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = HashMap), (status = 400, description = "Request was invalid, see given error") ) )] @@ -433,7 +433,7 @@ pub struct ManyUpdateData { operation_id = "getLatestVersionsFromHashes", request_body = ManyUpdateData, responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = HashMap), (status = 400, description = "Request was invalid, see given error") ) )] @@ -483,7 +483,7 @@ pub async fn update_files( operation_id = "getLatestVersionsFromHashesMany", request_body = ManyUpdateData, responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = HashMap>), (status = 400, description = "Request was invalid, see given error") ) )] @@ -550,7 +550,7 @@ pub struct ManyFileUpdateData { operation_id = "getLatestVersionsFromHashesIndividual", request_body = ManyFileUpdateData, responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = HashMap), (status = 400, description = "Request was invalid, see given error") ) )] diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs index 88ee07ab10..b92415cdbf 100644 --- a/apps/labrinth/src/routes/v2/versions.rs +++ b/apps/labrinth/src/routes/v2/versions.rs @@ -77,7 +77,7 @@ fn default_true() -> bool { ) ), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = Vec), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -188,7 +188,7 @@ pub async fn version_list( ) ), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyVersion), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -238,7 +238,7 @@ pub struct VersionIds { get, operation_id = "getVersions", params(("ids" = String, Query, description = "The JSON array of version IDs")), - responses((status = 200, description = "Expected response to a valid request")) + responses((status = 200, description = "Expected response to a valid request", body = Vec)) )] #[get("/versions")] pub async fn versions_get( @@ -284,7 +284,7 @@ pub async fn versions_get( operation_id = "getVersion", params(("version_id" = models::ids::VersionId, Path, description = "The ID of the version")), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = LegacyVersion), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" @@ -365,7 +365,7 @@ pub struct EditVersionFileType { params(("id" = VersionId, Path, description = "The ID of the version")), request_body = EditVersion, 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)" @@ -480,7 +480,7 @@ pub async fn version_edit( operation_id = "deleteVersion", params(("version_id" = VersionId, Path, description = "The ID of the version")), responses( - (status = 204, description = "Expected response to a valid request"), + (status = NO_CONTENT, description = "Expected response to a valid request"), ( status = 401, description = "Incorrect token scopes or no authorization to access the requested item(s)" diff --git a/apps/labrinth/src/routes/v3/analytics_get/old.rs b/apps/labrinth/src/routes/v3/analytics_get/old.rs index ea348597a2..632c06c7e5 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/old.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/old.rs @@ -49,7 +49,10 @@ pub struct GetData { } /// Get playtime data. -#[utoipa::path(tag = "analytics")] +#[utoipa::path( + tag = "analytics", + responses((status = OK, body = HashMap>)), +)] #[get("/playtime")] pub async fn playtimes_get( req: HttpRequest, @@ -121,7 +124,10 @@ pub async fn playtimes_get( /// } ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -#[utoipa::path(tag = "analytics")] +#[utoipa::path( + tag = "analytics", + responses((status = OK, body = HashMap>)), +)] #[get("/views")] pub async fn views_get( req: HttpRequest, @@ -193,7 +199,10 @@ pub async fn views_get( /// } ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -#[utoipa::path(tag = "analytics")] +#[utoipa::path( + tag = "analytics", + responses((status = OK, body = HashMap>)), +)] #[get("/downloads")] pub async fn downloads_get( req: HttpRequest, @@ -266,7 +275,10 @@ pub async fn downloads_get( /// } ///} /// ONLY project IDs can be used. Unauthorized projects will be filtered out. -#[utoipa::path(tag = "analytics")] +#[utoipa::path( + tag = "analytics", + responses((status = OK, body = HashMap>)), +)] #[get("/revenue")] pub async fn revenue_get( req: HttpRequest, @@ -408,7 +420,10 @@ pub async fn revenue_get( ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. /// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch -#[utoipa::path(tag = "analytics")] +#[utoipa::path( + tag = "analytics", + responses((status = OK, body = HashMap>)), +)] #[get("/countries/downloads")] pub async fn countries_downloads_get( req: HttpRequest, @@ -484,7 +499,10 @@ pub async fn countries_downloads_get( ///} /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. /// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch -#[utoipa::path(tag = "analytics")] +#[utoipa::path( + tag = "analytics", + responses((status = OK, body = HashMap>)), +)] #[get("/countries/views")] pub async fn countries_views_get( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/content/mod.rs b/apps/labrinth/src/routes/v3/content/mod.rs index 11b73fb018..dc571b6fd9 100644 --- a/apps/labrinth/src/routes/v3/content/mod.rs +++ b/apps/labrinth/src/routes/v3/content/mod.rs @@ -38,7 +38,11 @@ pub fn utoipa_config( } /// Resolve content. -#[utoipa::path(tag = "content", request_body = serde_json::Value)] +#[utoipa::path( + tag = "content", + request_body = serde_json::Value, + responses((status = OK, body = serde_json::Value)), +)] #[post("/content/resolve")] async fn resolve_content( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/friends.rs b/apps/labrinth/src/routes/v3/friends.rs index b621d3cf8d..5bed3aac78 100644 --- a/apps/labrinth/src/routes/v3/friends.rs +++ b/apps/labrinth/src/routes/v3/friends.rs @@ -24,7 +24,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { } /// Add a friend. -#[utoipa::path(tag = "friends")] +#[utoipa::path(tag = "friends", responses((status = NO_CONTENT)))] #[post("/friend/{id}")] pub async fn add_friend( req: HttpRequest, @@ -136,7 +136,7 @@ pub async fn add_friend( } /// Remove a friend. -#[utoipa::path(tag = "friends")] +#[utoipa::path(tag = "friends", responses((status = NO_CONTENT)))] #[delete("/friend/{id}")] pub async fn remove_friend( req: HttpRequest, @@ -180,7 +180,7 @@ pub async fn remove_friend( } /// List friends. -#[utoipa::path(tag = "friends")] +#[utoipa::path(tag = "friends", responses((status = OK, body = Vec)))] #[get("/friends")] pub async fn friends( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index 477949f6ae..82ffbc168c 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -101,7 +101,10 @@ pub async fn get_user_clients( } /// Get an OAuth client. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path( + tag = "oauth clients", + responses((status = OK, body = models::oauth_clients::OAuthClient)), +)] #[get("/app/{id}")] pub async fn get_client( id: web::Path, @@ -116,7 +119,10 @@ pub async fn get_client( } /// List OAuth clients. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path( + tag = "oauth clients", + responses((status = OK, body = Vec)), +)] #[get("/apps")] pub async fn get_clients( info: web::Query, @@ -159,7 +165,10 @@ pub struct NewOAuthApp { } /// Create an OAuth client. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path( + tag = "oauth clients", + responses((status = OK, body = OAuthClientCreationResult)), +)] #[post("/app")] pub async fn oauth_client_create( req: HttpRequest, @@ -222,7 +231,7 @@ pub async fn oauth_client_create( } /// Delete an OAuth client. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path(tag = "oauth clients", responses((status = NO_CONTENT)))] #[delete("/app/{id}")] pub async fn oauth_client_delete( req: HttpRequest, @@ -280,7 +289,7 @@ pub struct OAuthClientEdit { } /// Update an OAuth client. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path(tag = "oauth clients", responses((status = NO_CONTENT)))] #[patch("/app/{id}")] pub async fn oauth_client_edit( req: HttpRequest, @@ -357,7 +366,7 @@ pub struct Extension { } /// Update an OAuth client icon. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path(tag = "oauth clients", responses((status = NO_CONTENT)))] #[patch("/app/{id}/icon")] #[allow(clippy::too_many_arguments)] pub async fn oauth_client_icon_edit( @@ -431,7 +440,7 @@ pub async fn oauth_client_icon_edit( } /// Delete an OAuth client icon. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path(tag = "oauth clients", responses((status = NO_CONTENT)))] #[delete("/app/{id}/icon")] pub async fn oauth_client_icon_delete( req: HttpRequest, @@ -483,7 +492,10 @@ pub async fn oauth_client_icon_delete( } /// List OAuth authorizations. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path( + tag = "oauth clients", + responses((status = OK, body = Vec)), +)] #[get("/authorizations")] pub async fn get_user_oauth_authorizations( req: HttpRequest, @@ -514,7 +526,7 @@ pub async fn get_user_oauth_authorizations( } /// Revoke OAuth authorization. -#[utoipa::path(tag = "oauth clients")] +#[utoipa::path(tag = "oauth clients", responses((status = NO_CONTENT)))] #[delete("/authorizations")] pub async fn revoke_oauth_authorization( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index f131fba4f1..cbc3ed2cc1 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -7,7 +7,9 @@ use crate::database::redis::RedisPool; use crate::env::ENV; use crate::models::ids::PayoutId; use crate::models::pats::Scopes; -use crate::models::payouts::{PayoutMethodType, PayoutStatus, Withdrawal}; +use crate::models::payouts::{ + PayoutMethod, PayoutMethodType, PayoutStatus, Withdrawal, +}; use crate::queue::payouts::PayoutsQueue; use crate::queue::session::AuthQueue; use crate::routes::ApiError; @@ -52,7 +54,10 @@ pub struct RequestForm { } /// Submit a compliance form. -#[utoipa::path(tag = "payouts")] +#[utoipa::path( + tag = "payouts", + responses((status = OK, body = serde_json::Value)), +)] #[post("/compliance")] pub async fn post_compliance_form( req: HttpRequest, @@ -420,7 +425,7 @@ pub async fn tremendous_webhook( Ok(HttpResponse::NoContent().finish()) } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct WithdrawalFees { pub net_usd: Decimal2dp, pub fee: Decimal2dp, @@ -428,7 +433,10 @@ pub struct WithdrawalFees { } /// Calculate payout fees. -#[utoipa::path(tag = "payouts")] +#[utoipa::path( + tag = "payouts", + responses((status = OK, body = WithdrawalFees)), +)] #[post("/fees")] pub async fn calculate_fees( req: HttpRequest, @@ -462,7 +470,7 @@ pub async fn calculate_fees( } /// Create a payout. -#[utoipa::path(tag = "payouts")] +#[utoipa::path(tag = "payouts", responses((status = NO_CONTENT)))] #[post("")] pub async fn create_payout( req: HttpRequest, @@ -760,7 +768,7 @@ pub async fn transaction_history( } /// Cancel a payout. -#[utoipa::path(tag = "payouts")] +#[utoipa::path(tag = "payouts", responses((status = NO_CONTENT)))] #[delete("/{id}")] pub async fn cancel_payout( info: web::Path<(PayoutId,)>, @@ -869,7 +877,7 @@ pub struct MethodFilter { pub country: Option, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "kebab-case")] pub enum FormCompletionStatus { Unknown, @@ -880,7 +888,10 @@ pub enum FormCompletionStatus { } /// List payment methods. -#[utoipa::path(tag = "payouts")] +#[utoipa::path( + tag = "payouts", + responses((status = OK, body = Vec)), +)] #[get("/methods")] pub async fn payment_methods( payouts_queue: web::Data, @@ -904,7 +915,7 @@ pub async fn payment_methods( Ok(HttpResponse::Ok().json(methods)) } -#[derive(Serialize)] +#[derive(Serialize, utoipa::ToSchema)] pub struct UserBalance { pub available: Decimal, pub withdrawn_lifetime: Decimal, @@ -913,8 +924,19 @@ pub struct UserBalance { pub dates: HashMap, Decimal>, } +#[derive(Serialize, utoipa::ToSchema)] +pub struct BalanceResponse { + #[serde(flatten)] + balance: UserBalance, + requested_form_type: Option, + form_completion_status: Option, +} + /// Get account balance. -#[utoipa::path(tag = "payouts")] +#[utoipa::path( + tag = "payouts", + responses((status = OK, body = BalanceResponse)), +)] #[get("/balance")] pub async fn get_balance( req: HttpRequest, @@ -932,14 +954,6 @@ pub async fn get_balance( .await? .1; - #[derive(Serialize)] - struct Response { - #[serde(flatten)] - balance: UserBalance, - requested_form_type: Option, - form_completion_status: Option, - } - let balance = get_user_balance(user.id.into(), &pool).await?; let mut requested_form_type = None; @@ -973,7 +987,7 @@ pub async fn get_balance( ); } - Ok(HttpResponse::Ok().json(Response { + Ok(HttpResponse::Ok().json(BalanceResponse { balance, requested_form_type, form_completion_status, @@ -1131,14 +1145,14 @@ pub struct RevenueQuery { pub end: Option>, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct RevenueResponse { pub all_time: Decimal, pub all_time_available: Decimal, pub data: Vec, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct RevenueData { pub time: u64, pub revenue: Decimal, @@ -1146,7 +1160,10 @@ pub struct RevenueData { } /// Get platform revenue. -#[utoipa::path(tag = "payouts")] +#[utoipa::path( + tag = "payouts", + responses((status = OK, body = RevenueResponse)), +)] #[get("/platform_revenue")] pub async fn platform_revenue( query: web::Query, diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 626d22d872..8aa5a3fd52 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -15,7 +15,7 @@ use crate::models::ids::{ImageId, OrganizationId, ProjectId, VersionId}; use crate::models::images::{Image, ImageContext}; use crate::models::pats::Scopes; use crate::models::projects::{ - License, Link, MonetizationStatus, ProjectStatus, + License, Link, MonetizationStatus, Project, ProjectStatus, SideTypesMigrationReviewStatus, VersionStatus, }; use crate::models::teams::{OrganizationPermissions, ProjectPermissions}; @@ -284,7 +284,7 @@ pub async fn undo_uploads( } /// Create a project. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = OK, body = Project)))] #[post("")] pub async fn project_create( req: HttpRequest, @@ -365,7 +365,7 @@ pub async fn project_create_internal( /// Create a project with a specific ID. /// /// This is a testing endpoint only accessible behind an admin key. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = OK, body = Project)))] #[post("/{id}", guard = "admin_key_guard")] pub async fn project_create_with_id( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/project_creation/new.rs b/apps/labrinth/src/routes/v3/project_creation/new.rs index 0974983e29..cfc2314bad 100644 --- a/apps/labrinth/src/routes/v3/project_creation/new.rs +++ b/apps/labrinth/src/routes/v3/project_creation/new.rs @@ -114,7 +114,7 @@ pub struct ProjectCreate { /// /// Components must include `base` ([`exp::base::Project`]), and at least one /// other component. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = OK, body = ProjectId)))] #[put("")] pub async fn create( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 5886f577c7..b50fbc8268 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -43,7 +43,6 @@ use eyre::eyre; use futures::TryStreamExt; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use serde_json::json; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { @@ -141,11 +140,16 @@ pub async fn random_projects_get( Ok(HttpResponse::Ok().json(projects_data)) } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct ProjectIds { pub ids: String, } +#[derive(Serialize, utoipa::ToSchema)] +pub struct ProjectCheckResponse { + pub id: ProjectId, +} + pub async fn projects_get( req: HttpRequest, web::Query(ids): web::Query, @@ -176,7 +180,7 @@ pub async fn projects_get( } /// Get a project. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = OK, body = Project)))] #[get("/{id}")] async fn project_get( req: HttpRequest, @@ -308,7 +312,7 @@ pub struct EditProject { #[allow(clippy::too_many_arguments)] /// Update a project. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[patch("/{id}")] async fn project_edit( req: HttpRequest, @@ -1263,7 +1267,7 @@ pub async fn edit_project_categories( ) ), responses( - (status = 200, description = "Expected response to a valid request"), + (status = 200, description = "Expected response to a valid request", body = SearchResults), (status = 400, description = "Request was invalid, see given error") ) )] @@ -1294,7 +1298,11 @@ pub async fn project_search( // for more complicated search queries /// Search projects. -#[utoipa::path(tag = "search", request_body = serde_json::Value)] +#[utoipa::path( + tag = "search", + request_body = serde_json::Value, + responses((status = OK, body = SearchResults)) +)] #[post("/search")] pub async fn project_search_post( web::Json(info): web::Json, @@ -1307,7 +1315,7 @@ pub async fn project_search_post( //checks the validity of a project id or slug /// Check project availability. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = OK, body = ProjectCheckResponse)))] #[get("/{id}/check")] async fn project_get_check( info: web::Path<(String,)>, @@ -1328,22 +1336,22 @@ pub async fn project_get_check_internal( db_models::DBProject::get(&slug, &**pool, &redis).await?; if let Some(project) = project_data { - Ok(HttpResponse::Ok().json(json! ({ - "id": models::ids::ProjectId::from(project.inner.id) - }))) + Ok(HttpResponse::Ok().json(ProjectCheckResponse { + id: models::ids::ProjectId::from(project.inner.id), + })) } else { Err(ApiError::NotFound) } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct DependencyInfo { pub projects: Vec, pub versions: Vec, } /// List project dependencies. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = OK, body = DependencyInfo)))] #[get("/{project_id}/dependencies")] pub async fn dependency_list( req: HttpRequest, @@ -1790,7 +1798,7 @@ pub struct Extension { #[allow(clippy::too_many_arguments)] /// Update a project icon. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[patch("/{id}/icon")] async fn project_icon_edit( web::Query(ext): web::Query, @@ -1935,7 +1943,7 @@ pub async fn project_icon_edit_internal( } /// Delete a project icon. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[delete("/{id}/icon")] async fn delete_project_icon( req: HttpRequest, @@ -2062,7 +2070,7 @@ pub struct GalleryCreateQuery { #[allow(clippy::too_many_arguments)] /// Add a gallery item. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[post("/{id}/gallery")] pub async fn add_gallery_item( web::Query(ext): web::Query, @@ -2261,7 +2269,7 @@ pub struct GalleryEditQuery { } /// Update a gallery item. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[patch("/{id}/gallery")] async fn edit_gallery_item( req: HttpRequest, @@ -2451,7 +2459,7 @@ pub struct GalleryDeleteQuery { } /// Delete a gallery item. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[delete("/{id}/gallery")] async fn delete_gallery_item( req: HttpRequest, @@ -2587,7 +2595,7 @@ pub async fn delete_gallery_item_internal( } /// Delete a project. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[delete("/{id}")] async fn project_delete( req: HttpRequest, @@ -2746,7 +2754,7 @@ pub async fn project_delete_internal( } /// Follow a project. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[post("/{id}/follow")] async fn project_follow( req: HttpRequest, @@ -2839,7 +2847,7 @@ pub async fn project_follow_internal( } /// Unfollow a project. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] #[delete("/{id}/follow")] async fn project_unfollow( req: HttpRequest, @@ -2928,7 +2936,7 @@ pub async fn project_unfollow_internal( } /// Get a project's organization. -#[utoipa::path(tag = "projects")] +#[utoipa::path(tag = "projects", responses((status = OK, body = models::organizations::Organization)))] #[get("/{id}/organization")] pub async fn project_get_organization( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs b/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs index 9fdc289025..e273638e1c 100644 --- a/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs +++ b/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs @@ -36,6 +36,26 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } +/// Create a shared instance version. +#[utoipa::path( + tag = "versions", + post, + path = "/v3/shared-instance/{id}/version", + params(("id" = SharedInstanceId, Path, description = "The ID of the shared instance")), + responses( + (status = 201, description = "Expected response to a valid request", body = SharedInstanceVersion), + (status = 400, description = "Request was invalid, see given error"), + ( + status = 401, + description = "Incorrect token scopes or no authorization to access the requested item(s)" + ), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ), + security(("bearer_auth" = ["SHARED_INSTANCE_VERSION_CREATE"])) +)] #[allow(clippy::too_many_arguments)] pub async fn shared_instance_version_create( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index 43e29f300a..3b63ed414d 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -41,7 +41,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { // (Unlike team_members_get_project, which only returns the members of the project's team) // They can be differentiated by the "organization_permissions" field being null or not /// Get a project's team members. -#[utoipa::path(tag = "teams")] +#[utoipa::path(tag = "teams", responses((status = OK, body = Vec)))] #[get("/{project_id}/members")] async fn team_members_get_project( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 9749efee3c..061a118d4a 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -105,6 +105,25 @@ struct InitialFileData { } // under `/api/v1/version` +/// Create a version on an existing project. +#[utoipa::path( + tag = "versions", + post, + path = "/v3/version", + request_body( + content(("multipart/form-data")), + description = "Multipart payload containing `data` and uploaded files" + ), + responses( + (status = 200, description = "Expected response to a valid request", body = Version), + (status = 400, description = "Request was invalid, see given error"), + ( + status = 401, + description = "Incorrect token scopes or no authorization to access the requested item(s)" + ) + ), + security(("bearer_auth" = ["VERSION_CREATE"])) +)] pub async fn version_create( req: HttpRequest, mut payload: Multipart, @@ -546,6 +565,29 @@ async fn version_create_inner( Ok((HttpResponse::Ok().json(response), project_id)) } +/// Add files to an existing version. +#[utoipa::path( + tag = "versions", + post, + path = "/v3/version/{version_id}/file", + params(("version_id" = VersionId, Path, description = "The ID of the version")), + request_body( + content(("multipart/form-data")), + description = "Multipart payload containing files to upload" + ), + responses( + (status = NO_CONTENT, description = "Expected response to a valid request"), + ( + status = 401, + description = "Incorrect token scopes or no authorization to access the requested item(s)" + ), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ), + security(("bearer_auth" = ["VERSION_WRITE"])) +)] pub async fn upload_file_to_version( req: HttpRequest, url_data: web::Path<(VersionId,)>, diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index 2318da25eb..9df1068e38 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -753,7 +753,7 @@ pub async fn update_individual_files( ) ), 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)" @@ -931,7 +931,7 @@ pub struct DownloadRedirect { ) ), responses( - (status = 302, description = "Temporary redirect to file URL"), + (status = 302, description = "Temporary redirect to file URL", body = DownloadRedirect), ( status = 404, description = "The requested item(s) were not found or no authorization to access the requested item(s)" diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index bfb1c400b8..3be6823a11 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -58,7 +58,16 @@ pub fn config(cfg: &mut web::ServiceConfig) { // Given a project ID/slug and a version slug /// Get a project version. -#[utoipa::path(tag = "versions")] +#[utoipa::path( + tag = "versions", + responses( + (status = 200, description = "Expected response to a valid request", body = models::projects::Version), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] #[get("/{project_id}/version/{slug}")] pub async fn version_project_get( req: HttpRequest, @@ -163,6 +172,21 @@ fn default_true() -> bool { true } +/// Get multiple versions by ID. +#[utoipa::path( + tag = "versions", + get, + path = "/v3/versions", + params( + ("ids" = String, Query, description = "The JSON array of version IDs"), + ( + "include_changelog" = Option, + Query, + description = "Whether to include changelog fields" + ) + ), + responses((status = 200, description = "Expected response to a valid request", body = Vec)) +)] pub async fn versions_get( req: HttpRequest, web::Query(ids): web::Query, @@ -212,6 +236,20 @@ pub async fn versions_get( Ok(HttpResponse::Ok().json(versions)) } +/// Get a version by ID. +#[utoipa::path( + tag = "versions", + get, + path = "/v3/version/{id}", + params(("id" = VersionId, Path, description = "The ID of the version")), + responses( + (status = 200, description = "Expected response to a valid request", body = models::projects::Version), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] pub async fn version_get( req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, @@ -331,6 +369,25 @@ pub struct EditVersionFileType { pub file_type: Option, } +/// Update an existing version. +#[utoipa::path( + tag = "versions", + patch, + path = "/v3/version/{id}", + params(("id" = VersionId, Path, description = "The ID of the version")), + responses( + (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)" + ), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ), + security(("bearer_auth" = ["VERSION_WRITE"])) +)] pub async fn version_edit( req: HttpRequest, info: web::Path<(VersionId,)>, @@ -800,7 +857,16 @@ pub struct VersionListFilters { } /// List project versions. -#[utoipa::path(tag = "versions")] +#[utoipa::path( + tag = "versions", + responses( + (status = 200, description = "Expected response to a valid request", body = Vec), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ) +)] #[get("/{project_id}/version")] async fn version_list( req: HttpRequest, @@ -992,6 +1058,25 @@ pub async fn version_list_internal( } } +/// Delete a version by ID. +#[utoipa::path( + tag = "versions", + delete, + path = "/v3/version/{id}", + params(("id" = VersionId, Path, description = "The ID of the version")), + responses( + (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)" + ), + ( + status = 404, + description = "The requested item(s) were not found or no authorization to access the requested item(s)" + ) + ), + security(("bearer_auth" = ["VERSION_DELETE"])) +)] pub async fn version_delete( req: HttpRequest, info: web::Path<(VersionId,)>, diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 9507aafc2e..cfef656bc1 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -285,7 +285,7 @@ pub struct UploadSearchProject { pub loader_fields: HashMap>, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)] pub struct SearchProjectDependency { pub project_id: String, pub dependency_type: DependencyType, @@ -294,7 +294,7 @@ pub struct SearchProjectDependency { pub icon_url: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, ToSchema)] pub struct SearchResults { pub hits: Vec, pub page: usize, @@ -302,7 +302,7 @@ pub struct SearchResults { pub total_hits: usize, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)] pub struct ResultSearchProject { pub version_id: String, pub project_id: String, From 7f8c7f8b3900fbad19dfef66871474ea2914457a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Wed, 1 Jul 2026 17:32:02 -0400 Subject: [PATCH 5/7] fix: restore labrinth docs routes --- apps/labrinth/src/lib.rs | 17 +++++++-- apps/labrinth/src/main.rs | 5 ++- apps/labrinth/src/models/v2/notifications.rs | 2 + apps/labrinth/src/models/v2/threads.rs | 4 +- apps/labrinth/src/models/v3/billing.rs | 10 ++++- apps/labrinth/src/models/v3/payouts.rs | 12 +++--- apps/labrinth/src/routes/internal/billing.rs | 38 +++++++++---------- .../routes/internal/external_notifications.rs | 4 +- apps/labrinth/src/routes/internal/globals.rs | 2 +- apps/labrinth/src/routes/internal/medal.rs | 10 ++--- apps/labrinth/src/routes/internal/mod.rs | 4 ++ .../src/routes/internal/moderation/mod.rs | 12 ++++++ apps/labrinth/src/routes/internal/search.rs | 2 +- apps/labrinth/src/routes/v2/mod.rs | 2 + .../src/routes/v3/analytics_get/facets/mod.rs | 4 ++ .../src/routes/v3/analytics_get/mod.rs | 6 +++ .../src/routes/v3/analytics_get/old.rs | 9 +++++ apps/labrinth/src/routes/v3/collections.rs | 16 ++++---- apps/labrinth/src/routes/v3/images.rs | 2 +- apps/labrinth/src/routes/v3/limits.rs | 8 ++-- apps/labrinth/src/routes/v3/mod.rs | 12 +++++- apps/labrinth/src/routes/v3/notifications.rs | 14 +++---- apps/labrinth/src/routes/v3/oauth_clients.rs | 2 +- apps/labrinth/src/routes/v3/organizations.rs | 24 ++++++------ apps/labrinth/src/routes/v3/payouts.rs | 24 +++++++++--- .../src/routes/v3/project_creation.rs | 29 ++++++++++++++ .../src/routes/v3/project_creation/new.rs | 4 ++ apps/labrinth/src/routes/v3/projects.rs | 25 ++++++++++-- apps/labrinth/src/routes/v3/reports.rs | 12 +++--- .../src/routes/v3/shared_instances.rs | 25 ++++++------ apps/labrinth/src/routes/v3/statistics.rs | 2 +- apps/labrinth/src/routes/v3/tags.rs | 20 +++++----- apps/labrinth/src/routes/v3/teams.rs | 16 ++++---- apps/labrinth/src/routes/v3/threads.rs | 10 ++--- apps/labrinth/src/routes/v3/users.rs | 36 +++++++++--------- apps/labrinth/src/routes/v3/version_file.rs | 23 ++++++----- apps/labrinth/src/routes/v3/versions.rs | 14 +++---- apps/labrinth/src/test/api_v2/mod.rs | 5 ++- apps/labrinth/src/test/api_v3/mod.rs | 5 ++- 39 files changed, 310 insertions(+), 161 deletions(-) diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index a012735776..8fd7cfc09b 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_base_config(cfg, labrinth_config); + app_fallback_config(cfg); +} + +pub fn app_base_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() @@ -400,9 +408,12 @@ pub fn app_config( .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().to(routes::not_found).wrap(default_cors())); + .configure(routes::internal::config); +} + +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 utoipa_app_config( diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index 8010ede9f7..b508cc5996 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -15,7 +15,7 @@ use labrinth::search; use labrinth::util::anrok; use labrinth::util::gotenberg::GotenbergClient; use labrinth::util::ratelimit::rate_limit_middleware; -use labrinth::{app_config, env}; +use labrinth::{app_base_config, app_fallback_config, env}; use labrinth::{clickhouse, database, file_hosting}; use labrinth::{ utoipa_app_config_internal, utoipa_app_config_v2, utoipa_app_config_v3, @@ -264,6 +264,7 @@ 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) + .configure(|cfg| app_base_config(cfg, labrinth_config.clone())) .into_utoipa_app() .openapi(DocsV2::openapi()) .configure(|cfg| utoipa_app_config_v2(cfg, labrinth_config.clone())) @@ -343,7 +344,7 @@ async fn app() -> std::io::Result<()> { docs_internal, )) .configure(scalar_config("/docs", &scalar_configuration)) - .configure(|cfg| app_config(cfg, labrinth_config.clone())) + .configure(app_fallback_config) }) .bind(&ENV.BIND_ADDR)? .run() diff --git a/apps/labrinth/src/models/v2/notifications.rs b/apps/labrinth/src/models/v2/notifications.rs index 7b41e5949c..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::{ diff --git a/apps/labrinth/src/models/v2/threads.rs b/apps/labrinth/src/models/v2/threads.rs index c7cd1e1736..ddaa6d0729 100644 --- a/apps/labrinth/src/models/v2/threads.rs +++ b/apps/labrinth/src/models/v2/threads.rs @@ -49,7 +49,9 @@ pub enum LegacyMessageBody { }, } -#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone, utoipa::ToSchema)] +#[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/v3/billing.rs b/apps/labrinth/src/models/v3/billing.rs index 9832c48c6d..c3ef13facb 100644 --- a/apps/labrinth/src/models/v3/billing.rs +++ b/apps/labrinth/src/models/v3/billing.rs @@ -76,7 +76,15 @@ impl Price { } #[derive( - Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone, utoipa::ToSchema, + Serialize, + Deserialize, + Hash, + Eq, + PartialEq, + Debug, + Copy, + Clone, + utoipa::ToSchema, )] #[serde(rename_all = "kebab-case")] pub enum PriceDuration { diff --git a/apps/labrinth/src/models/v3/payouts.rs b/apps/labrinth/src/models/v3/payouts.rs index e628f61b71..221205e95c 100644 --- a/apps/labrinth/src/models/v3/payouts.rs +++ b/apps/labrinth/src/models/v3/payouts.rs @@ -275,12 +275,12 @@ impl PayoutMethodFee { 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() - } + fn schema() -> utoipa::openapi::RefOr { + utoipa::openapi::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Number) + .build() + .into() + } } impl utoipa::ToSchema for PayoutDecimal {} diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index c031f04e3e..a56ece18ac 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -1514,17 +1514,17 @@ pub async fn payment_methods( #[derive(Deserialize)] pub struct ActiveServersQuery { - pub subscription_status: Option, + pub subscription_status: Option, } #[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, + 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. @@ -1557,25 +1557,25 @@ pub async fn active_servers( ) .await?; - let server_ids = servers - .into_iter() - .filter_map(|x| { - x.metadata.as_ref().and_then(|metadata| match metadata { - SubscriptionMetadata::Pyro { id, region } => { - Some(ActiveServerResponse { - user_id: x.user_id.into(), - server_id: id.clone(), - price_id: x.price_id.into(), + let server_ids = servers + .into_iter() + .filter_map(|x| { + x.metadata.as_ref().and_then(|metadata| match metadata { + SubscriptionMetadata::Pyro { id, region } => { + Some(ActiveServerResponse { + user_id: x.user_id.into(), + server_id: id.clone(), + price_id: x.price_id.into(), interval: x.interval, region: region.clone(), }) } SubscriptionMetadata::Medal { .. } => None, }) - }) - .collect::>(); + }) + .collect::>(); - Ok(HttpResponse::Ok().json(server_ids)) + Ok(HttpResponse::Ok().json(server_ids)) } #[derive(Deserialize, utoipa::ToSchema)] diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs index bb86ee2e0e..b76132a44b 100644 --- a/apps/labrinth/src/routes/internal/external_notifications.rs +++ b/apps/labrinth/src/routes/internal/external_notifications.rs @@ -106,8 +106,8 @@ pub async fn create( ) )] #[post( - "/external_notifications/email-sync", - guard = "external_notification_key_guard" + "/external_notifications/email-sync", + guard = "external_notification_key_guard" )] pub async fn create_email_sync( pool: web::Data, diff --git a/apps/labrinth/src/routes/internal/globals.rs b/apps/labrinth/src/routes/internal/globals.rs index b28ea4145c..aeb3c9eb1c 100644 --- a/apps/labrinth/src/routes/internal/globals.rs +++ b/apps/labrinth/src/routes/internal/globals.rs @@ -96,7 +96,7 @@ pub fn tax_compliance_payout_threshold_for_year( )] #[get("")] pub async fn get_globals() -> web::Json { - web::Json(GLOBALS.clone()) + web::Json(GLOBALS.clone()) } #[cfg(test)] diff --git a/apps/labrinth/src/routes/internal/medal.rs b/apps/labrinth/src/routes/internal/medal.rs index cf02ae6bda..4803d898a1 100644 --- a/apps/labrinth/src/routes/internal/medal.rs +++ b/apps/labrinth/src/routes/internal/medal.rs @@ -29,13 +29,13 @@ pub fn utoipa_config( #[derive(Deserialize)] struct MedalQuery { - username: String, + username: String, } #[derive(Serialize, utoipa::ToSchema)] struct VerifyResponse { - user_id: UserId, - redeemed: bool, + user_id: UserId, + redeemed: bool, } /// Verify Medal credentials. @@ -56,8 +56,8 @@ pub async fn verify( ) .await?; - match maybe_fields { - None => Err(ApiError::NotFound), + match maybe_fields { + None => Err(ApiError::NotFound), Some(fields) => Ok(HttpResponse::Ok().json(VerifyResponse { user_id: fields.user_id.into(), redeemed: fields.redeemal_status.is_some(), diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index 08314324be..b1cb4dc275 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -63,6 +63,10 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(pats::create_pat); cfg.service(pats::edit_pat); cfg.service(pats::delete_pat); + cfg.service( + actix_web::web::scope("/moderation") + .configure(moderation::web_config), + ); }) .configure(oauth_clients::config) .configure(billing::config) diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index 6a6101daa6..70ba294102 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -45,6 +45,18 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { ); } +pub fn web_config(cfg: &mut web::ServiceConfig) { + cfg.service(get_projects) + .service(get_project_meta) + .service(set_project_meta) + .service(acquire_lock) + .service(override_lock) + .service(get_lock_status) + .service(release_lock) + .service(release_lock_beacon) + .service(delete_all_locks); +} + #[derive(Deserialize, utoipa::ToSchema)] pub struct ProjectsRequestOptions { /// How many projects to fetch. diff --git a/apps/labrinth/src/routes/internal/search.rs b/apps/labrinth/src/routes/internal/search.rs index b2da298349..63c99675f3 100644 --- a/apps/labrinth/src/routes/internal/search.rs +++ b/apps/labrinth/src/routes/internal/search.rs @@ -16,7 +16,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { )] #[get("/tasks", guard = "admin_key_guard")] pub async fn tasks( - search: web::Data, + search: web::Data, ) -> Result, ApiError> { Ok(web::Json(search.tasks().await.map_err(ApiError::Internal)?)) } diff --git a/apps/labrinth/src/routes/v2/mod.rs b/apps/labrinth/src/routes/v2/mod.rs index f9bc20865b..fed5f3826c 100644 --- a/apps/labrinth/src/routes/v2/mod.rs +++ b/apps/labrinth/src/routes/v2/mod.rs @@ -24,6 +24,8 @@ pub fn utoipa_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) .configure(projects::config) diff --git a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs index 8755374832..b5d019155c 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs @@ -18,6 +18,10 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(fetch_facets); } +pub fn web_config(cfg: &mut web::ServiceConfig) { + cfg.service(fetch_facets); +} + #[derive(Debug, Serialize, utoipa::ToSchema)] pub struct FacetsResponse { pub facets: AnalyticsFacets, diff --git a/apps/labrinth/src/routes/v3/analytics_get/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/mod.rs index d43e05fded..4ddea33c1b 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/mod.rs @@ -60,6 +60,12 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.configure(old::config); } +pub fn web_config(cfg: &mut web::ServiceConfig) { + cfg.service(fetch_analytics); + cfg.configure(facets::web_config); + cfg.configure(old::web_config); +} + // request /// Requests analytics data, aggregating over all possible analytics sources diff --git a/apps/labrinth/src/routes/v3/analytics_get/old.rs b/apps/labrinth/src/routes/v3/analytics_get/old.rs index 632c06c7e5..a0026b9bcb 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/old.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/old.rs @@ -30,6 +30,15 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { .service(countries_views_get); } +pub fn web_config(cfg: &mut web::ServiceConfig) { + cfg.service(playtimes_get) + .service(views_get) + .service(downloads_get) + .service(revenue_get) + .service(countries_downloads_get) + .service(countries_views_get); +} + /// The json data to be passed to fetch analytic data. /// /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. diff --git a/apps/labrinth/src/routes/v3/collections.rs b/apps/labrinth/src/routes/v3/collections.rs index 605f857065..ed4a1ab2cb 100644 --- a/apps/labrinth/src/routes/v3/collections.rs +++ b/apps/labrinth/src/routes/v3/collections.rs @@ -28,16 +28,16 @@ use serde::{Deserialize, Serialize}; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("collections", web::get().to(collections_get)); - cfg.route("collection", web::post().to(collection_create)); + cfg.route("/collections", web::get().to(collections_get)); + cfg.route("/collection", web::post().to(collection_create)); cfg.service( - web::scope("collection") - .route("{id}", web::get().to(collection_get)) - .route("{id}", web::delete().to(collection_delete)) - .route("{id}", web::patch().to(collection_edit)) - .route("{id}/icon", web::patch().to(collection_icon_edit)) - .route("{id}/icon", web::delete().to(delete_collection_icon)), + web::scope("/collection") + .route("/{id}", web::get().to(collection_get)) + .route("/{id}", web::delete().to(collection_delete)) + .route("/{id}", web::patch().to(collection_edit)) + .route("/{id}/icon", web::patch().to(collection_icon_edit)) + .route("/{id}/icon", web::delete().to(delete_collection_icon)), ); } diff --git a/apps/labrinth/src/routes/v3/images.rs b/apps/labrinth/src/routes/v3/images.rs index d00e03946c..95a98b0b2d 100644 --- a/apps/labrinth/src/routes/v3/images.rs +++ b/apps/labrinth/src/routes/v3/images.rs @@ -18,7 +18,7 @@ use actix_web::{HttpRequest, HttpResponse, web}; use serde::{Deserialize, Serialize}; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("image", web::post().to(images_add)); + cfg.route("/image", web::post().to(images_add)); } #[derive(Serialize, Deserialize)] diff --git a/apps/labrinth/src/routes/v3/limits.rs b/apps/labrinth/src/routes/v3/limits.rs index 5dfbbef251..3b71d2d91d 100644 --- a/apps/labrinth/src/routes/v3/limits.rs +++ b/apps/labrinth/src/routes/v3/limits.rs @@ -10,10 +10,10 @@ use actix_web::{HttpRequest, web}; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope("limits") - .route("projects", web::get().to(get_project_limits)) - .route("organizations", web::get().to(get_organization_limits)) - .route("collections", web::get().to(get_collection_limits)), + web::scope("/limits") + .route("/projects", web::get().to(get_project_limits)) + .route("/organizations", web::get().to(get_organization_limits)) + .route("/collections", web::get().to(get_collection_limits)), ); } diff --git a/apps/labrinth/src/routes/v3/mod.rs b/apps/labrinth/src/routes/v3/mod.rs index 87ce0070be..7b266e26de 100644 --- a/apps/labrinth/src/routes/v3/mod.rs +++ b/apps/labrinth/src/routes/v3/mod.rs @@ -31,16 +31,23 @@ pub mod oauth_clients; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope("v3") + web::scope("/v3") .wrap(default_cors()) .configure(limits::config) .configure(collections::config) .configure(content::config) .configure(images::config) .configure(notifications::config) + .configure(oauth_clients::config) .configure(organizations::config) .configure(payouts::webhook_config) + .configure(payouts::web_config) .configure(projects::config) + .service( + web::scope("/project") + .configure(project_creation::web_config) + .configure(projects::project_config), + ) .configure(reports::config) .configure(shared_instance_version_creation::config) .configure(shared_instances::config) @@ -51,6 +58,9 @@ pub fn config(cfg: &mut web::ServiceConfig) { .configure(users::config) .configure(version_file::config) .configure(versions::config) + .service( + web::scope("/analytics").configure(analytics_get::web_config), + ) .configure(friends::config), ); } diff --git a/apps/labrinth/src/routes/v3/notifications.rs b/apps/labrinth/src/routes/v3/notifications.rs index b04c83390b..4cd3787ad9 100644 --- a/apps/labrinth/src/routes/v3/notifications.rs +++ b/apps/labrinth/src/routes/v3/notifications.rs @@ -11,15 +11,15 @@ use actix_web::{HttpRequest, HttpResponse, web}; use serde::{Deserialize, Serialize}; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("notifications", web::get().to(notifications_get)); - cfg.route("notifications", web::patch().to(notifications_read)); - cfg.route("notifications", web::delete().to(notifications_delete)); + cfg.route("/notifications", web::get().to(notifications_get)); + cfg.route("/notifications", web::patch().to(notifications_read)); + cfg.route("/notifications", web::delete().to(notifications_delete)); cfg.service( - web::scope("notification") - .route("{id}", web::get().to(notification_get)) - .route("{id}", web::patch().to(notification_read)) - .route("{id}", web::delete().to(notification_delete)), + web::scope("/notification") + .route("/{id}", web::get().to(notification_get)) + .route("/{id}", web::patch().to(notification_read)) + .route("/{id}", web::delete().to(notification_delete)), ); } diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index 82ffbc168c..663f22a8f8 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -43,7 +43,7 @@ use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( - scope("oauth") + scope("/oauth") .configure(crate::auth::oauth::config) .service(revoke_oauth_authorization) .service(oauth_client_create) diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index b59e74cefd..5f77b3479f 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -29,24 +29,24 @@ use serde::{Deserialize, Serialize}; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("organizations", web::get().to(organizations_get)); + cfg.route("/organizations", web::get().to(organizations_get)); cfg.service( - web::scope("organization") + web::scope("/organization") .route("", web::post().to(organization_create)) - .route("{id}/projects", web::get().to(organization_projects_get)) - .route("{id}/notes", web::patch().to(organization_notes_edit)) - .route("{id}", web::get().to(organization_get)) - .route("{id}", web::patch().to(organizations_edit)) - .route("{id}", web::delete().to(organization_delete)) - .route("{id}/projects", web::post().to(organization_projects_add)) + .route("/{id}/projects", web::get().to(organization_projects_get)) + .route("/{id}/notes", web::patch().to(organization_notes_edit)) + .route("/{id}", web::get().to(organization_get)) + .route("/{id}", web::patch().to(organizations_edit)) + .route("/{id}", web::delete().to(organization_delete)) + .route("/{id}/projects", web::post().to(organization_projects_add)) .route( - "{id}/projects/{project_id}", + "/{id}/projects/{project_id}", web::delete().to(organization_projects_remove), ) - .route("{id}/icon", web::patch().to(organization_icon_edit)) - .route("{id}/icon", web::delete().to(delete_organization_icon)) + .route("/{id}/icon", web::patch().to(organization_icon_edit)) + .route("/{id}/icon", web::delete().to(delete_organization_icon)) .route( - "{id}/members", + "/{id}/members", web::get().to(super::teams::team_members_get_organization), ), ); diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index cbc3ed2cc1..d21cfc3e5b 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -8,7 +8,7 @@ use crate::env::ENV; use crate::models::ids::PayoutId; use crate::models::pats::Scopes; use crate::models::payouts::{ - PayoutMethod, PayoutMethodType, PayoutStatus, Withdrawal, + PayoutMethod, PayoutMethodType, PayoutStatus, Withdrawal, }; use crate::queue::payouts::PayoutsQueue; use crate::queue::session::AuthQueue; @@ -44,6 +44,20 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { .service(post_compliance_form); } +pub fn web_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/payout") + .service(transaction_history) + .service(calculate_fees) + .service(create_payout) + .service(cancel_payout) + .service(payment_methods) + .service(get_balance) + .service(platform_revenue) + .service(post_compliance_form), + ); +} + pub fn webhook_config(cfg: &mut web::ServiceConfig) { cfg.service(paypal_webhook).service(tremendous_webhook); } @@ -926,10 +940,10 @@ pub struct UserBalance { #[derive(Serialize, utoipa::ToSchema)] pub struct BalanceResponse { - #[serde(flatten)] - balance: UserBalance, - requested_form_type: Option, - form_completion_status: Option, + #[serde(flatten)] + balance: UserBalance, + requested_form_type: Option, + form_completion_status: Option, } /// Get account balance. diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 8aa5a3fd52..1344890aa1 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -52,6 +52,12 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { .configure(new::config); } +pub fn web_config(cfg: &mut web::ServiceConfig) { + cfg.route("", web::post().to(project_create_web)) + .service(project_create_with_id) + .configure(new::web_config); +} + #[derive(Error, Debug)] pub enum CreateError { #[error("An unknown database error occurred")] @@ -309,6 +315,29 @@ pub async fn project_create( .await } +async fn project_create_web( + req: HttpRequest, + payload: Multipart, + client: Data, + redis: Data, + file_host: Data, + session_queue: Data, + http: Data, + search_state: Data, +) -> Result { + project_create_internal( + req, + payload, + client, + redis, + file_host, + session_queue, + http, + search_state, + ) + .await +} + pub async fn project_create_internal( req: HttpRequest, mut payload: Multipart, diff --git a/apps/labrinth/src/routes/v3/project_creation/new.rs b/apps/labrinth/src/routes/v3/project_creation/new.rs index cfc2314bad..8d72ae7bc0 100644 --- a/apps/labrinth/src/routes/v3/project_creation/new.rs +++ b/apps/labrinth/src/routes/v3/project_creation/new.rs @@ -40,6 +40,10 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { cfg.service(create); } +pub fn web_config(cfg: &mut web::ServiceConfig) { + cfg.service(create); +} + #[derive(Debug, thiserror::Error)] pub enum CreateError { #[error("project limit reached")] diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index b50fbc8268..dbede369ba 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -48,9 +48,28 @@ use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(project_search); cfg.service(project_search_post); - cfg.route("projects", web::get().to(projects_get)); - cfg.route("projects", web::patch().to(projects_edit)); - cfg.route("projects_random", web::get().to(random_projects_get)); + cfg.route("/projects", web::get().to(projects_get)); + cfg.route("/projects", web::patch().to(projects_edit)); + cfg.route("/projects_random", web::get().to(random_projects_get)); +} + +pub fn project_config(cfg: &mut web::ServiceConfig) { + cfg.service(project_get) + .service(project_get_check) + .service(project_delete) + .service(project_edit) + .service(project_icon_edit) + .service(delete_project_icon) + .service(add_gallery_item) + .service(edit_gallery_item) + .service(delete_gallery_item) + .service(project_follow) + .service(project_unfollow) + .service(project_get_organization) + .service(super::teams::team_members_get_project) + .service(super::versions::version_list) + .service(super::versions::version_project_get) + .service(dependency_list); } pub fn utoipa_config( diff --git a/apps/labrinth/src/routes/v3/reports.rs b/apps/labrinth/src/routes/v3/reports.rs index e13f4ae572..2bf0355720 100644 --- a/apps/labrinth/src/routes/v3/reports.rs +++ b/apps/labrinth/src/routes/v3/reports.rs @@ -26,12 +26,12 @@ use serde::Deserialize; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("report", web::post().to(report_create)); - cfg.route("report", web::get().to(reports)); - cfg.route("reports", web::get().to(reports_get)); - cfg.route("report/{id}", web::get().to(report_get)); - cfg.route("report/{id}", web::patch().to(report_edit)); - cfg.route("report/{id}", web::delete().to(report_delete)); + cfg.route("/report", web::post().to(report_create)); + cfg.route("/report", web::get().to(reports)); + cfg.route("/reports", web::get().to(reports_get)); + cfg.route("/report/{id}", web::get().to(report_get)); + cfg.route("/report/{id}", web::patch().to(report_edit)); + cfg.route("/report/{id}", web::delete().to(report_delete)); } #[derive(Deserialize, Validate)] diff --git a/apps/labrinth/src/routes/v3/shared_instances.rs b/apps/labrinth/src/routes/v3/shared_instances.rs index 3768777029..2786595fc0 100644 --- a/apps/labrinth/src/routes/v3/shared_instances.rs +++ b/apps/labrinth/src/routes/v3/shared_instances.rs @@ -25,21 +25,24 @@ use serde::Deserialize; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("shared-instance", web::post().to(shared_instance_create)); - cfg.route("shared-instance", web::get().to(shared_instance_list)); + cfg.route("/shared-instance", web::post().to(shared_instance_create)); + cfg.route("/shared-instance", web::get().to(shared_instance_list)); cfg.service( - web::scope("shared-instance") - .route("{id}", web::get().to(shared_instance_get)) - .route("{id}", web::patch().to(shared_instance_edit)) - .route("{id}", web::delete().to(shared_instance_delete)) - .route("{id}/version", web::get().to(shared_instance_version_list)), + web::scope("/shared-instance") + .route("/{id}", web::get().to(shared_instance_get)) + .route("/{id}", web::patch().to(shared_instance_edit)) + .route("/{id}", web::delete().to(shared_instance_delete)) + .route( + "/{id}/version", + web::get().to(shared_instance_version_list), + ), ); cfg.service( - web::scope("shared-instance-version") - .route("{id}", web::get().to(shared_instance_version_get)) - .route("{id}", web::delete().to(shared_instance_version_delete)) + web::scope("/shared-instance-version") + .route("/{id}", web::get().to(shared_instance_version_get)) + .route("/{id}", web::delete().to(shared_instance_version_delete)) .route( - "{id}/download", + "/{id}/download", web::get().to(shared_instance_version_download), ), ); diff --git a/apps/labrinth/src/routes/v3/statistics.rs b/apps/labrinth/src/routes/v3/statistics.rs index a0f7141d9d..258af981b3 100644 --- a/apps/labrinth/src/routes/v3/statistics.rs +++ b/apps/labrinth/src/routes/v3/statistics.rs @@ -3,7 +3,7 @@ use crate::routes::ApiError; use actix_web::{HttpResponse, web}; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("statistics", web::get().to(get_stats)); + cfg.route("/statistics", web::get().to(get_stats)); } #[derive(serde::Serialize, serde::Deserialize)] diff --git a/apps/labrinth/src/routes/v3/tags.rs b/apps/labrinth/src/routes/v3/tags.rs index 175ee06016..a8340d07b2 100644 --- a/apps/labrinth/src/routes/v3/tags.rs +++ b/apps/labrinth/src/routes/v3/tags.rs @@ -16,17 +16,17 @@ use serde_json::Value; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope("tag") - .route("category", web::get().to(category_list)) - .route("loader", web::get().to(loader_list)), + web::scope("/tag") + .route("/category", web::get().to(category_list)) + .route("/loader", web::get().to(loader_list)), ) - .route("games", web::get().to(games_list)) - .route("loader_field", web::get().to(loader_fields_list)) - .route("license", web::get().to(license_list)) - .route("license/{id}", web::get().to(license_text)) - .route("link_platform", web::get().to(link_platform_list)) - .route("report_type", web::get().to(report_type_list)) - .route("project_type", web::get().to(project_type_list)); + .route("/games", web::get().to(games_list)) + .route("/loader_field", web::get().to(loader_fields_list)) + .route("/license", web::get().to(license_list)) + .route("/license/{id}", web::get().to(license_text)) + .route("/link_platform", web::get().to(link_platform_list)) + .route("/report_type", web::get().to(report_type_list)) + .route("/project_type", web::get().to(project_type_list)); } #[derive(serde::Serialize, serde::Deserialize)] diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index 3b63ed414d..f31a8cf0ab 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -19,19 +19,19 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("teams", web::get().to(teams_get)); + cfg.route("/teams", web::get().to(teams_get)); cfg.service( - web::scope("team") - .route("{id}/members", web::get().to(team_members_get)) - .route("{id}/members/{user_id}", web::patch().to(edit_team_member)) + web::scope("/team") + .route("/{id}/members", web::get().to(team_members_get)) + .route("/{id}/members/{user_id}", web::patch().to(edit_team_member)) .route( - "{id}/members/{user_id}", + "/{id}/members/{user_id}", web::delete().to(remove_team_member), ) - .route("{id}/members", web::post().to(add_team_member)) - .route("{id}/join", web::post().to(join_team)) - .route("{id}/owner", web::patch().to(transfer_ownership)), + .route("/{id}/members", web::post().to(add_team_member)) + .route("/{id}/join", web::post().to(join_team)) + .route("/{id}/owner", web::patch().to(transfer_ownership)), ); } diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 23ce400feb..2f66b06b29 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -22,14 +22,14 @@ use serde::Deserialize; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope("thread") - .route("{id}", web::get().to(thread_get)) - .route("{id}", web::post().to(thread_send_message)), + web::scope("/thread") + .route("/{id}", web::get().to(thread_get)) + .route("/{id}", web::post().to(thread_send_message)), ); cfg.service( - web::scope("message").route("{id}", web::delete().to(message_delete)), + web::scope("/message").route("/{id}", web::delete().to(message_delete)), ); - cfg.route("threads", web::get().to(threads_get)); + cfg.route("/threads", web::get().to(threads_get)); } pub async fn is_authorized_thread( diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index c03a6088c8..5da14c7173 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -37,26 +37,26 @@ use serde::{Deserialize, Serialize}; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("user", web::get().to(user_auth_get)); - cfg.route("users", web::get().to(users_get)); - cfg.route("users/search", web::get().to(users_search)); - cfg.route("user_email", web::get().to(admin_user_email)); + cfg.route("/user", web::get().to(user_auth_get)); + cfg.route("/users", web::get().to(users_get)); + cfg.route("/users/search", web::get().to(users_search)); + cfg.route("/user_email", web::get().to(admin_user_email)); cfg.service( - web::scope("user") - .route("{user_id}/all-projects", web::get().to(all_projects)) - .route("{user_id}/projects", web::get().to(projects_list)) - .route("{id}/notes", web::patch().to(user_notes_edit)) - .route("{id}", web::get().to(user_get)) - .route("{user_id}/collections", web::get().to(collections_list)) - .route("{user_id}/organizations", web::get().to(orgs_list)) - .route("{id}", web::patch().to(user_edit)) - .route("{id}/icon", web::patch().to(user_icon_edit)) - .route("{id}/icon", web::delete().to(user_icon_delete)) - .route("{id}", web::delete().to(user_delete)) - .route("{id}/follows", web::get().to(user_follows)) - .route("{id}/notifications", web::get().to(user_notifications)) - .route("{id}/oauth_apps", web::get().to(get_user_clients)), + web::scope("/user") + .route("/{user_id}/all-projects", web::get().to(all_projects)) + .route("/{user_id}/projects", web::get().to(projects_list)) + .route("/{id}/notes", web::patch().to(user_notes_edit)) + .route("/{id}", web::get().to(user_get)) + .route("/{user_id}/collections", web::get().to(collections_list)) + .route("/{user_id}/organizations", web::get().to(orgs_list)) + .route("/{id}", web::patch().to(user_edit)) + .route("/{id}/icon", web::patch().to(user_icon_edit)) + .route("/{id}/icon", web::delete().to(user_icon_delete)) + .route("/{id}", web::delete().to(user_delete)) + .route("/{id}/follows", web::get().to(user_follows)) + .route("/{id}/notifications", web::get().to(user_notifications)) + .route("/{id}/oauth_apps", web::get().to(get_user_clients)), ); } diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index 9df1068e38..43bd3f73c3 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -20,20 +20,23 @@ use std::collections::HashMap; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope("version_file") - .route("{version_id}", web::get().to(get_version_from_hash)) - .route("{version_id}/update", web::post().to(get_update_from_hash)) - .route("project", web::post().to(get_projects_from_hashes)) - .route("{version_id}", web::delete().to(delete_file)) - .route("{version_id}/download", web::get().to(download_version)), + web::scope("/version_file") + .route("/{version_id}", web::get().to(get_version_from_hash)) + .route("/{version_id}/update", web::post().to(get_update_from_hash)) + .route("/project", web::post().to(get_projects_from_hashes)) + .route("/{version_id}", web::delete().to(delete_file)) + .route("/{version_id}/download", web::get().to(download_version)), ); cfg.service( - web::scope("version_files") + web::scope("/version_files") // DEPRECATED - use `update_many` instead // see `fn update_files` comment - .route("update", web::post().to(update_files)) - .route("update_many", web::post().to(update_files_many)) - .route("update_individual", web::post().to(update_individual_files)) + .route("/update", web::post().to(update_files)) + .route("/update_many", web::post().to(update_files_many)) + .route( + "/update_individual", + web::post().to(update_individual_files), + ) .route("", web::post().to(get_versions_from_hashes)), ); } diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 3be6823a11..acf0a52a97 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -39,18 +39,18 @@ use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { cfg.route( - "version", + "/version", web::post().to(super::version_creation::version_create), ); - cfg.route("versions", web::get().to(versions_get)); + cfg.route("/versions", web::get().to(versions_get)); cfg.service( - web::scope("version") - .route("{id}", web::get().to(version_get)) - .route("{id}", web::patch().to(version_edit)) - .route("{id}", web::delete().to(version_delete)) + web::scope("/version") + .route("/{id}", web::get().to(version_get)) + .route("/{id}", web::patch().to(version_edit)) + .route("/{id}", web::delete().to(version_delete)) .route( - "{version_id}/file", + "/{version_id}/file", web::post().to(super::version_creation::upload_file_to_version), ), ); diff --git a/apps/labrinth/src/test/api_v2/mod.rs b/apps/labrinth/src/test/api_v2/mod.rs index 6ce5151881..e7085910d2 100644 --- a/apps/labrinth/src/test/api_v2/mod.rs +++ b/apps/labrinth/src/test/api_v2/mod.rs @@ -25,12 +25,15 @@ pub struct ApiV2 { impl ApiBuildable for ApiV2 { async fn build(labrinth_config: LabrinthConfig) -> Self { let app = App::new() + .configure(|cfg| { + crate::app_base_config(cfg, labrinth_config.clone()) + }) .into_utoipa_app() .configure(|cfg| { crate::utoipa_app_config(cfg, labrinth_config.clone()) }) .into_app() - .configure(|cfg| crate::app_config(cfg, labrinth_config.clone())); + .configure(crate::app_fallback_config); let test_app: Rc = Rc::new(test::init_service(app).await); diff --git a/apps/labrinth/src/test/api_v3/mod.rs b/apps/labrinth/src/test/api_v3/mod.rs index 3cfd373699..e7587f88c7 100644 --- a/apps/labrinth/src/test/api_v3/mod.rs +++ b/apps/labrinth/src/test/api_v3/mod.rs @@ -30,12 +30,15 @@ pub struct ApiV3 { impl ApiBuildable for ApiV3 { async fn build(labrinth_config: LabrinthConfig) -> Self { let app = App::new() + .configure(|cfg| { + crate::app_base_config(cfg, labrinth_config.clone()) + }) .into_utoipa_app() .configure(|cfg| { crate::utoipa_app_config(cfg, labrinth_config.clone()) }) .into_app() - .configure(|cfg| crate::app_config(cfg, labrinth_config.clone())); + .configure(crate::app_fallback_config); let test_app: Rc = Rc::new(test::init_service(app).await); From bd17856bde3b650d74d7b38986a4f08f94578244 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:56:24 +0100 Subject: [PATCH 6/7] Fix path parameter docs in routes --- .../src/routes/internal/attribution.rs | 1 + .../src/routes/internal/moderation/mod.rs | 5 + apps/labrinth/src/routes/v2/projects.rs | 25 + apps/labrinth/src/routes/v2/versions.rs | 24 +- .../src/routes/v3/analytics_get/old.rs | 36 + apps/labrinth/src/routes/v3/payouts.rs | 5 + apps/labrinth/src/routes/v3/projects.rs | 36 +- apps/labrinth/src/routes/v3/versions.rs | 9 + scripts/labrinth-openapi-check.ts | 1013 +++++++++++++++++ 9 files changed, 1149 insertions(+), 5 deletions(-) create mode 100644 scripts/labrinth-openapi-check.ts diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs index 8e6e3895cf..13c893adf4 100644 --- a/apps/labrinth/src/routes/internal/attribution.rs +++ b/apps/labrinth/src/routes/internal/attribution.rs @@ -208,6 +208,7 @@ async fn scan( /// List project attribution groups. #[utoipa::path( tag = "attribution", + params(("project_id" = ProjectId, Path)), responses((status = OK, body = inline(Vec))) )] #[get("/{project_id}")] diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index 70ba294102..e5fe4a794c 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -177,6 +177,11 @@ pub struct DeleteAllLocksResponse { /// 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")] diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs index e5cdc3fcdd..ca7401ff6d 100644 --- a/apps/labrinth/src/routes/v2/projects.rs +++ b/apps/labrinth/src/routes/v2/projects.rs @@ -78,6 +78,31 @@ 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( diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs index b92415cdbf..9417bf8bcf 100644 --- a/apps/labrinth/src/routes/v2/versions.rs +++ b/apps/labrinth/src/routes/v2/versions.rs @@ -70,6 +70,21 @@ fn default_true() -> bool { Query, description = "Filter by featured status" ), + ( + "version_type" = Option, + Query, + description = "Filter by version type" + ), + ( + "limit" = Option, + Query, + description = "Maximum number of versions" + ), + ( + "offset" = Option, + Query, + description = "Version offset" + ), ( "include_changelog" = Option, Query, @@ -237,7 +252,14 @@ pub struct VersionIds { tag = "versions", get, operation_id = "getVersions", - params(("ids" = String, Query, description = "The JSON array of version IDs")), + params( + ("ids" = String, Query, description = "The JSON array of version IDs"), + ( + "include_changelog" = Option, + Query, + description = "Whether to include changelog fields" + ) + ), responses((status = 200, description = "Expected response to a valid request", body = Vec)) )] #[get("/versions")] diff --git a/apps/labrinth/src/routes/v3/analytics_get/old.rs b/apps/labrinth/src/routes/v3/analytics_get/old.rs index a0026b9bcb..05e1822815 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/old.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/old.rs @@ -60,6 +60,12 @@ pub struct GetData { /// Get playtime data. #[utoipa::path( tag = "analytics", + params( + ("project_ids" = Option, Query), + ("start_date" = Option, Query), + ("end_date" = Option, Query), + ("resolution_minutes" = Option, Query) + ), responses((status = OK, body = HashMap>)), )] #[get("/playtime")] @@ -135,6 +141,12 @@ pub async fn playtimes_get( /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. #[utoipa::path( tag = "analytics", + params( + ("project_ids" = Option, Query), + ("start_date" = Option, Query), + ("end_date" = Option, Query), + ("resolution_minutes" = Option, Query) + ), responses((status = OK, body = HashMap>)), )] #[get("/views")] @@ -210,6 +222,12 @@ pub async fn views_get( /// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. #[utoipa::path( tag = "analytics", + params( + ("project_ids" = Option, Query), + ("start_date" = Option, Query), + ("end_date" = Option, Query), + ("resolution_minutes" = Option, Query) + ), responses((status = OK, body = HashMap>)), )] #[get("/downloads")] @@ -286,6 +304,12 @@ pub async fn downloads_get( /// ONLY project IDs can be used. Unauthorized projects will be filtered out. #[utoipa::path( tag = "analytics", + params( + ("project_ids" = Option, Query), + ("start_date" = Option, Query), + ("end_date" = Option, Query), + ("resolution_minutes" = Option, Query) + ), responses((status = OK, body = HashMap>)), )] #[get("/revenue")] @@ -431,6 +455,12 @@ pub async fn revenue_get( /// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch #[utoipa::path( tag = "analytics", + params( + ("project_ids" = Option, Query), + ("start_date" = Option, Query), + ("end_date" = Option, Query), + ("resolution_minutes" = Option, Query) + ), responses((status = OK, body = HashMap>)), )] #[get("/countries/downloads")] @@ -510,6 +540,12 @@ pub async fn countries_downloads_get( /// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch #[utoipa::path( tag = "analytics", + params( + ("project_ids" = Option, Query), + ("start_date" = Option, Query), + ("end_date" = Option, Query), + ("resolution_minutes" = Option, Query) + ), responses((status = OK, body = HashMap>)), )] #[get("/countries/views")] diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index d21cfc3e5b..668e854766 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -904,6 +904,7 @@ pub enum FormCompletionStatus { /// List payment methods. #[utoipa::path( tag = "payouts", + params(("country" = Option, Query)), responses((status = OK, body = Vec)), )] #[get("/methods")] @@ -1176,6 +1177,10 @@ pub struct RevenueData { /// Get platform revenue. #[utoipa::path( tag = "payouts", + params( + ("start" = Option, Query), + ("end" = Option, Query) + ), responses((status = OK, body = RevenueResponse)), )] #[get("/platform_revenue")] diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index dbede369ba..a7d65ea24c 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -1817,7 +1817,11 @@ pub struct Extension { #[allow(clippy::too_many_arguments)] /// Update a project icon. -#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] +#[utoipa::path( + tag = "projects", + params(("ext" = String, Query)), + responses((status = NO_CONTENT)) +)] #[patch("/{id}/icon")] async fn project_icon_edit( web::Query(ext): web::Query, @@ -2089,7 +2093,17 @@ pub struct GalleryCreateQuery { #[allow(clippy::too_many_arguments)] /// Add a gallery item. -#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] +#[utoipa::path( + tag = "projects", + params( + ("ext" = String, Query), + ("featured" = bool, Query), + ("name" = Option, Query), + ("description" = Option, Query), + ("ordering" = Option, Query) + ), + responses((status = NO_CONTENT)) +)] #[post("/{id}/gallery")] pub async fn add_gallery_item( web::Query(ext): web::Query, @@ -2288,7 +2302,17 @@ pub struct GalleryEditQuery { } /// Update a gallery item. -#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] +#[utoipa::path( + tag = "projects", + params( + ("url" = String, Query), + ("featured" = Option, Query), + ("name" = Option, Query), + ("description" = Option, Query), + ("ordering" = Option, Query) + ), + responses((status = NO_CONTENT)) +)] #[patch("/{id}/gallery")] async fn edit_gallery_item( req: HttpRequest, @@ -2478,7 +2502,11 @@ pub struct GalleryDeleteQuery { } /// Delete a gallery item. -#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] +#[utoipa::path( + tag = "projects", + params(("url" = String, Query)), + responses((status = NO_CONTENT)) +)] #[delete("/{id}/gallery")] async fn delete_gallery_item( req: HttpRequest, diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index acf0a52a97..329430b26f 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -859,6 +859,15 @@ pub struct VersionListFilters { /// List project versions. #[utoipa::path( tag = "versions", + params( + ("loaders" = Option, Query), + ("featured" = Option, Query), + ("version_type" = Option, Query), + ("limit" = Option, Query), + ("offset" = Option, Query), + ("loader_fields" = Option, Query), + ("include_changelog" = Option, Query) + ), responses( (status = 200, description = "Expected response to a valid request", body = Vec), ( diff --git a/scripts/labrinth-openapi-check.ts b/scripts/labrinth-openapi-check.ts new file mode 100644 index 0000000000..2e75b75dbd --- /dev/null +++ b/scripts/labrinth-openapi-check.ts @@ -0,0 +1,1013 @@ +import chalk from 'chalk' +import * as fs from 'node:fs' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace'] as const +const BODY_EXTRACTORS = ['json', 'form', 'payload', 'bytes', 'multipart', 'text'] as const +const AUTH_HEADERS = { + authorization: 'Bearer mra_admin', + 'modrinth-admin': 'feedbeef', +} +const IGNORED_PROBE_OPERATIONS = new Set([ + 'POST /v2/admin/_force_reindex', + 'POST /v2/admin/_force_reindex/{project_id}', +]) + +type HttpMethod = (typeof HTTP_METHODS)[number] +type BodyExtractor = (typeof BODY_EXTRACTORS)[number] + +interface Args { + baseUrl: string + openapi?: string + sourceDir: string + probe: boolean + staticCheck: boolean + includeMutatingProbes: boolean + json: boolean + verbose: boolean +} + +interface OpenApiSpec { + paths?: Record> + components?: unknown +} + +interface OpenApiOperation { + operationId?: string + parameters?: OpenApiParameter[] + requestBody?: OpenApiRequestBody | { $ref: string } + responses?: unknown + [key: string]: unknown +} + +interface OpenApiParameter { + name?: string + in?: string + required?: boolean + schema?: OpenApiSchema +} + +interface OpenApiRequestBody { + required?: boolean + content?: Record +} + +interface OpenApiSchema { + type?: string | string[] + format?: string + $ref?: string + oneOf?: OpenApiSchema[] + anyOf?: OpenApiSchema[] + allOf?: OpenApiSchema[] +} + +interface Operation { + path: string + method: HttpMethod + operationId: string + operation: OpenApiOperation + pathParams: Set + queryParams: Set + requestBodyContentTypes: Set + hasRequestBody: boolean +} + +interface StructField { + name: string + type: string + optional: boolean +} + +interface StructInfo { + name: string + file: string + fields: StructField[] +} + +interface HandlerInfo { + operationId: string + functionName: string + file: string + line: number + importedTypes: Map + jsonTypes: string[] + queryTypes: string[] + pathTypes: string[] + formTypes: string[] + bodyExtractors: Set + routeMethods: Set + routePathParams: Set +} + +interface SourceExpectations { + handlers: HandlerInfo[] + structsByFileAndName: Map + structsByName: Map +} + +interface Issue { + severity: 'error' | 'warning' + code: string + message: string + file?: string + line?: number + operation?: string +} + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +function parseArgs(): Args { + const args = process.argv.slice(2) + const parsed: Args = { + baseUrl: 'http://127.0.0.1:8000', + sourceDir: path.join(repoRoot, 'apps/labrinth/src'), + probe: true, + staticCheck: true, + includeMutatingProbes: true, + json: false, + verbose: false, + } + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + const next = () => { + const value = args[++i] + if (!value) throw new Error(`missing value for ${arg}`) + return value + } + + switch (arg) { + case '--base-url': + parsed.baseUrl = next().replace(/\/+$/, '') + break + case '--openapi': + parsed.openapi = next() + break + case '--source': + parsed.sourceDir = path.resolve(next()) + break + case '--no-probe': + parsed.probe = false + break + case '--no-static': + parsed.staticCheck = false + break + case '--safe-probes': + parsed.includeMutatingProbes = false + break + case '--include-mutating-probes': + parsed.includeMutatingProbes = true + break + case '--json': + parsed.json = true + break + case '--verbose': + parsed.verbose = true + break + case '--help': + case '-h': + printUsage() + process.exit(0) + default: + throw new Error(`unknown argument ${arg}`) + } + } + + return parsed +} + +function printUsage() { + console.log(`Usage: pnpm scripts labrinth-openapi-check [options] + +Options: + --base-url Running Labrinth URL. Default: http://127.0.0.1:8000 + --openapi OpenAPI JSON file or URL. Default: /docs + --source Labrinth source directory. Default: apps/labrinth/src + --no-probe Skip live route reachability checks + --no-static Skip Rust handler signature checks + --safe-probes Only probe GET/HEAD routes + --include-mutating-probes + Probe non-GET/HEAD routes. Enabled by default. + --json Print machine-readable JSON + --verbose Print skipped static comparisons`) +} + +async function main() { + const args = parseArgs() + const spec = await loadSpec(args) + const operations = collectOperations(spec) + const issues: Issue[] = [] + + if (args.probe) { + issues.push(...(await probeOperations(args.baseUrl, operations, args.includeMutatingProbes))) + } + + if (args.staticCheck) { + const source = collectSourceExpectations(args.sourceDir) + issues.push(...compareSourceToSpec(operations, source, args.verbose)) + } + + if (args.json) { + console.log(JSON.stringify({ operations: operations.length, issues }, null, 2)) + } else { + printReport(operations, issues) + } + + if (issues.some((issue) => issue.severity === 'error')) { + process.exit(1) + } +} + +async function loadSpec(args: Args): Promise { + const source = args.openapi ?? `${args.baseUrl}/docs` + const text = await readText(source) + const trimmed = text.trim() + + if (trimmed.startsWith('{')) { + return JSON.parse(trimmed) + } + + const sourceUrls = [...trimmed.matchAll(/"url"\s*:\s*"([^"]+\.json)"/g)].map((match) => match[1]) + if (sourceUrls.length) { + const specs = await Promise.all( + sourceUrls.map(async (url) => { + const resolved = resolveSource(url, source) + const specText = await readText(resolved) + return JSON.parse(specText) as OpenApiSpec + }), + ) + + return mergeSpecs(specs) + } + + const scalarScript = trimmed.match( + /]*\bid=(["'])api-reference\1[^>]*>([\s\S]*?)<\/script>/i, + ) + if (!scalarScript) { + throw new Error(`could not find OpenAPI JSON in ${source}`) + } + + return JSON.parse(decodeHtmlEntities(scalarScript[2].trim())) +} + +function resolveSource(source: string, baseSource: string): string { + if (/^https?:\/\//.test(source)) return source + + if (/^https?:\/\//.test(baseSource)) { + return new URL(source, baseSource).toString() + } + + return path.resolve(path.dirname(path.resolve(baseSource)), source) +} + +function mergeSpecs(specs: OpenApiSpec[]): OpenApiSpec { + const merged: OpenApiSpec = { paths: {} } + + for (const spec of specs) { + Object.assign(merged.paths ?? {}, spec.paths ?? {}) + } + + return merged +} + +async function readText(source: string): Promise { + if (/^https?:\/\//.test(source)) { + const response = await fetch(source, { headers: AUTH_HEADERS }) + if (!response.ok) { + throw new Error(`failed to fetch ${source}: HTTP ${response.status}`) + } + return response.text() + } + + return fs.readFileSync(path.resolve(source), 'utf8') +} + +function decodeHtmlEntities(value: string): string { + return value + .replace(/"/g, '"') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') +} + +function collectOperations(spec: OpenApiSpec): Operation[] { + const operations: Operation[] = [] + + for (const [apiPath, pathItem] of Object.entries(spec.paths ?? {})) { + for (const method of HTTP_METHODS) { + const operation = pathItem[method] + if (!operation) continue + + const parameters = operation.parameters ?? [] + const requestBody = + operation.requestBody && !('$ref' in operation.requestBody) + ? operation.requestBody + : undefined + + operations.push({ + path: apiPath, + method, + operationId: operation.operationId ?? `${method} ${apiPath}`, + operation, + pathParams: new Set( + parameters + .filter((param) => param.in === 'path' && param.name) + .map((param) => param.name as string), + ), + queryParams: new Set( + parameters + .filter((param) => param.in === 'query' && param.name) + .map((param) => param.name as string), + ), + requestBodyContentTypes: new Set(Object.keys(requestBody?.content ?? {})), + hasRequestBody: Boolean(requestBody), + }) + } + } + + return operations.sort((a, b) => `${a.path} ${a.method}`.localeCompare(`${b.path} ${b.method}`)) +} + +async function probeOperations( + baseUrl: string, + operations: Operation[], + includeMutatingProbes: boolean, +): Promise { + const issues: Issue[] = [] + + for (const operation of operations) { + if (IGNORED_PROBE_OPERATIONS.has(operationKey(operation))) { + continue + } + + if (!includeMutatingProbes && !['get', 'head'].includes(operation.method)) { + continue + } + + const url = new URL(renderProbePath(operation), `${baseUrl}/`) + const { body, contentType } = bodyForProbe(operation) + const headers: Record = { + ...AUTH_HEADERS, + accept: 'application/json', + 'user-agent': 'modrinth-labrinth-openapi-check', + } + if (contentType) headers['content-type'] = contentType + + let response: Response + let text = '' + try { + response = await fetch(url, { + method: operation.method.toUpperCase(), + headers, + body, + redirect: 'manual', + }) + text = await response.text() + } catch (error) { + issues.push({ + severity: 'error', + code: 'probe_failed', + operation: formatOperation(operation), + message: `could not probe ${url}: ${String(error)}`, + }) + continue + } + + if (isActixMissingRoute(response, text)) { + issues.push({ + severity: 'error', + code: 'documented_route_unreachable', + operation: formatOperation(operation), + message: `${operation.method.toUpperCase()} ${operation.path} is in OpenAPI but the running backend returned the fallback 404`, + }) + } + } + + return issues +} + +function renderProbePath(operation: Operation): string { + return operation.path.replace(/\{([^}]+)\}/g, (_, name: string) => + encodeURIComponent(samplePathValue(name, operation)), + ) +} + +function samplePathValue(name: string, operation: Operation): string { + const param = (operation.operation.parameters ?? []).find( + (candidate) => candidate.in === 'path' && candidate.name === name, + ) + const schema = param?.schema + const ref = schema?.$ref?.split('/').at(-1)?.toLowerCase() ?? '' + const type = Array.isArray(schema?.type) ? schema?.type.join('|') : schema?.type + + if (name.toLowerCase().includes('sha1')) return '0000000000000000000000000000000000000000' + if (schema?.format === 'uuid') return '00000000-0000-0000-0000-000000000000' + if (type === 'integer' || ref.endsWith('id') || name.toLowerCase().endsWith('id')) return '1' + return 'test' +} + +function bodyForProbe(operation: Operation): { body?: BodyInit; contentType?: string } { + if (!operation.hasRequestBody) return {} + + if (operation.requestBodyContentTypes.has('application/json')) { + return { body: '{}', contentType: 'application/json' } + } + if (operation.requestBodyContentTypes.has('text/plain')) { + return { body: 'test', contentType: 'text/plain' } + } + if ( + [...operation.requestBodyContentTypes].some((type) => type.startsWith('multipart/form-data')) + ) { + return { body: '', contentType: 'multipart/form-data; boundary=openapi-check' } + } + + const first = [...operation.requestBodyContentTypes][0] + return { body: '', contentType: first } +} + +function isActixMissingRoute(response: Response, text: string): boolean { + if (response.status !== 404) return false + + try { + const parsed = JSON.parse(text) + return ( + parsed?.error === 'not_found' && parsed?.description === 'the requested route does not exist' + ) + } catch { + return false + } +} + +function collectSourceExpectations(sourceDir: string): SourceExpectations { + const files = findRustFiles(sourceDir) + const structsByFileAndName = new Map() + const structsByName = new Map() + const handlers: HandlerInfo[] = [] + + for (const file of files) { + const content = fs.readFileSync(file, 'utf8') + for (const structInfo of extractStructs(content, file)) { + structsByFileAndName.set(structKey(file, structInfo.name), structInfo) + const namedStructs = structsByName.get(structInfo.name) ?? [] + namedStructs.push(structInfo) + structsByName.set(structInfo.name, namedStructs) + } + handlers.push(...extractHandlers(content, file, sourceDir)) + } + + return { handlers, structsByFileAndName, structsByName } +} + +function findRustFiles(root: string): string[] { + const files: string[] = [] + + function walk(current: string) { + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const fullPath = path.join(current, entry.name) + if (entry.isDirectory()) { + if (!entry.name.startsWith('.') && entry.name !== 'target') walk(fullPath) + } else if (entry.isFile() && entry.name.endsWith('.rs')) { + files.push(fullPath) + } + } + } + + walk(root) + return files +} + +function extractStructs(content: string, file: string): StructInfo[] { + const structs: StructInfo[] = [] + const regex = + /((?:\s*#\[[\s\S]*?\]\s*)*)(?:pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g + let match: RegExpExecArray | null + + while ((match = regex.exec(content))) { + const name = match[2] + const openBrace = content.indexOf('{', match.index + match[0].length - 1) + const closeBrace = findMatching(content, openBrace, '{', '}') + if (closeBrace === -1) continue + + const attrs = match[1] + const body = content.slice(openBrace + 1, closeBrace) + const renameAll = extractSerdeRenameAll(attrs) + const fields: StructField[] = [] + const fieldRegex = + /((?:\s*#\[[^\]]*\]\s*)*)\s*pub(?:\([^)]*\))?\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^,\n]+(?:<[^;{}]*>)?)/g + let fieldMatch: RegExpExecArray | null + + while ((fieldMatch = fieldRegex.exec(body))) { + const fieldAttrs = fieldMatch[1] + if (/\bskip_deserializing\b|\bskip\b/.test(fieldAttrs)) continue + + const rustName = fieldMatch[2] + const rustType = fieldMatch[3].trim() + fields.push({ + name: extractSerdeRename(fieldAttrs) ?? applyRenameAll(rustName, renameAll), + type: rustType, + optional: /^Option\s*() + const routePathParams = new Set() + + for (const routeAttr of routeAttrs) { + const method = routeAttr[1] + const body = routeAttr[2] + if (method === 'route') { + for (const methodMatch of body.matchAll(/method\s*=\s*"([A-Z]+)"/g)) { + const routeMethod = methodMatch[1].toLowerCase() as HttpMethod + if (HTTP_METHODS.includes(routeMethod)) routeMethods.add(routeMethod) + } + } else { + routeMethods.add(method as HttpMethod) + } + + const pathMatch = body.match(/"([^"]+)"/) + if (pathMatch) { + for (const paramMatch of pathMatch[1].matchAll(/\{([^}:]+)(?::[^}]+)?\}/g)) { + routePathParams.add(paramMatch[1]) + } + } + } + + const operationId = attrText.match(/\boperation_id\s*=\s*"([^"]+)"/)?.[1] ?? fnName + const jsonTypes = extractGenericExtractorTypes(params, 'Json') + const queryTypes = extractGenericExtractorTypes(params, 'Query') + const pathTypes = extractGenericExtractorTypes(params, 'Path') + const formTypes = extractGenericExtractorTypes(params, 'Form') + const bodyExtractors = new Set() + + if (jsonTypes.length) bodyExtractors.add('json') + if (formTypes.length) bodyExtractors.add('form') + if (/\b(?:web::)?Payload\b/.test(params)) bodyExtractors.add('payload') + if (/\b(?:web::)?Bytes\b/.test(params)) bodyExtractors.add('bytes') + if (/\bMultipart\b/.test(params)) bodyExtractors.add('multipart') + if (/(?:^|,)\s*(?:[A-Za-z_][A-Za-z0-9_]*\s*:\s*)?String\s*(?:,|$)/.test(params)) { + bodyExtractors.add('text') + } + + handlers.push({ + operationId, + functionName: fnName, + file, + line: lineNumber(content, attrStart), + importedTypes, + jsonTypes, + queryTypes, + pathTypes, + formTypes, + bodyExtractors, + routeMethods, + routePathParams, + }) + + index = parenEnd + 1 + } + + return handlers +} + +function extractImportedTypeFiles(content: string, sourceDir: string): Map { + const importedTypes = new Map() + + for (const importMatch of content.matchAll(/use\s+crate::([\w:]+)::\{([^}]+)\};/g)) { + const moduleFile = resolveModuleFile(sourceDir, importMatch[1]) + if (!moduleFile) continue + + for (const rawItem of importMatch[2].split(',')) { + const item = rawItem.trim() + if (!item || item === 'self' || item.includes('::')) continue + + const simpleName = item.match(/^([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+\w+)?$/)?.[1] + if (simpleName) importedTypes.set(simpleName, moduleFile) + } + } + + for (const importMatch of content.matchAll( + /use\s+crate::([\w:]+)::([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+\w+)?;/g, + )) { + const moduleFile = resolveModuleFile(sourceDir, importMatch[1]) + if (moduleFile) importedTypes.set(importMatch[2], moduleFile) + } + + return importedTypes +} + +function resolveModuleFile(sourceDir: string, modulePath: string): string | null { + const relativePath = modulePath.replace(/::/g, path.sep) + const directFile = path.join(sourceDir, `${relativePath}.rs`) + if (fs.existsSync(directFile)) return directFile + + const modFile = path.join(sourceDir, relativePath, 'mod.rs') + if (fs.existsSync(modFile)) return modFile + + return null +} + +function extractGenericExtractorTypes(params: string, extractor: string): string[] { + const types: string[] = [] + const regex = new RegExp(`(?:web::)?${extractor}\\s*<`, 'g') + let match: RegExpExecArray | null + + while ((match = regex.exec(params))) { + const open = params.indexOf('<', match.index) + const close = findMatching(params, open, '<', '>') + if (close === -1) continue + + types.push(params.slice(open + 1, close).trim()) + regex.lastIndex = close + 1 + } + + return types +} + +function compareSourceToSpec( + operations: Operation[], + source: SourceExpectations, + verbose: boolean, +): Issue[] { + const issues: Issue[] = [] + const operationsById = new Map() + + for (const operation of operations) { + const existing = operationsById.get(operation.operationId) ?? [] + existing.push(operation) + operationsById.set(operation.operationId, existing) + } + + for (const handler of source.handlers) { + const candidates = operationsById.get(handler.operationId) ?? [] + if (!candidates.length) { + issues.push({ + severity: 'error', + code: 'annotated_handler_missing_from_openapi', + file: handler.file, + line: handler.line, + operation: handler.operationId, + message: `handler ${handler.functionName} has #[utoipa::path] but no OpenAPI operation with operationId ${handler.operationId}`, + }) + continue + } + + const operation = chooseOperation(handler, candidates) + if (!operation) { + if (verbose) { + issues.push({ + severity: 'warning', + code: 'duplicate_operation_id_skipped', + file: handler.file, + line: handler.line, + operation: handler.operationId, + message: `operationId ${handler.operationId} appears ${candidates.length} times, so static signature comparison was skipped`, + }) + } + continue + } + + compareBody(handler, operation, issues) + compareQuery(handler, operation, source, issues) + comparePath(handler, operation, issues) + } + + return issues +} + +function chooseOperation(handler: HandlerInfo, candidates: Operation[]): Operation | null { + if (candidates.length === 1) return candidates[0] + + const methodMatches = candidates.filter( + (operation) => !handler.routeMethods.size || handler.routeMethods.has(operation.method), + ) + if (methodMatches.length === 1) return methodMatches[0] + + const pathMatches = methodMatches.filter((operation) => { + for (const param of handler.routePathParams) { + if (!operation.pathParams.has(param)) return false + } + return true + }) + if (pathMatches.length === 1) return pathMatches[0] + + return null +} + +function compareBody(handler: HandlerInfo, operation: Operation, issues: Issue[]) { + const expectsBody = handler.bodyExtractors.size > 0 + + if (expectsBody && !operation.hasRequestBody) { + issues.push({ + severity: 'error', + code: 'missing_request_body', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `${handler.functionName} expects ${[...handler.bodyExtractors].join(', ')} body data, but OpenAPI has no requestBody`, + }) + } + + if (!expectsBody && operation.hasRequestBody) { + issues.push({ + severity: 'error', + code: 'unexpected_request_body', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `OpenAPI documents a requestBody, but ${handler.functionName} has no recognized body extractor`, + }) + } + + if ( + handler.bodyExtractors.has('json') && + !operation.requestBodyContentTypes.has('application/json') + ) { + issues.push({ + severity: 'error', + code: 'request_body_content_type_mismatch', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `${handler.functionName} expects web::Json, but OpenAPI content types are ${formatSet(operation.requestBodyContentTypes)}`, + }) + } + + if (handler.bodyExtractors.has('text') && !operation.requestBodyContentTypes.has('text/plain')) { + issues.push({ + severity: 'error', + code: 'request_body_content_type_mismatch', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `${handler.functionName} expects a plain String body, but OpenAPI content types are ${formatSet(operation.requestBodyContentTypes)}`, + }) + } +} + +function compareQuery( + handler: HandlerInfo, + operation: Operation, + source: SourceExpectations, + issues: Issue[], +) { + const expectedQueryParams = new Set() + + for (const queryType of handler.queryTypes) { + const simpleType = simpleRustType(queryType) + if (isDynamicExtractorType(simpleType)) continue + + const structInfo = resolveStructInfo(handler, simpleType, source) + if (!structInfo) { + issues.push({ + severity: 'warning', + code: 'query_type_not_resolved', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `could not resolve query type ${queryType} for ${handler.functionName}`, + }) + continue + } + + for (const field of structInfo.fields) expectedQueryParams.add(field.name) + } + + for (const expected of expectedQueryParams) { + if (!operation.queryParams.has(expected)) { + issues.push({ + severity: 'error', + code: 'missing_query_parameter', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `${handler.functionName} expects query parameter ${expected}, but OpenAPI does not document it`, + }) + } + } +} + +function resolveStructInfo( + handler: HandlerInfo, + simpleType: string, + source: SourceExpectations, +): StructInfo | null { + const sameFileStruct = source.structsByFileAndName.get(structKey(handler.file, simpleType)) + if (sameFileStruct) return sameFileStruct + + const importedFile = handler.importedTypes.get(simpleType) + if (importedFile) { + const importedStruct = source.structsByFileAndName.get(structKey(importedFile, simpleType)) + if (importedStruct) return importedStruct + } + + const structs = source.structsByName.get(simpleType) ?? [] + if (structs.length === 1) return structs[0] + + return null +} + +function comparePath(handler: HandlerInfo, operation: Operation, issues: Issue[]) { + if (handler.pathTypes.length && !operation.pathParams.size) { + issues.push({ + severity: 'error', + code: 'missing_path_parameters', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `${handler.functionName} extracts path data, but OpenAPI has no path parameters`, + }) + } + + for (const routeParam of handler.routePathParams) { + if (!operation.pathParams.has(routeParam)) { + issues.push({ + severity: 'error', + code: 'path_parameter_name_mismatch', + file: handler.file, + line: handler.line, + operation: formatOperation(operation), + message: `${handler.functionName} route macro uses path parameter ${routeParam}, but OpenAPI path parameters are ${formatSet(operation.pathParams)}`, + }) + } + } +} + +function simpleRustType(type: string): string { + const trimmed = type.trim() + if (trimmed.startsWith('Option<')) { + return simpleRustType(trimmed.slice('Option<'.length, -1)) + } + + const withoutGenerics = trimmed.replace(/<[\s\S]*>$/, '') + return withoutGenerics.split('::').at(-1)?.replace(/[^\w]/g, '') ?? withoutGenerics +} + +function structKey(file: string, name: string): string { + return `${file}::${name}` +} + +function isDynamicExtractorType(type: string): boolean { + return ['HashMap', 'Value', 'Map'].includes(type) +} + +function extractSerdeRename(attrs: string): string | undefined { + return attrs.match(/\brename\s*=\s*"([^"]+)"/)?.[1] +} + +function extractSerdeRenameAll(attrs: string): string | undefined { + return attrs.match(/\brename_all\s*=\s*"([^"]+)"/)?.[1] +} + +function applyRenameAll(name: string, renameAll?: string): string { + switch (renameAll) { + case 'camelCase': + return name.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase()) + case 'kebab-case': + return name.replace(/_/g, '-') + default: + return name + } +} + +function findMatching(content: string, openIndex: number, open: string, close: string): number { + if (openIndex < 0 || content[openIndex] !== open) return -1 + + let depth = 0 + let quote: string | null = null + let escaped = false + + for (let i = openIndex; i < content.length; i++) { + const char = content[i] + + if (quote) { + if (escaped) { + escaped = false + } else if (char === '\\') { + escaped = true + } else if (char === quote) { + quote = null + } + continue + } + + if (char === '"') { + quote = char + continue + } + + if (char === open) depth++ + if (char === close) { + depth-- + if (depth === 0) return i + } + } + + return -1 +} + +function lineNumber(content: string, index: number): number { + let line = 1 + for (let i = 0; i < index; i++) { + if (content[i] === '\n') line++ + } + return line +} + +function printReport(operations: Operation[], issues: Issue[]) { + const errors = issues.filter((issue) => issue.severity === 'error') + const warnings = issues.filter((issue) => issue.severity === 'warning') + + console.log(chalk.bold(`Checked ${operations.length} OpenAPI operations`)) + + if (!issues.length) { + console.log(chalk.green('No OpenAPI mismatches found')) + return + } + + for (const issue of issues) { + const color = issue.severity === 'error' ? chalk.red : chalk.yellow + const location = issue.file ? ` ${formatLocation(issue.file, issue.line)}` : '' + const operation = issue.operation ? ` ${chalk.gray(issue.operation)}` : '' + console.log( + `${color(issue.severity.toUpperCase())} ${chalk.bold(issue.code)}${operation}${location}`, + ) + console.log(` ${issue.message}`) + } + + console.log() + console.log( + `${chalk.red(`${errors.length} error(s)`)} ${chalk.yellow(`${warnings.length} warning(s)`)}`, + ) +} + +function formatOperation(operation: Operation): string { + return `${operation.method.toUpperCase()} ${operation.path} (${operation.operationId})` +} + +function operationKey(operation: Operation): string { + return `${operation.method.toUpperCase()} ${operation.path}` +} + +function formatSet(values: Set): string { + return values.size ? [...values].join(', ') : '' +} + +function formatLocation(file: string, line?: number): string { + const relative = path.relative(repoRoot, file) + return line ? `${relative}:${line}` : relative +} + +main().catch((error) => { + console.error(chalk.red(error instanceof Error ? error.message : String(error))) + process.exit(1) +}) From d4c6466a1d4a58fc49e42597238b1fe3e25643a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Thu, 2 Jul 2026 12:58:43 -0400 Subject: [PATCH 7/7] remove utoipa-actix-web --- Cargo.lock | 12 - Cargo.toml | 1 - apps/labrinth/Cargo.toml | 1 - apps/labrinth/src/auth/oauth/mod.rs | 22 +- apps/labrinth/src/lib.rs | 37 ++- apps/labrinth/src/main.rs | 49 ++-- .../src/models/v3/moderation_notes.rs | 2 +- apps/labrinth/src/routes/analytics.rs | 20 +- apps/labrinth/src/routes/debug/mod.rs | 27 +-- apps/labrinth/src/routes/debug/pprof.rs | 7 +- apps/labrinth/src/routes/internal/admin.rs | 17 +- .../labrinth/src/routes/internal/affiliate.rs | 7 +- .../src/routes/internal/attribution.rs | 7 +- apps/labrinth/src/routes/internal/billing.rs | 45 ++-- apps/labrinth/src/routes/internal/campaign.rs | 7 +- .../src/routes/internal/delphi/mod.rs | 19 +- .../routes/internal/external_notifications.rs | 19 +- apps/labrinth/src/routes/internal/flows.rs | 48 +++- apps/labrinth/src/routes/internal/gdpr.rs | 13 +- apps/labrinth/src/routes/internal/globals.rs | 7 +- .../labrinth/src/routes/internal/gotenberg.rs | 15 +- apps/labrinth/src/routes/internal/medal.rs | 17 +- apps/labrinth/src/routes/internal/mod.rs | 150 ++++--------- .../internal/moderation/external_license.rs | 15 +- .../src/routes/internal/moderation/mod.rs | 48 ++-- .../routes/internal/moderation/tech_review.rs | 15 +- apps/labrinth/src/routes/internal/mural.rs | 15 +- apps/labrinth/src/routes/internal/pats.rs | 7 +- apps/labrinth/src/routes/internal/search.rs | 7 +- .../src/routes/internal/server_ping.rs | 7 +- apps/labrinth/src/routes/internal/session.rs | 17 +- apps/labrinth/src/routes/internal/statuses.rs | 13 +- apps/labrinth/src/routes/maven.rs | 27 ++- apps/labrinth/src/routes/mod.rs | 51 +++-- apps/labrinth/src/routes/updates.rs | 10 +- apps/labrinth/src/routes/v2/mod.rs | 30 ++- apps/labrinth/src/routes/v2/moderation.rs | 17 +- apps/labrinth/src/routes/v2/notifications.rs | 33 ++- .../src/routes/v2/project_creation.rs | 7 +- apps/labrinth/src/routes/v2/projects.rs | 68 +++++- apps/labrinth/src/routes/v2/reports.rs | 14 +- apps/labrinth/src/routes/v2/statistics.rs | 7 +- apps/labrinth/src/routes/v2/tags.rs | 27 ++- apps/labrinth/src/routes/v2/teams.rs | 48 +++- apps/labrinth/src/routes/v2/threads.rs | 34 ++- apps/labrinth/src/routes/v2/users.rs | 47 +++- .../src/routes/v2/version_creation.rs | 13 ++ apps/labrinth/src/routes/v2/version_file.rs | 49 +++- apps/labrinth/src/routes/v2/versions.rs | 44 +++- .../labrinth/src/routes/v3/analytics_event.rs | 12 +- .../src/routes/v3/analytics_get/facets/mod.rs | 11 +- .../src/routes/v3/analytics_get/mod.rs | 24 +- .../src/routes/v3/analytics_get/old.rs | 23 +- apps/labrinth/src/routes/v3/collections.rs | 54 +++-- apps/labrinth/src/routes/v3/content/mod.rs | 15 +- apps/labrinth/src/routes/v3/friends.rs | 7 +- apps/labrinth/src/routes/v3/images.rs | 13 +- apps/labrinth/src/routes/v3/limits.rs | 28 ++- apps/labrinth/src/routes/v3/mod.rs | 102 ++++----- apps/labrinth/src/routes/v3/notifications.rs | 104 ++++++++- apps/labrinth/src/routes/v3/oauth_clients.rs | 36 ++- apps/labrinth/src/routes/v3/organizations.rs | 83 ++++--- apps/labrinth/src/routes/v3/payouts.rs | 55 +++-- .../src/routes/v3/project_creation.rs | 46 ++-- .../src/routes/v3/project_creation/new.rs | 11 +- apps/labrinth/src/routes/v3/projects.rs | 134 ++++++++--- apps/labrinth/src/routes/v3/reports.rs | 103 ++++++++- .../v3/shared_instance_version_creation.rs | 16 +- .../src/routes/v3/shared_instances.rs | 67 ++++-- apps/labrinth/src/routes/v3/statistics.rs | 19 +- apps/labrinth/src/routes/v3/tags.rs | 118 ++++++++-- apps/labrinth/src/routes/v3/teams.rs | 153 +++++++++++-- apps/labrinth/src/routes/v3/threads.rs | 80 ++++++- apps/labrinth/src/routes/v3/users.rs | 212 ++++++++++++++++-- .../src/routes/v3/version_creation.rs | 61 ++++- apps/labrinth/src/routes/v3/version_file.rs | 168 +++++++++++--- apps/labrinth/src/routes/v3/versions.rs | 123 ++++++++-- apps/labrinth/src/test/api_v2/mod.rs | 7 +- apps/labrinth/src/test/api_v3/mod.rs | 7 +- apps/labrinth/tests/openapi_route_scan.rs | 117 ++++++++++ 80 files changed, 2444 insertions(+), 784 deletions(-) create mode 100644 apps/labrinth/tests/openapi_route_scan.rs diff --git a/Cargo.lock b/Cargo.lock index 7e9d6fdad6..028da5aaf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5456,7 +5456,6 @@ dependencies = [ "url", "urlencoding", "utoipa", - "utoipa-actix-web", "uuid 1.23.3", "validator", "webauthn-rs", @@ -11858,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" diff --git a/Cargo.toml b/Cargo.toml index 1b87bd1c63..a7a6d055b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -218,7 +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" } 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 2226d6f03b..4f702d6bab 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -129,7 +129,6 @@ tracing-actix-web = { workspace = true } url = { workspace = true } urlencoding = { workspace = true } utoipa = { workspace = true, features = ["url"] } -utoipa-actix-web = { workspace = true } 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/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 8fd7cfc09b..ce2054ce3d 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -362,11 +362,11 @@ pub fn app_config( cfg: &mut web::ServiceConfig, labrinth_config: LabrinthConfig, ) { - app_base_config(cfg, labrinth_config); + app_data_config(cfg, labrinth_config); app_fallback_config(cfg); } -pub fn app_base_config( +pub fn app_data_config( cfg: &mut web::ServiceConfig, labrinth_config: LabrinthConfig, ) { @@ -406,9 +406,7 @@ pub fn app_base_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); + .app_data(labrinth_config.webauthn.clone()); } pub fn app_fallback_config(cfg: &mut web::ServiceConfig) { @@ -416,8 +414,8 @@ pub fn app_fallback_config(cfg: &mut web::ServiceConfig) { .default_service(web::get().to(routes::not_found).wrap(default_cors())); } -pub fn utoipa_app_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +pub fn app_routes_config( + cfg: &mut web::ServiceConfig, labrinth_config: LabrinthConfig, ) { cfg.configure({ @@ -430,28 +428,29 @@ pub fn utoipa_app_config( |_cfg| () } }) - .configure(|cfg| utoipa_app_config_v2(cfg, labrinth_config.clone())) - .configure(|cfg| utoipa_app_config_v3(cfg, labrinth_config.clone())) - .configure(|cfg| utoipa_app_config_internal(cfg, labrinth_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 utoipa_app_config_v2( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +pub fn app_routes_config_v2( + cfg: &mut web::ServiceConfig, _labrinth_config: LabrinthConfig, ) { - cfg.configure(routes::v2::utoipa_config); + cfg.configure(routes::v2::config); } -pub fn utoipa_app_config_v3( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +pub fn app_routes_config_v3( + cfg: &mut web::ServiceConfig, _labrinth_config: LabrinthConfig, ) { - cfg.configure(routes::v3::utoipa_config); + cfg.configure(routes::public_config) + .configure(routes::v3::config); } -pub fn utoipa_app_config_internal( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, +pub fn app_routes_config_internal( + cfg: &mut web::ServiceConfig, _labrinth_config: LabrinthConfig, ) { - cfg.configure(routes::internal::utoipa_config); + cfg.configure(routes::internal::config); } diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index b508cc5996..2a75f394d7 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -15,11 +15,11 @@ use labrinth::search; use labrinth::util::anrok; use labrinth::util::gotenberg::GotenbergClient; use labrinth::util::ratelimit::rate_limit_middleware; -use labrinth::{app_base_config, app_fallback_config, env}; -use labrinth::{clickhouse, database, file_hosting}; +use labrinth::{app_data_config, app_fallback_config, env}; use labrinth::{ - utoipa_app_config_internal, utoipa_app_config_v2, utoipa_app_config_v3, + 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; @@ -29,7 +29,6 @@ use tracing_actix_web::TracingLogger; use utoipa::OpenApi; use utoipa::openapi::extensions::ExtensionsBuilder; use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}; -use utoipa_actix_web::AppExt; #[cfg(target_os = "linux")] #[global_allocator] @@ -227,7 +226,15 @@ async fn app() -> std::io::Result<()> { info!("Starting Actix HTTP server!"); HttpServer::new(move || { - let (app, docs_v2) = 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`. @@ -264,23 +271,12 @@ 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) - .configure(|cfg| app_base_config(cfg, labrinth_config.clone())) - .into_utoipa_app() - .openapi(DocsV2::openapi()) - .configure(|cfg| utoipa_app_config_v2(cfg, labrinth_config.clone())) - .split_for_parts(); - let (app, docs_v3) = app - .into_utoipa_app() - .openapi(DocsV3::openapi()) - .configure(|cfg| utoipa_app_config_v3(cfg, labrinth_config.clone())) - .split_for_parts(); - let (app, docs_internal) = app - .into_utoipa_app() - .openapi(DocsInternal::openapi()) + .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| { - utoipa_app_config_internal(cfg, labrinth_config.clone()) - }) - .split_for_parts(); + app_routes_config_internal(cfg, labrinth_config.clone()) + }); let scalar_configuration = json!({ "sources": [ @@ -362,17 +358,6 @@ const API_V2_DESCRIPTION: &str = include_str!("api_v2_description.md"); #[derive(utoipa::OpenApi)] #[openapi( - paths( - labrinth::routes::v3::version_file::get_version_from_hash, - labrinth::routes::v3::version_file::get_update_from_hash, - labrinth::routes::v3::version_file::get_versions_from_hashes, - labrinth::routes::v3::version_file::get_projects_from_hashes, - labrinth::routes::v3::version_file::update_files_many, - labrinth::routes::v3::version_file::update_files, - labrinth::routes::v3::version_file::update_individual_files, - labrinth::routes::v3::version_file::delete_file, - labrinth::routes::v3::version_file::download_version - ), info( title = "API v3 (UNSTABLE - DO NOT USE)", version = "3.0.0" 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/routes/analytics.rs b/apps/labrinth/src/routes/analytics.rs index f45c8307e9..1b321870fa 100644 --- a/apps/labrinth/src/routes/analytics.rs +++ b/apps/labrinth/src/routes/analytics.rs @@ -46,18 +46,19 @@ 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 +#[utoipa::path(tag = "analytics", responses((status = NO_CONTENT)))] #[post("/view")] async fn page_view_ingest( req: HttpRequest, @@ -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,6 +180,7 @@ pub struct PlaytimeInput { parent: Option, } +#[utoipa::path(tag = "analytics", responses((status = NO_CONTENT)))] #[post("/playtime")] async fn playtime_ingest( req: HttpRequest, @@ -249,7 +251,7 @@ struct MinecraftProfile { name: String, } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct MinecraftJavaServerPlayInput { project_id: ProjectId, username: String, @@ -258,6 +260,7 @@ pub struct MinecraftJavaServerPlayInput { pub const MINECRAFT_SERVER_PLAYS: &str = "minecraft_server_plays"; +#[utoipa::path(tag = "analytics", responses((status = NO_CONTENT)))] #[post("/minecraft-server-play")] async fn minecraft_server_play_ingest( req: HttpRequest, @@ -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 1a4f1953c5..3948a05a60 100644 --- a/apps/labrinth/src/routes/debug/pprof.rs +++ b/apps/labrinth/src/routes/debug/pprof.rs @@ -5,7 +5,7 @@ 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); } @@ -112,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 f298480d7e..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), @@ -365,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 a8af09287a..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) @@ -417,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 13c893adf4..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) @@ -987,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 a56ece18ac..ad06707625 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -42,31 +42,9 @@ 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) - .service(subscriptions) - .service(user_customer) - .service(edit_subscription) - .service(payment_methods) - .service(add_payment_method_flow) - .service(edit_payment_method) - .service(remove_payment_method) - .service(charges) - .service(credit) - .service(active_servers) - .service(initiate_payment) - .service(stripe_webhook) - .service(refund_charge), - ); -} - -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service( - utoipa_actix_web::scope("/_internal/billing") .service(products) .service(subscriptions) .service(user_customer) @@ -2757,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 59621256a0..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); } @@ -480,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 513f089fb5..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) @@ -45,18 +45,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service( - utoipa_actix_web::scope("/_internal/delphi") - .service(ingest_report) - .service(_run) - .service(version) - .service(issue_type_schema), - ); -} - /// Type of [`DelphiReportIssueDetails::key`]. /// /// Delphi may provide `null` for the key, but we require a key for storing @@ -588,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 b76132a44b..c674df43c3 100644 --- a/apps/labrinth/src/routes/internal/external_notifications.rs +++ b/apps/labrinth/src/routes/internal/external_notifications.rs @@ -26,25 +26,13 @@ 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); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service( - utoipa_actix_web::scope("/_internal") - .service(create) - .service(create_email_sync) - .service(remove) - .service(send_custom_email), - ); -} - #[derive(Deserialize, utoipa::ToSchema)] struct CreateNotification { #[schema(value_type = serde_json::Value)] @@ -375,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 a69e3e3a24..fd51ded61f 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -60,9 +60,11 @@ 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) .service(create_oauth_account) .service(validate_create_account_with_password) @@ -1065,6 +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(tag = "auth", responses((status = TEMPORARY_REDIRECT), (status = OK)))] #[get("/init")] pub async fn init( req: HttpRequest, @@ -1155,6 +1158,7 @@ pub async fn init( .json(serde_json::json!({ "url": url }))) } +#[utoipa::path(tag = "auth", responses((status = OK)))] #[get("/callback")] pub async fn auth_callback( req: HttpRequest, @@ -3646,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 5c5de097ad..e8f0082ec1 100644 --- a/apps/labrinth/src/routes/internal/gdpr.rs +++ b/apps/labrinth/src/routes/internal/gdpr.rs @@ -6,16 +6,10 @@ 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)); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service(utoipa_actix_web::scope("/_internal/gdpr").service(export)); -} - /// Export GDPR data. #[utoipa::path( tag = "GDPR", @@ -225,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 aeb3c9eb1c..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); } @@ -121,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 2ec3717c51..8955344b81 100644 --- a/apps/labrinth/src/routes/internal/gotenberg.rs +++ b/apps/labrinth/src/routes/internal/gotenberg.rs @@ -28,16 +28,6 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(success_callback).service(error_callback); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service( - utoipa_actix_web::scope("/_internal") - .service(success_callback) - .service(error_callback), - ); -} - /// Receive a Gotenberg success callback. #[utoipa::path( tag = "gotenberg", @@ -248,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 4803d898a1..02f92954e6 100644 --- a/apps/labrinth/src/routes/internal/medal.rs +++ b/apps/labrinth/src/routes/internal/medal.rs @@ -13,20 +13,10 @@ 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)); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service( - utoipa_actix_web::scope("/_internal/medal") - .service(verify) - .service(redeem), - ); -} - #[derive(Deserialize)] struct MedalQuery { username: String, @@ -127,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 b1cb4dc275..b7244d9af3 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -21,120 +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); - cfg.service( - actix_web::web::scope("/moderation") - .configure(moderation::web_config), - ); - }) + .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), + .configure(statuses::config), ) .service( - utoipa_actix_web::scope("/_internal/globals") - .wrap(default_cors()) - .configure(globals::config), - ) - .service( - utoipa_actix_web::scope("/_internal/server-ping") - .wrap(default_cors()) - .configure(server_ping::config), - ) - .service( - utoipa_actix_web::scope("/_internal/attribution") - .wrap(default_cors()) - .configure(attribution::config), - ) - .service( - utoipa_actix_web::scope("/v2") - .wrap(default_cors()) - .configure(admin::config) - .configure(super::v2::moderation::config), - ) - .service( - utoipa_actix_web::scope("/v3/analytics-event") + web::scope("/v3/analytics-event") .wrap(default_cors()) .configure(super::v3::analytics_event::config), - ) - .configure(billing::utoipa_config) - .configure(delphi::utoipa_config) - .configure(external_notifications::utoipa_config) - .configure(gdpr::utoipa_config) - .configure(gotenberg::utoipa_config) - .configure(medal::utoipa_config) - .configure(mural::utoipa_config) - .configure(statuses::utoipa_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 2adaedc439..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) @@ -682,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 e5fe4a794c..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,28 +35,12 @@ 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), ); } -pub fn web_config(cfg: &mut web::ServiceConfig) { - cfg.service(get_projects) - .service(get_project_meta) - .service(set_project_meta) - .service(acquire_lock) - .service(override_lock) - .service(get_lock_status) - .service(release_lock) - .service(release_lock_beacon) - .service(delete_all_locks); -} - #[derive(Deserialize, utoipa::ToSchema)] pub struct ProjectsRequestOptions { /// How many projects to fetch. @@ -873,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 9c0c3c4a70..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) @@ -1346,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 82bc9c9a0e..3cf8c7d2ca 100644 --- a/apps/labrinth/src/routes/internal/mural.rs +++ b/apps/labrinth/src/routes/internal/mural.rs @@ -6,18 +6,10 @@ 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); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service( - utoipa_actix_web::scope("/_internal").service(get_bank_details), - ); -} - /// Get bank details. #[utoipa::path( tag = "mural", @@ -39,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 406a3be1a2..e708653bec 100644 --- a/apps/labrinth/src/routes/internal/pats.rs +++ b/apps/labrinth/src/routes/internal/pats.rs @@ -22,7 +22,7 @@ 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); @@ -353,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 63c99675f3..7a993a3371 100644 --- a/apps/labrinth/src/routes/internal/search.rs +++ b/apps/labrinth/src/routes/internal/search.rs @@ -5,7 +5,7 @@ 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); } @@ -37,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 37cfed34c9..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); } @@ -51,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 1fcfbfea24..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), @@ -316,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 0d15916a57..8f10172f4d 100644 --- a/apps/labrinth/src/routes/internal/statuses.rs +++ b/apps/labrinth/src/routes/internal/statuses.rs @@ -35,16 +35,10 @@ 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); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service(utoipa_actix_web::scope("/_internal").service(ws_init)); -} - #[derive(Deserialize)] struct LauncherHeartbeatInit { code: String, @@ -565,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 d76e34e87e..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,6 +70,10 @@ pub struct MavenPom { description: String, } +#[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, @@ -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,6 +360,7 @@ pub async fn version_file( Err(ApiError::NotFound) } +#[utoipa::path(tag = "maven", responses((status = OK, body = String)))] #[get("/maven/modrinth/{id}/{versionnum}/{file}.sha1")] pub async fn version_file_sha1( req: HttpRequest, @@ -396,6 +408,7 @@ pub async fn version_file_sha1( )) } +#[utoipa::path(tag = "maven", responses((status = OK, body = String)))] #[get("/maven/modrinth/{id}/{versionnum}/{file}.sha512")] pub async fn version_file_sha512( req: HttpRequest, @@ -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 14786a2d29..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,6 +31,7 @@ fn default_neoforge() -> String { "none".into() } +#[utoipa::path(tag = "updates", responses((status = OK)))] #[get("/{id}/forge_updates.json")] pub async fn forge_updates( req: HttpRequest, @@ -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 fed5f3826c..d000426e05 100644 --- a/apps/labrinth/src/routes/v2/mod.rs +++ b/apps/labrinth/src/routes/v2/mod.rs @@ -14,12 +14,11 @@ 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::session::config) .configure(super::internal::flows::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 3fb68a8d97..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)] @@ -80,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 101bdf9a67..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), @@ -287,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 b7b2e13ed9..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); } @@ -305,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 ca7401ff6d..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), @@ -1370,3 +1370,65 @@ pub async fn project_unfollow( .await .or_else(v2_reroute::flatten_404_error) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + project_search, + random_projects_get, + projects_get, + project_get, + project_get_check, + dependency_list, + project_edit, + projects_edit, + project_icon_edit, + delete_project_icon, + add_gallery_item, + edit_gallery_item, + delete_gallery_item, + project_delete, + project_follow, + project_unfollow, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + project_search, + random_projects_get, + projects_get, + projects_edit, +))] +pub(crate) struct RootRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + project_get, + project_get_check, + project_edit, + project_icon_edit, + delete_project_icon, + add_gallery_item, + edit_gallery_item, + delete_gallery_item, + project_delete, + project_follow, + project_unfollow, +))] +pub(crate) struct ProjectRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(dependency_list,))] +pub(crate) struct ProjectIdRoutesDoc; + +pub(crate) struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let openapi = RootRoutesDoc::openapi(); + openapi + .nest("/project", ProjectRoutesDoc::openapi()) + .nest("/project/{project_id}", ProjectIdRoutesDoc::openapi()) + } +} diff --git a/apps/labrinth/src/routes/v2/reports.rs b/apps/labrinth/src/routes/v2/reports.rs index eca90d142e..94ff852446 100644 --- a/apps/labrinth/src/routes/v2/reports.rs +++ b/apps/labrinth/src/routes/v2/reports.rs @@ -8,7 +8,7 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use serde::Deserialize; use validator::Validate; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(reports_get); cfg.service(reports); cfg.service(report_create); @@ -315,3 +315,15 @@ pub async fn report_delete( .await .or_else(v2_reroute::flatten_404_error) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + report_create, + reports, + reports_get, + report_get, + report_edit, + report_delete, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v2/statistics.rs b/apps/labrinth/src/routes/v2/statistics.rs index 8ef30247b6..e6536e6fc0 100644 --- a/apps/labrinth/src/routes/v2/statistics.rs +++ b/apps/labrinth/src/routes/v2/statistics.rs @@ -5,7 +5,7 @@ use crate::routes::{ }; use actix_web::{HttpResponse, get, web}; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(get_stats); } @@ -51,3 +51,8 @@ pub async fn get_stats( Err(response) => Ok(response), } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(get_stats,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v2/tags.rs b/apps/labrinth/src/routes/v2/tags.rs index 5d63cfa461..4f807fed1a 100644 --- a/apps/labrinth/src/routes/v2/tags.rs +++ b/apps/labrinth/src/routes/v2/tags.rs @@ -12,9 +12,9 @@ use actix_web::{HttpResponse, get, web}; use chrono::{DateTime, Utc}; use itertools::Itertools; -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("/tag") + web::scope("/tag") .service(category_list) .service(loader_list) .service(game_version_list) @@ -463,3 +463,26 @@ pub async fn side_type_list() -> Result { let side_types = side_types.iter().map(|s| s.to_string()).collect_vec(); Ok(HttpResponse::Ok().json(side_types)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + category_list, + loader_list, + game_version_list, + license_list, + license_text, + donation_platform_list, + report_type_list, + project_type_list, + side_type_list, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi( + nest( + (path = "/tag", api = RouteDoc), + ) +)] +pub(crate) struct ApiDoc; diff --git a/apps/labrinth/src/routes/v2/teams.rs b/apps/labrinth/src/routes/v2/teams.rs index 353375ac98..fe442ff2e9 100644 --- a/apps/labrinth/src/routes/v2/teams.rs +++ b/apps/labrinth/src/routes/v2/teams.rs @@ -12,10 +12,10 @@ use ariadne::ids::UserId; 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(teams_get); cfg.service( - utoipa_actix_web::scope("/team") + web::scope("/team") .service(team_members_get) .service(edit_team_member) .service(transfer_ownership) @@ -415,3 +415,47 @@ pub async fn remove_team_member( .await .or_else(v2_reroute::flatten_404_error) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + team_members_get_project, + team_members_get, + teams_get, + join_team, + add_team_member, + edit_team_member, + transfer_ownership, + remove_team_member, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(teams_get,))] +pub(crate) struct RootRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(team_members_get_project,))] +pub(crate) struct ProjectRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + team_members_get, + join_team, + add_team_member, + edit_team_member, + transfer_ownership, + remove_team_member, +))] +pub(crate) struct TeamRoutesDoc; + +pub(crate) struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let openapi = RootRoutesDoc::openapi(); + openapi + .nest("/project", ProjectRoutesDoc::openapi()) + .nest("/team", TeamRoutesDoc::openapi()) + } +} diff --git a/apps/labrinth/src/routes/v2/threads.rs b/apps/labrinth/src/routes/v2/threads.rs index b8da38aa34..cc77e470af 100644 --- a/apps/labrinth/src/routes/v2/threads.rs +++ b/apps/labrinth/src/routes/v2/threads.rs @@ -9,13 +9,13 @@ use crate::routes::{ApiError, v2_reroute, v3}; use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use serde::Deserialize; -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("/thread") + web::scope("/thread") .service(thread_get) .service(thread_send_message), ); - cfg.service(utoipa_actix_web::scope("/message").service(message_delete)); + cfg.service(web::scope("/message").service(message_delete)); cfg.service(threads_get); } @@ -185,3 +185,31 @@ pub async fn message_delete( .await .or_else(v2_reroute::flatten_404_error) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(thread_get, threads_get, thread_send_message, message_delete,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(threads_get,))] +pub(crate) struct RootRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(thread_get, thread_send_message,))] +pub(crate) struct ThreadRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(message_delete,))] +pub(crate) struct MessageRoutesDoc; + +pub(crate) struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let openapi = RootRoutesDoc::openapi(); + openapi + .nest("/thread", ThreadRoutesDoc::openapi()) + .nest("/message", MessageRoutesDoc::openapi()) + } +} diff --git a/apps/labrinth/src/routes/v2/users.rs b/apps/labrinth/src/routes/v2/users.rs index 7f63046afa..b57430c8a0 100644 --- a/apps/labrinth/src/routes/v2/users.rs +++ b/apps/labrinth/src/routes/v2/users.rs @@ -13,11 +13,11 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::{Deserialize, Serialize}; 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(user_auth_get); cfg.service(users_get); cfg.service( - utoipa_actix_web::scope("/user") + web::scope("/user") .service(user_get) .service(projects_list) .service(user_delete) @@ -486,3 +486,46 @@ pub async fn user_notifications( Err(response) => Ok(response), } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + user_auth_get, + users_get, + user_get, + projects_list, + user_edit, + user_icon_edit, + user_icon_delete, + user_delete, + user_follows, + user_notifications, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(users_get,))] +pub(crate) struct RootRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + user_auth_get, + user_get, + projects_list, + user_edit, + user_icon_edit, + user_icon_delete, + user_delete, + user_follows, + user_notifications, +))] +pub(crate) struct UserRoutesDoc; + +pub(crate) struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let openapi = RootRoutesDoc::openapi(); + openapi.nest("/user", UserRoutesDoc::openapi()) + } +} diff --git a/apps/labrinth/src/routes/v2/version_creation.rs b/apps/labrinth/src/routes/v2/version_creation.rs index 9990f95141..9dacc4e2a9 100644 --- a/apps/labrinth/src/routes/v2/version_creation.rs +++ b/apps/labrinth/src/routes/v2/version_creation.rs @@ -356,3 +356,16 @@ pub async fn upload_file_to_version( .await?; Ok(response) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(version_create, upload_file_to_version,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(version_create,))] +pub(crate) struct RootRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(upload_file_to_version,))] +pub(crate) struct VersionRoutesDoc; diff --git a/apps/labrinth/src/routes/v2/version_file.rs b/apps/labrinth/src/routes/v2/version_file.rs index 4e0ac19dc1..bc9e4b52d3 100644 --- a/apps/labrinth/src/routes/v2/version_file.rs +++ b/apps/labrinth/src/routes/v2/version_file.rs @@ -11,9 +11,9 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service( - utoipa_actix_web::scope("/version_file") + web::scope("/version_file") .service(delete_file) .service(get_version_from_hash) .service(download_version) @@ -22,7 +22,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { ); cfg.service( - utoipa_actix_web::scope("/version_files") + web::scope("/version_files") .service(get_versions_from_hashes) .service(update_files) .service(update_files_many) @@ -615,3 +615,46 @@ pub async fn update_individual_files( Err(response) => Ok(response), } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + get_version_from_hash, + download_version, + delete_file, + get_update_from_hash, + get_versions_from_hashes, + get_projects_from_hashes, + update_files, + update_files_many, + update_individual_files, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + get_version_from_hash, + download_version, + delete_file, + get_update_from_hash, + get_projects_from_hashes, +))] +pub(crate) struct VersionFileRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + get_versions_from_hashes, + update_files, + update_files_many, + update_individual_files, +))] +pub(crate) struct VersionFilesRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi( + nest( + (path = "/version_file", api = VersionFileRoutesDoc), + (path = "/version_files", api = VersionFilesRoutesDoc), + ) +)] +pub(crate) struct ApiDoc; diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs index 9417bf8bcf..41e91a825a 100644 --- a/apps/labrinth/src/routes/v2/versions.rs +++ b/apps/labrinth/src/routes/v2/versions.rs @@ -16,11 +16,11 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::{Deserialize, Serialize}; 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(versions_get); cfg.service(super::version_creation::version_create); cfg.service( - utoipa_actix_web::scope("/version") + web::scope("/version") .service(version_get) .service(version_delete) .service(version_edit) @@ -537,3 +537,43 @@ pub async fn version_delete( .await .or_else(v2_reroute::flatten_404_error) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + version_list, + version_project_get, + versions_get, + version_get, + version_edit, + version_delete, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(versions_get,))] +pub(crate) struct RootRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(version_get, version_edit, version_delete,))] +pub(crate) struct VersionRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(version_list, version_project_get,))] +pub(crate) struct ProjectRoutesDoc; + +pub(crate) struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let mut openapi = RootRoutesDoc::openapi(); + openapi.merge(super::version_creation::RootRoutesDoc::openapi()); + openapi + .nest("/version", VersionRoutesDoc::openapi()) + .nest("/project/{project_id}", ProjectRoutesDoc::openapi()) + .nest( + "/version", + super::version_creation::VersionRoutesDoc::openapi(), + ) + } +} diff --git a/apps/labrinth/src/routes/v3/analytics_event.rs b/apps/labrinth/src/routes/v3/analytics_event.rs index d35159e12e..94939bed1f 100644 --- a/apps/labrinth/src/routes/v3/analytics_event.rs +++ b/apps/labrinth/src/routes/v3/analytics_event.rs @@ -22,7 +22,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(analytics_events_get) .service(analytics_event_create) .service(analytics_event_edit) @@ -199,3 +199,13 @@ pub async fn analytics_event_delete( Ok(()) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + analytics_events_get, + analytics_event_create, + analytics_event_edit, + analytics_event_delete, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs index b5d019155c..598c2d9336 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs @@ -14,11 +14,7 @@ use crate::{ routes::ApiError, }; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { - cfg.service(fetch_facets); -} - -pub fn web_config(cfg: &mut web::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(fetch_facets); } @@ -88,3 +84,8 @@ pub async fn fetch_facets( Ok(web::Json(FacetsResponse { facets })) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(fetch_facets,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/analytics_get/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/mod.rs index 4ddea33c1b..91056044ab 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/mod.rs @@ -54,18 +54,12 @@ use crate::{ pub(crate) use metrics::normalize_download_source; pub use metrics::*; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(fetch_analytics); cfg.configure(facets::config); cfg.configure(old::config); } -pub fn web_config(cfg: &mut web::ServiceConfig) { - cfg.service(fetch_analytics); - cfg.configure(facets::web_config); - cfg.configure(old::web_config); -} - // request /// Requests analytics data, aggregating over all possible analytics sources @@ -1078,3 +1072,19 @@ mod tests { assert_eq!(serde_json::to_value(src).unwrap(), target); } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(fetch_analytics,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +pub(crate) struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let mut openapi = RouteDoc::openapi(); + openapi.merge(facets::RouteDoc::openapi()); + openapi.merge(old::RouteDoc::openapi()); + openapi + } +} diff --git a/apps/labrinth/src/routes/v3/analytics_get/old.rs b/apps/labrinth/src/routes/v3/analytics_get/old.rs index 05e1822815..b7927b1213 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/old.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/old.rs @@ -21,16 +21,7 @@ use std::collections::HashMap; use std::convert::TryInto; use std::num::NonZeroU32; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { - cfg.service(playtimes_get) - .service(views_get) - .service(downloads_get) - .service(revenue_get) - .service(countries_downloads_get) - .service(countries_views_get); -} - -pub fn web_config(cfg: &mut web::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(playtimes_get) .service(views_get) .service(downloads_get) @@ -730,3 +721,15 @@ async fn filter_allowed_ids( // Only one of project_ids or version_ids will be Some Ok(project_ids) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + playtimes_get, + views_get, + downloads_get, + revenue_get, + countries_downloads_get, + countries_views_get, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/collections.rs b/apps/labrinth/src/routes/v3/collections.rs index ed4a1ab2cb..a38e18bb2e 100644 --- a/apps/labrinth/src/routes/v3/collections.rs +++ b/apps/labrinth/src/routes/v3/collections.rs @@ -19,7 +19,7 @@ use crate::util::routes::read_limited_from_payload; use crate::util::validate::validation_errors_to_string; use crate::{database, models}; use actix_web::web::Data; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use ariadne::ids::base62_impl::parse_base62; use chrono::Utc; use eyre::eyre; @@ -27,21 +27,17 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/collections", web::get().to(collections_get)); - cfg.route("/collection", web::post().to(collection_create)); - - cfg.service( - web::scope("/collection") - .route("/{id}", web::get().to(collection_get)) - .route("/{id}", web::delete().to(collection_delete)) - .route("/{id}", web::patch().to(collection_edit)) - .route("/{id}/icon", web::patch().to(collection_icon_edit)) - .route("/{id}/icon", web::delete().to(delete_collection_icon)), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(collections_get) + .service(collection_create) + .service(collection_get) + .service(collection_delete) + .service(collection_edit) + .service(collection_icon_edit) + .service(delete_collection_icon); } -#[derive(Serialize, Deserialize, Validate, Clone)] +#[derive(Serialize, Deserialize, Validate, Clone, utoipa::ToSchema)] pub struct CollectionCreateData { #[validate( length(min = 3, max = 64), @@ -58,6 +54,8 @@ pub struct CollectionCreateData { pub projects: Vec, } +#[utoipa::path(tag = "collections", responses((status = OK)))] +#[post("/collection")] pub async fn collection_create( req: HttpRequest, collection_create_data: web::Json, @@ -142,6 +140,8 @@ pub async fn collection_create( pub struct CollectionIds { pub ids: String, } +#[utoipa::path(tag = "collections", responses((status = OK)))] +#[get("/collections")] pub async fn collections_get( req: HttpRequest, web::Query(ids): web::Query, @@ -178,6 +178,8 @@ pub async fn collections_get( Ok(HttpResponse::Ok().json(collections)) } +#[utoipa::path(tag = "collections", responses((status = OK)))] +#[get("/collection/{id}")] pub async fn collection_get( req: HttpRequest, info: web::Path<(String,)>, @@ -209,7 +211,7 @@ pub async fn collection_get( Err(ApiError::NotFound) } -#[derive(Deserialize, Validate)] +#[derive(Deserialize, Validate, utoipa::ToSchema)] pub struct EditCollection { #[validate( length(min = 3, max = 64), @@ -223,11 +225,14 @@ pub struct EditCollection { with = "::serde_with::rust::double_option" )] pub description: Option>, + #[schema(value_type = Option)] pub status: Option, #[validate(length(max = 1024))] pub new_projects: Option>, } +#[utoipa::path(tag = "collections", responses((status = NO_CONTENT)))] +#[patch("/collection/{id}")] pub async fn collection_edit( req: HttpRequest, info: web::Path<(String,)>, @@ -383,6 +388,8 @@ pub struct Extension { } #[allow(clippy::too_many_arguments)] +#[utoipa::path(tag = "collections", responses((status = NO_CONTENT)))] +#[patch("/collection/{id}/icon")] pub async fn collection_icon_edit( web::Query(ext): web::Query, req: HttpRequest, @@ -468,6 +475,8 @@ pub async fn collection_icon_edit( Ok(HttpResponse::NoContent().body("")) } +#[utoipa::path(tag = "collections", responses((status = NO_CONTENT)))] +#[delete("/collection/{id}/icon")] pub async fn delete_collection_icon( req: HttpRequest, info: web::Path<(String,)>, @@ -527,6 +536,8 @@ pub async fn delete_collection_icon( Ok(HttpResponse::NoContent().body("")) } +#[utoipa::path(tag = "collections", responses((status = NO_CONTENT)))] +#[delete("/collection/{id}")] pub async fn collection_delete( req: HttpRequest, info: web::Path<(String,)>, @@ -581,3 +592,16 @@ fn can_modify_collection( ) -> bool { collection.user_id == user.id.into() || user.role.is_mod() } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + collection_create, + collections_get, + collection_get, + collection_edit, + collection_icon_edit, + delete_collection_icon, + collection_delete, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/content/mod.rs b/apps/labrinth/src/routes/v3/content/mod.rs index dc571b6fd9..85d626e691 100644 --- a/apps/labrinth/src/routes/v3/content/mod.rs +++ b/apps/labrinth/src/routes/v3/content/mod.rs @@ -27,14 +27,8 @@ const CONTENT_RESOLVE_CACHE_HEAT_NAMESPACE: &str = "content_resolve_heat"; const CONTENT_RESOLVE_CACHE_SCHEMA_VERSION: &str = "v1"; const CONTENT_RESOLVE_CACHE_HEAT_WINDOW_SECONDS: i64 = 60 * 60 * 24; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(resolve_content); -} - -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service(utoipa_actix_web::scope("/v3").service(resolve_content)); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(web::scope("/v3").service(resolve_content)); } /// Resolve content. @@ -654,3 +648,8 @@ fn resolve_error_to_api(error: ResolveError) -> ApiError { } } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(resolve_content,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/friends.rs b/apps/labrinth/src/routes/v3/friends.rs index 5bed3aac78..e5bacae67c 100644 --- a/apps/labrinth/src/routes/v3/friends.rs +++ b/apps/labrinth/src/routes/v3/friends.rs @@ -17,7 +17,7 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use ariadne::networking::message::ServerToClientMessage; use chrono::Utc; -pub fn config(cfg: &mut web::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(add_friend); cfg.service(remove_friend); cfg.service(friends); @@ -206,3 +206,8 @@ pub async fn friends( Ok(HttpResponse::Ok().json(friends)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(add_friend, remove_friend, friends,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/images.rs b/apps/labrinth/src/routes/v3/images.rs index 95a98b0b2d..324cd8189f 100644 --- a/apps/labrinth/src/routes/v3/images.rs +++ b/apps/labrinth/src/routes/v3/images.rs @@ -14,11 +14,11 @@ use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::img::upload_image_optimized; use crate::util::routes::read_limited_from_payload; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, post, web}; use serde::{Deserialize, Serialize}; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/image", web::post().to(images_add)); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(images_add); } #[derive(Serialize, Deserialize)] @@ -36,6 +36,8 @@ pub struct ImageUpload { pub report_id: Option, } +#[utoipa::path(tag = "images", responses((status = OK)))] +#[post("/image")] pub async fn images_add( req: HttpRequest, web::Query(data): web::Query, @@ -253,3 +255,8 @@ pub async fn images_add( Ok(HttpResponse::Ok().json(image)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(images_add,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/limits.rs b/apps/labrinth/src/routes/v3/limits.rs index 3b71d2d91d..e8c419759d 100644 --- a/apps/labrinth/src/routes/v3/limits.rs +++ b/apps/labrinth/src/routes/v3/limits.rs @@ -6,17 +6,16 @@ use crate::{ queue::session::AuthQueue, routes::ApiError, }; -use actix_web::{HttpRequest, web}; +use actix_web::{HttpRequest, get, web}; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/limits") - .route("/projects", web::get().to(get_project_limits)) - .route("/organizations", web::get().to(get_organization_limits)) - .route("/collections", web::get().to(get_collection_limits)), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(get_project_limits) + .service(get_organization_limits) + .service(get_collection_limits); } +#[utoipa::path(tag = "limits", responses((status = OK)))] +#[get("/limits/projects")] async fn get_project_limits( req: HttpRequest, pool: web::Data, @@ -36,6 +35,8 @@ async fn get_project_limits( Ok(web::Json(limits)) } +#[utoipa::path(tag = "limits", responses((status = OK)))] +#[get("/limits/organizations")] async fn get_organization_limits( req: HttpRequest, pool: web::Data, @@ -55,6 +56,8 @@ async fn get_organization_limits( Ok(web::Json(limits)) } +#[utoipa::path(tag = "limits", responses((status = OK)))] +#[get("/limits/collections")] async fn get_collection_limits( req: HttpRequest, pool: web::Data, @@ -73,3 +76,12 @@ async fn get_collection_limits( let limits = UserLimits::get_for_collections(&user, &pool).await?; Ok(web::Json(limits)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + get_project_limits, + get_organization_limits, + get_collection_limits, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/mod.rs b/apps/labrinth/src/routes/v3/mod.rs index 7b266e26de..9a6494bbf2 100644 --- a/apps/labrinth/src/routes/v3/mod.rs +++ b/apps/labrinth/src/routes/v3/mod.rs @@ -30,24 +30,34 @@ pub mod versions; pub mod oauth_clients; pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/v3/analytics") + .wrap(default_cors()) + .configure(analytics_get::config), + ); + cfg.service( + web::scope("/v3/payout") + .wrap(default_cors()) + .configure(payouts::config), + ); + cfg.service( + web::scope("/v3/project") + .wrap(default_cors()) + .configure(projects::project_config) + .configure(project_creation::config), + ); cfg.service( web::scope("/v3") .wrap(default_cors()) .configure(limits::config) .configure(collections::config) - .configure(content::config) .configure(images::config) .configure(notifications::config) .configure(oauth_clients::config) .configure(organizations::config) - .configure(payouts::webhook_config) - .configure(payouts::web_config) + .service(payouts::paypal_webhook) + .service(payouts::tremendous_webhook) .configure(projects::config) - .service( - web::scope("/project") - .configure(project_creation::web_config) - .configure(projects::project_config), - ) .configure(reports::config) .configure(shared_instance_version_creation::config) .configure(shared_instances::config) @@ -58,52 +68,44 @@ pub fn config(cfg: &mut web::ServiceConfig) { .configure(users::config) .configure(version_file::config) .configure(versions::config) - .service( - web::scope("/analytics").configure(analytics_get::web_config), - ) .configure(friends::config), ); + cfg.configure(content::config); } -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { - cfg.service( - utoipa_actix_web::scope("/v3/analytics") - .wrap(default_cors()) - .configure(analytics_get::config), - ); - cfg.service( - utoipa_actix_web::scope("/v3/payout") - .wrap(default_cors()) - .configure(payouts::config), - ); - cfg.service( - utoipa_actix_web::scope("/v3/project") - .wrap(default_cors()) - .configure(projects::utoipa_config) - .configure(project_creation::config), - ); - cfg.service( - utoipa_actix_web::scope("/v3") - .wrap(default_cors()) - .service(friends::add_friend) - .service(friends::remove_friend) - .service(friends::friends) - .service(projects::project_search) - .service(projects::project_search_post) - .service(oauth_clients::get_client) - .service(oauth_clients::get_clients) - .service(oauth_clients::oauth_client_create) - .service(oauth_clients::oauth_client_delete) - .service(oauth_clients::oauth_client_edit) - .service(oauth_clients::oauth_client_icon_edit) - .service(oauth_clients::oauth_client_icon_delete) - .service(oauth_clients::get_user_oauth_authorizations) - .service(oauth_clients::revoke_oauth_authorization), - ); - cfg.configure(content::utoipa_config); -} +#[derive(utoipa::OpenApi)] +#[openapi( + nest( + (path = "/v3/analytics", api = analytics_get::ApiDoc), + (path = "/v3/payout", api = payouts::PayoutRoutesDoc), + (path = "/v3/project", api = projects::ProjectRoutesDoc), + (path = "/v3/project", api = project_creation::ApiDoc), + (path = "/v3/project", api = teams::ProjectRoutesDoc), + (path = "/v3/project", api = versions::ProjectRoutesDoc), + (path = "/v3", api = limits::RouteDoc), + (path = "/v3", api = collections::RouteDoc), + (path = "/v3", api = images::RouteDoc), + (path = "/v3", api = notifications::RouteDoc), + (path = "/v3", api = oauth_clients::ApiDoc), + (path = "/v3", api = organizations::RouteDoc), + (path = "/v3", api = payouts::WebhookRoutesDoc), + (path = "/v3", api = projects::RootRoutesDoc), + (path = "/v3", api = reports::RouteDoc), + (path = "/v3", api = shared_instance_version_creation::RouteDoc), + (path = "/v3", api = shared_instances::RouteDoc), + (path = "/v3", api = statistics::RouteDoc), + (path = "/v3", api = tags::RouteDoc), + (path = "/v3", api = teams::RootRoutesDoc), + (path = "/v3", api = threads::RouteDoc), + (path = "/v3", api = users::RouteDoc), + (path = "/v3", api = version_creation::RouteDoc), + (path = "/v3", api = version_file::RouteDoc), + (path = "/v3", api = versions::RootRoutesDoc), + (path = "/v3", api = friends::RouteDoc), + (path = "/v3", api = content::RouteDoc), + ) +)] +pub struct ApiDoc; pub async fn hello_world() -> Result { Ok(HttpResponse::Ok().json(json!({ diff --git a/apps/labrinth/src/routes/v3/notifications.rs b/apps/labrinth/src/routes/v3/notifications.rs index 4cd3787ad9..5c808808f3 100644 --- a/apps/labrinth/src/routes/v3/notifications.rs +++ b/apps/labrinth/src/routes/v3/notifications.rs @@ -7,20 +7,16 @@ use crate::models::notifications::Notification; use crate::models::pats::Scopes; use crate::queue::session::AuthQueue; use crate::routes::ApiError; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::{Deserialize, Serialize}; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/notifications", web::get().to(notifications_get)); - cfg.route("/notifications", web::patch().to(notifications_read)); - cfg.route("/notifications", web::delete().to(notifications_delete)); - - cfg.service( - web::scope("/notification") - .route("/{id}", web::get().to(notification_get)) - .route("/{id}", web::patch().to(notification_read)) - .route("/{id}", web::delete().to(notification_delete)), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(notifications_get_route) + .service(notifications_read_route) + .service(notifications_delete_route) + .service(notification_get_route) + .service(notification_read_route) + .service(notification_delete_route); } #[derive(Serialize, Deserialize)] @@ -28,6 +24,18 @@ pub struct NotificationIds { pub ids: String, } +#[utoipa::path(tag = "notifications", responses((status = OK)))] +#[get("/notifications")] +async fn notifications_get_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + notifications_get(req, ids, pool, redis, session_queue).await +} + pub async fn notifications_get( req: HttpRequest, web::Query(ids): web::Query, @@ -70,6 +78,18 @@ pub async fn notifications_get( Ok(HttpResponse::Ok().json(notifications)) } +#[utoipa::path(tag = "notifications", responses((status = OK)))] +#[get("/notification/{id}")] +async fn notification_get_route( + req: HttpRequest, + info: web::Path<(NotificationId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + notification_get(req, info, pool, redis, session_queue).await +} + pub async fn notification_get( req: HttpRequest, info: web::Path<(NotificationId,)>, @@ -107,6 +127,18 @@ pub async fn notification_get( } } +#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))] +#[patch("/notification/{id}")] +async fn notification_read_route( + req: HttpRequest, + info: web::Path<(NotificationId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + notification_read(req, info, pool, redis, session_queue).await +} + pub async fn notification_read( req: HttpRequest, info: web::Path<(NotificationId,)>, @@ -157,6 +189,18 @@ pub async fn notification_read( } } +#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))] +#[delete("/notification/{id}")] +async fn notification_delete_route( + req: HttpRequest, + info: web::Path<(NotificationId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + notification_delete(req, info, pool, redis, session_queue).await +} + pub async fn notification_delete( req: HttpRequest, info: web::Path<(NotificationId,)>, @@ -208,6 +252,18 @@ pub async fn notification_delete( } } +#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))] +#[patch("/notifications")] +async fn notifications_read_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + notifications_read(req, ids, pool, redis, session_queue).await +} + pub async fn notifications_read( req: HttpRequest, web::Query(ids): web::Query, @@ -261,6 +317,18 @@ pub async fn notifications_read( Ok(HttpResponse::NoContent().body("")) } +#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))] +#[delete("/notifications")] +async fn notifications_delete_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + notifications_delete(req, ids, pool, redis, session_queue).await +} + pub async fn notifications_delete( req: HttpRequest, web::Query(ids): web::Query, @@ -313,3 +381,15 @@ pub async fn notifications_delete( Ok(HttpResponse::NoContent().body("")) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + notifications_get_route, + notification_get_route, + notification_read_route, + notification_delete_route, + notifications_read_route, + notifications_delete_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index 663f22a8f8..c33f8c469f 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -29,10 +29,7 @@ use crate::{ file_hosting::FileHost, models::oauth_clients::DeleteOAuthClientQueryParam, util::routes::read_limited_from_payload, }; -use actix_web::{ - HttpRequest, HttpResponse, delete, get, patch, post, - web::{self, scope}, -}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use ariadne::ids::base62_impl::parse_base62; use chrono::Utc; use itertools::Itertools; @@ -41,9 +38,9 @@ use rand_chacha::ChaCha20Rng; use serde::{Deserialize, Serialize}; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service( - scope("/oauth") + web::scope("/oauth") .configure(crate::auth::oauth::config) .service(revoke_oauth_authorization) .service(oauth_client_create) @@ -57,6 +54,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } +#[utoipa::path(tag = "oauth clients", responses((status = OK)))] +#[get("/user/{id}/oauth_apps")] pub async fn get_user_clients( req: HttpRequest, info: web::Path, @@ -622,3 +621,28 @@ pub async fn get_clients_inner( Ok(clients.into_iter().map(|c| c.into()).collect_vec()) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + get_user_clients, + get_client, + get_clients, + oauth_client_create, + oauth_client_delete, + oauth_client_edit, + oauth_client_icon_edit, + oauth_client_icon_delete, + get_user_oauth_authorizations, + revoke_oauth_authorization, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi( + nest( + (path = "/oauth", api = RouteDoc), + (path = "/oauth", api = crate::auth::oauth::RouteDoc), + ) +)] +pub(crate) struct ApiDoc; diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index 5f77b3479f..1ed66de412 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -21,37 +21,29 @@ use crate::util::img::delete_old_images; use crate::util::routes::read_limited_from_payload; use crate::util::validate::validation_errors_to_string; use crate::{database, models}; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use ariadne::ids::UserId; use futures::TryStreamExt; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/organizations", web::get().to(organizations_get)); - cfg.service( - web::scope("/organization") - .route("", web::post().to(organization_create)) - .route("/{id}/projects", web::get().to(organization_projects_get)) - .route("/{id}/notes", web::patch().to(organization_notes_edit)) - .route("/{id}", web::get().to(organization_get)) - .route("/{id}", web::patch().to(organizations_edit)) - .route("/{id}", web::delete().to(organization_delete)) - .route("/{id}/projects", web::post().to(organization_projects_add)) - .route( - "/{id}/projects/{project_id}", - web::delete().to(organization_projects_remove), - ) - .route("/{id}/icon", web::patch().to(organization_icon_edit)) - .route("/{id}/icon", web::delete().to(delete_organization_icon)) - .route( - "/{id}/members", - web::get().to(super::teams::team_members_get_organization), - ), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(organizations_get) + .service(organization_create) + .service(organization_projects_get) + .service(organization_notes_edit) + .service(organization_get) + .service(organizations_edit) + .service(organization_delete) + .service(organization_projects_add) + .service(organization_projects_remove) + .service(organization_icon_edit) + .service(delete_organization_icon); } +#[utoipa::path(tag = "organizations", responses((status = OK)))] +#[get("/organization/{id}/projects")] pub async fn organization_projects_get( req: HttpRequest, info: web::Path<(String,)>, @@ -106,7 +98,7 @@ pub async fn organization_projects_get( } } -#[derive(Deserialize, Validate)] +#[derive(Deserialize, Validate, utoipa::ToSchema)] pub struct NewOrganization { #[validate( length(min = 3, max = 64), @@ -120,6 +112,8 @@ pub struct NewOrganization { pub description: String, } +#[utoipa::path(tag = "organizations", responses((status = OK)))] +#[post("/organization")] pub async fn organization_create( req: HttpRequest, new_organization: web::Json, @@ -222,6 +216,8 @@ pub async fn organization_create( Ok(HttpResponse::Ok().json(organization)) } +#[utoipa::path(tag = "organizations", responses((status = OK)))] +#[get("/organization/{id}")] pub async fn organization_get( req: HttpRequest, info: web::Path<(String,)>, @@ -300,6 +296,8 @@ pub async fn organization_get( Err(ApiError::NotFound) } +#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))] +#[patch("/organization/{id}/notes")] pub async fn organization_notes_edit( req: HttpRequest, info: web::Path<(String,)>, @@ -380,6 +378,8 @@ pub struct OrganizationIds { pub ids: String, } +#[utoipa::path(tag = "organizations", responses((status = OK)))] +#[get("/organizations")] pub async fn organizations_get( req: HttpRequest, web::Query(ids): web::Query, @@ -484,7 +484,7 @@ pub async fn organizations_get( Ok(HttpResponse::Ok().json(organizations)) } -#[derive(Serialize, Deserialize, Validate)] +#[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)] pub struct OrganizationEdit { #[validate(length(min = 3, max = 256))] pub description: Option, @@ -497,6 +497,8 @@ pub struct OrganizationEdit { pub name: Option, } +#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))] +#[patch("/organization/{id}")] pub async fn organizations_edit( req: HttpRequest, info: web::Path<(String,)>, @@ -658,6 +660,8 @@ pub async fn organizations_edit( } } +#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))] +#[delete("/organization/{id}")] pub async fn organization_delete( req: HttpRequest, info: web::Path<(String,)>, @@ -823,10 +827,12 @@ pub async fn organization_delete( } } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct OrganizationProjectAdd { pub project_id: String, // Also allow name/slug } +#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))] +#[post("/organization/{id}/projects")] pub async fn organization_projects_add( req: HttpRequest, info: web::Path<(String,)>, @@ -985,13 +991,15 @@ pub async fn organization_projects_add( Ok(HttpResponse::Ok().finish()) } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct OrganizationProjectRemoval { // A new owner must be supplied for the project. // That user must be a member of the organization, but not necessarily a member of the project. pub new_owner: UserId, } +#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))] +#[delete("/organization/{id}/projects/{project_id}")] pub async fn organization_projects_remove( req: HttpRequest, info: web::Path<(String, String)>, @@ -1181,6 +1189,8 @@ pub struct Extension { } #[allow(clippy::too_many_arguments)] +#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))] +#[patch("/organization/{id}/icon")] pub async fn organization_icon_edit( web::Query(ext): web::Query, req: HttpRequest, @@ -1288,6 +1298,8 @@ pub async fn organization_icon_edit( Ok(HttpResponse::NoContent().body("")) } +#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))] +#[delete("/organization/{id}/icon")] pub async fn delete_organization_icon( req: HttpRequest, info: web::Path<(String,)>, @@ -1371,3 +1383,20 @@ pub async fn delete_organization_icon( Ok(HttpResponse::NoContent().body("")) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + organization_projects_get, + organization_create, + organization_get, + organization_notes_edit, + organizations_get, + organizations_edit, + organization_delete, + organization_projects_add, + organization_projects_remove, + organization_icon_edit, + delete_organization_icon, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index 668e854766..8c460084d5 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -33,7 +33,7 @@ use tracing::error; const COMPLIANCE_CHECK_DEBOUNCE: chrono::Duration = chrono::Duration::seconds(15); -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(transaction_history) .service(calculate_fees) .service(create_payout) @@ -44,24 +44,6 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { .service(post_compliance_form); } -pub fn web_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/payout") - .service(transaction_history) - .service(calculate_fees) - .service(create_payout) - .service(cancel_payout) - .service(payment_methods) - .service(get_balance) - .service(platform_revenue) - .service(post_compliance_form), - ); -} - -pub fn webhook_config(cfg: &mut web::ServiceConfig) { - cfg.service(paypal_webhook).service(tremendous_webhook); -} - #[derive(Deserialize, utoipa::ToSchema)] pub struct RequestForm { form_type: users_compliance::FormType, @@ -171,6 +153,7 @@ pub async fn post_compliance_form( } /// Receive PayPal webhook. +#[utoipa::path(tag = "payouts", responses((status = NO_CONTENT)))] #[post("/_paypal")] pub async fn paypal_webhook( req: HttpRequest, @@ -329,6 +312,7 @@ pub async fn paypal_webhook( } /// Receive Tremendous webhook. +#[utoipa::path(tag = "payouts", responses((status = NO_CONTENT)))] #[post("/_tremendous")] pub async fn tremendous_webhook( req: HttpRequest, @@ -1242,3 +1226,36 @@ pub async fn platform_revenue( Ok(HttpResponse::Ok().json(res)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + post_compliance_form, + paypal_webhook, + tremendous_webhook, + calculate_fees, + create_payout, + transaction_history, + cancel_payout, + payment_methods, + get_balance, + platform_revenue, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + post_compliance_form, + calculate_fees, + create_payout, + transaction_history, + cancel_payout, + payment_methods, + get_balance, + platform_revenue, +))] +pub(crate) struct PayoutRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(paypal_webhook, tremendous_webhook,))] +pub(crate) struct WebhookRoutesDoc; diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 1344890aa1..5b77c0979f 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -46,18 +46,12 @@ use validator::Validate; mod new; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(project_create) .service(project_create_with_id) .configure(new::config); } -pub fn web_config(cfg: &mut web::ServiceConfig) { - cfg.route("", web::post().to(project_create_web)) - .service(project_create_with_id) - .configure(new::web_config); -} - #[derive(Error, Debug)] pub enum CreateError { #[error("An unknown database error occurred")] @@ -315,29 +309,6 @@ pub async fn project_create( .await } -async fn project_create_web( - req: HttpRequest, - payload: Multipart, - client: Data, - redis: Data, - file_host: Data, - session_queue: Data, - http: Data, - search_state: Data, -) -> Result { - project_create_internal( - req, - payload, - client, - redis, - file_host, - session_queue, - http, - search_state, - ) - .await -} - pub async fn project_create_internal( req: HttpRequest, mut payload: Multipart, @@ -1207,3 +1178,18 @@ async fn process_icon_upload( upload_result.color, )) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(project_create, project_create_with_id,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +pub(crate) struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let mut openapi = RouteDoc::openapi(); + openapi.merge(new::RouteDoc::openapi()); + openapi + } +} diff --git a/apps/labrinth/src/routes/v3/project_creation/new.rs b/apps/labrinth/src/routes/v3/project_creation/new.rs index 8d72ae7bc0..0fe952fcdd 100644 --- a/apps/labrinth/src/routes/v3/project_creation/new.rs +++ b/apps/labrinth/src/routes/v3/project_creation/new.rs @@ -36,11 +36,7 @@ use crate::{ }, }; -pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { - cfg.service(create); -} - -pub fn web_config(cfg: &mut web::ServiceConfig) { +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(create); } @@ -412,3 +408,8 @@ mod tests { } } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(create,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index a7d65ea24c..3a7b10c595 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -45,36 +45,15 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(project_search); - cfg.service(project_search_post); - cfg.route("/projects", web::get().to(projects_get)); - cfg.route("/projects", web::patch().to(projects_edit)); - cfg.route("/projects_random", web::get().to(random_projects_get)); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(project_search) + .service(project_search_post) + .service(projects_get_route) + .service(projects_edit_route) + .service(random_projects_get_route); } -pub fn project_config(cfg: &mut web::ServiceConfig) { - cfg.service(project_get) - .service(project_get_check) - .service(project_delete) - .service(project_edit) - .service(project_icon_edit) - .service(delete_project_icon) - .service(add_gallery_item) - .service(edit_gallery_item) - .service(delete_gallery_item) - .service(project_follow) - .service(project_unfollow) - .service(project_get_organization) - .service(super::teams::team_members_get_project) - .service(super::versions::version_list) - .service(super::versions::version_project_get) - .service(dependency_list); -} - -pub fn utoipa_config( - cfg: &mut utoipa_actix_web::service_config::ServiceConfig, -) { +pub fn project_config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(project_get) .service(project_get_check) .service(project_delete) @@ -118,6 +97,16 @@ pub struct RandomProjects { pub count: u32, } +#[utoipa::path(tag = "projects", responses((status = OK)))] +#[get("/projects_random")] +async fn random_projects_get_route( + count: web::Query, + pool: web::Data, + redis: web::Data, +) -> Result { + random_projects_get(count, pool, redis).await +} + pub async fn random_projects_get( web::Query(count): web::Query, pool: web::Data, @@ -169,6 +158,18 @@ pub struct ProjectCheckResponse { pub id: ProjectId, } +#[utoipa::path(tag = "projects", responses((status = OK)))] +#[get("/projects")] +async fn projects_get_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + projects_get(req, ids, pool, redis, session_queue).await +} + pub async fn projects_get( req: HttpRequest, web::Query(ids): web::Query, @@ -1494,7 +1495,7 @@ pub struct CategoryChanges<'a> { pub remove_categories: &'a Option>, } -#[derive(Deserialize, Validate)] +#[derive(Deserialize, Validate, utoipa::ToSchema)] pub struct BulkEditProject { #[validate(length(max = 3))] pub categories: Option>, @@ -1514,6 +1515,29 @@ pub struct BulkEditProject { pub link_urls: Option>>, } +#[utoipa::path(tag = "projects", responses((status = NO_CONTENT)))] +#[patch("/projects")] +async fn projects_edit_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + bulk_edit_project: web::Json, + redis: web::Data, + session_queue: web::Data, + search_state: web::Data, +) -> Result { + projects_edit( + req, + ids, + pool, + bulk_edit_project, + redis, + session_queue, + search_state, + ) + .await +} + pub async fn projects_edit( req: HttpRequest, web::Query(ids): web::Query, @@ -3077,3 +3101,55 @@ pub async fn project_get_organization( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + random_projects_get_route, + projects_get_route, + project_get, + project_edit, + project_search, + project_search_post, + project_get_check, + dependency_list, + projects_edit_route, + project_icon_edit, + delete_project_icon, + add_gallery_item, + edit_gallery_item, + delete_gallery_item, + project_delete, + project_follow, + project_unfollow, + project_get_organization, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + project_search, + project_search_post, + projects_get_route, + projects_edit_route, + random_projects_get_route, +))] +pub(crate) struct RootRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + project_get, + project_get_check, + project_delete, + project_edit, + project_icon_edit, + delete_project_icon, + add_gallery_item, + edit_gallery_item, + delete_gallery_item, + project_follow, + project_unfollow, + project_get_organization, + dependency_list, +))] +pub(crate) struct ProjectRoutesDoc; diff --git a/apps/labrinth/src/routes/v3/reports.rs b/apps/labrinth/src/routes/v3/reports.rs index 2bf0355720..29e2167bbc 100644 --- a/apps/labrinth/src/routes/v3/reports.rs +++ b/apps/labrinth/src/routes/v3/reports.rs @@ -18,20 +18,20 @@ use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::img; use crate::util::routes::read_typed_from_payload; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use ariadne::ids::UserId; use ariadne::ids::base62_impl::parse_base62; use chrono::Utc; use serde::Deserialize; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/report", web::post().to(report_create)); - cfg.route("/report", web::get().to(reports)); - cfg.route("/reports", web::get().to(reports_get)); - cfg.route("/report/{id}", web::get().to(report_get)); - cfg.route("/report/{id}", web::patch().to(report_edit)); - cfg.route("/report/{id}", web::delete().to(report_delete)); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(report_create_route) + .service(reports_route) + .service(reports_get_route) + .service(report_get_route) + .service(report_edit_route) + .service(report_delete_route); } #[derive(Deserialize, Validate)] @@ -46,6 +46,18 @@ pub struct CreateReport { pub uploaded_images: Vec, } +#[utoipa::path(tag = "reports", responses((status = OK)))] +#[post("/report")] +async fn report_create_route( + req: HttpRequest, + pool: web::Data, + body: web::Payload, + redis: web::Data, + session_queue: web::Data, +) -> Result { + report_create(req, pool, body, redis, session_queue).await +} + pub async fn report_create( req: HttpRequest, pool: web::Data, @@ -247,6 +259,18 @@ fn default_all() -> bool { true } +#[utoipa::path(tag = "reports", responses((status = OK)))] +#[get("/report")] +async fn reports_route( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + request_opts: web::Query, + session_queue: web::Data, +) -> Result { + reports(req, pool, redis, request_opts, session_queue).await +} + pub async fn reports( req: HttpRequest, pool: web::Data, @@ -322,6 +346,18 @@ pub struct ReportIds { pub ids: String, } +#[utoipa::path(tag = "reports", responses((status = OK)))] +#[get("/reports")] +async fn reports_get_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + reports_get(req, ids, pool, redis, session_queue).await +} + pub async fn reports_get( req: HttpRequest, web::Query(ids): web::Query, @@ -361,6 +397,18 @@ pub async fn reports_get( Ok(HttpResponse::Ok().json(all_reports)) } +#[utoipa::path(tag = "reports", responses((status = OK)))] +#[get("/report/{id}")] +async fn report_get_route( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + info: web::Path<(crate::models::ids::ReportId,)>, + session_queue: web::Data, +) -> Result { + report_get(req, pool, redis, info, session_queue).await +} + pub async fn report_get( req: HttpRequest, pool: web::Data, @@ -395,13 +443,26 @@ pub async fn report_get( } } -#[derive(Deserialize, Validate)] +#[derive(Deserialize, Validate, utoipa::ToSchema)] pub struct EditReport { #[validate(length(max = 65536))] pub body: Option, pub closed: Option, } +#[utoipa::path(tag = "reports", responses((status = NO_CONTENT)))] +#[patch("/report/{id}")] +async fn report_edit_route( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + info: web::Path<(crate::models::ids::ReportId,)>, + session_queue: web::Data, + edit_report: web::Json, +) -> Result { + report_edit(req, pool, redis, info, session_queue, edit_report).await +} + pub async fn report_edit( req: HttpRequest, pool: web::Data, @@ -511,6 +572,18 @@ pub async fn report_edit( } } +#[utoipa::path(tag = "reports", responses((status = NO_CONTENT)))] +#[delete("/report/{id}")] +async fn report_delete_route( + req: HttpRequest, + pool: web::Data, + info: web::Path<(crate::models::ids::ReportId,)>, + redis: web::Data, + session_queue: web::Data, +) -> Result { + report_delete(req, pool, info, redis, session_queue).await +} + pub async fn report_delete( req: HttpRequest, pool: web::Data, @@ -555,3 +628,15 @@ pub async fn report_delete( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + report_create_route, + reports_route, + reports_get_route, + report_get_route, + report_edit_route, + report_delete_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs b/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs index e273638e1c..602d9a23dc 100644 --- a/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs +++ b/apps/labrinth/src/routes/v3/shared_instance_version_creation.rs @@ -20,7 +20,7 @@ use crate::routes::v3::project_creation::UploadedFile; use crate::util::ext::MRPACK_MIME_TYPE; use actix_web::http::header::ContentLength; use actix_web::web::Data; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, post, web}; use bytes::BytesMut; use chrono::Utc; use futures_util::StreamExt; @@ -29,18 +29,14 @@ use hex::FromHex; const MAX_FILE_SIZE: usize = 500 * 1024 * 1024; const MAX_FILE_SIZE_TEXT: &str = "500 MB"; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route( - "shared-instance/{id}/version", - web::post().to(shared_instance_version_create), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(shared_instance_version_create); } /// Create a shared instance version. #[utoipa::path( tag = "versions", post, - path = "/v3/shared-instance/{id}/version", params(("id" = SharedInstanceId, Path, description = "The ID of the shared instance")), responses( (status = 201, description = "Expected response to a valid request", body = SharedInstanceVersion), @@ -56,6 +52,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { ), security(("bearer_auth" = ["SHARED_INSTANCE_VERSION_CREATE"])) )] +#[post("/shared-instance/{id}/version")] #[allow(clippy::too_many_arguments)] pub async fn shared_instance_version_create( req: HttpRequest, @@ -217,3 +214,8 @@ async fn shared_instance_version_create_inner( let version: SharedInstanceVersion = new_version.into(); Ok(HttpResponse::Created().json(version)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(shared_instance_version_create,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/shared_instances.rs b/apps/labrinth/src/routes/v3/shared_instances.rs index 2786595fc0..cdba321516 100644 --- a/apps/labrinth/src/routes/v3/shared_instances.rs +++ b/apps/labrinth/src/routes/v3/shared_instances.rs @@ -19,33 +19,21 @@ use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::routes::read_typed_from_payload; use actix_web::web::{Data, Redirect}; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use futures_util::future::try_join_all; use serde::Deserialize; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/shared-instance", web::post().to(shared_instance_create)); - cfg.route("/shared-instance", web::get().to(shared_instance_list)); - cfg.service( - web::scope("/shared-instance") - .route("/{id}", web::get().to(shared_instance_get)) - .route("/{id}", web::patch().to(shared_instance_edit)) - .route("/{id}", web::delete().to(shared_instance_delete)) - .route( - "/{id}/version", - web::get().to(shared_instance_version_list), - ), - ); - cfg.service( - web::scope("/shared-instance-version") - .route("/{id}", web::get().to(shared_instance_version_get)) - .route("/{id}", web::delete().to(shared_instance_version_delete)) - .route( - "/{id}/download", - web::get().to(shared_instance_version_download), - ), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(shared_instance_create) + .service(shared_instance_list) + .service(shared_instance_get) + .service(shared_instance_edit) + .service(shared_instance_delete) + .service(shared_instance_version_list) + .service(shared_instance_version_get) + .service(shared_instance_version_delete) + .service(shared_instance_version_download); } #[derive(Deserialize, Validate)] @@ -59,6 +47,8 @@ pub struct CreateSharedInstance { pub public: bool, } +#[utoipa::path(tag = "shared instances", responses((status = OK)))] +#[post("/shared-instance")] pub async fn shared_instance_create( req: HttpRequest, pool: Data, @@ -104,6 +94,8 @@ pub async fn shared_instance_create( })) } +#[utoipa::path(tag = "shared instances", responses((status = OK)))] +#[get("/shared-instance")] pub async fn shared_instance_list( req: HttpRequest, pool: Data, @@ -151,6 +143,8 @@ pub async fn shared_instance_list( Ok(HttpResponse::Ok().json(instances)) } +#[utoipa::path(tag = "shared instances", responses((status = OK)))] +#[get("/shared-instance/{id}")] pub async fn shared_instance_get( req: HttpRequest, pool: Data, @@ -222,6 +216,8 @@ pub struct EditSharedInstance { pub public: Option, } +#[utoipa::path(tag = "shared instances", responses((status = NO_CONTENT)))] +#[patch("/shared-instance/{id}")] pub async fn shared_instance_edit( req: HttpRequest, pool: Data, @@ -302,6 +298,8 @@ pub async fn shared_instance_edit( Ok(HttpResponse::NoContent().body("")) } +#[utoipa::path(tag = "shared instances", responses((status = NO_CONTENT)))] +#[delete("/shared-instance/{id}")] pub async fn shared_instance_delete( req: HttpRequest, pool: Data, @@ -358,6 +356,8 @@ pub async fn shared_instance_delete( Ok(HttpResponse::NoContent().body("")) } +#[utoipa::path(tag = "shared instances", responses((status = OK)))] +#[get("/shared-instance/{id}/version")] pub async fn shared_instance_version_list( req: HttpRequest, pool: Data, @@ -404,6 +404,8 @@ pub async fn shared_instance_version_list( } } +#[utoipa::path(tag = "shared instances", responses((status = OK)))] +#[get("/shared-instance-version/{id}")] pub async fn shared_instance_version_get( req: HttpRequest, pool: Data, @@ -464,6 +466,8 @@ async fn can_access_instance_as_maybe_user( })) } +#[utoipa::path(tag = "shared instances", responses((status = NO_CONTENT)))] +#[delete("/shared-instance-version/{id}")] pub async fn shared_instance_version_delete( req: HttpRequest, pool: Data, @@ -563,6 +567,8 @@ async fn delete_instance_version( Ok(()) } +#[utoipa::path(tag = "shared instances", responses((status = TEMPORARY_REDIRECT)))] +#[get("/shared-instance-version/{id}/download")] pub async fn shared_instance_version_download( req: HttpRequest, pool: Data, @@ -612,3 +618,18 @@ pub async fn shared_instance_version_download( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + shared_instance_create, + shared_instance_list, + shared_instance_get, + shared_instance_edit, + shared_instance_delete, + shared_instance_version_list, + shared_instance_version_get, + shared_instance_version_delete, + shared_instance_version_download, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/statistics.rs b/apps/labrinth/src/routes/v3/statistics.rs index 258af981b3..a14183e14f 100644 --- a/apps/labrinth/src/routes/v3/statistics.rs +++ b/apps/labrinth/src/routes/v3/statistics.rs @@ -1,9 +1,9 @@ use crate::database::PgPool; use crate::routes::ApiError; -use actix_web::{HttpResponse, web}; +use actix_web::{HttpResponse, get, web}; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/statistics", web::get().to(get_stats)); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(get_stats_route); } #[derive(serde::Serialize, serde::Deserialize)] @@ -14,6 +14,14 @@ pub struct V3Stats { pub files: Option, } +#[utoipa::path(tag = "statistics", responses((status = OK)))] +#[get("/statistics")] +async fn get_stats_route( + pool: web::Data, +) -> Result { + get_stats(pool).await +} + pub async fn get_stats( pool: web::Data, ) -> Result { @@ -92,3 +100,8 @@ pub async fn get_stats( Ok(HttpResponse::Ok().json(v3_stats)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(get_stats_route,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/tags.rs b/apps/labrinth/src/routes/v3/tags.rs index a8340d07b2..5d46e3d5c3 100644 --- a/apps/labrinth/src/routes/v3/tags.rs +++ b/apps/labrinth/src/routes/v3/tags.rs @@ -8,25 +8,22 @@ use crate::database::models::loader_fields::{ Game, Loader, LoaderField, LoaderFieldEnumValue, LoaderFieldType, }; use crate::database::redis::RedisPool; -use actix_web::{HttpResponse, web}; +use actix_web::{HttpResponse, get, web}; use crate::database::PgPool; use itertools::Itertools; use serde_json::Value; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/tag") - .route("/category", web::get().to(category_list)) - .route("/loader", web::get().to(loader_list)), - ) - .route("/games", web::get().to(games_list)) - .route("/loader_field", web::get().to(loader_fields_list)) - .route("/license", web::get().to(license_list)) - .route("/license/{id}", web::get().to(license_text)) - .route("/link_platform", web::get().to(link_platform_list)) - .route("/report_type", web::get().to(report_type_list)) - .route("/project_type", web::get().to(project_type_list)); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(category_list_route) + .service(loader_list_route) + .service(games_list_route) + .service(loader_fields_list_route) + .service(license_list_route) + .service(license_text_route) + .service(link_platform_list_route) + .service(report_type_list_route) + .service(project_type_list_route); } #[derive(serde::Serialize, serde::Deserialize)] @@ -37,6 +34,15 @@ pub struct GameData { pub banner: Option, } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/games")] +async fn games_list_route( + pool: web::Data, + redis: web::Data, +) -> Result { + games_list(pool, redis).await +} + pub async fn games_list( pool: web::Data, redis: web::Data, @@ -63,6 +69,15 @@ pub struct CategoryData { pub header: String, } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/tag/category")] +async fn category_list_route( + pool: web::Data, + redis: web::Data, +) -> Result { + category_list(pool, redis).await +} + pub async fn category_list( pool: web::Data, redis: web::Data, @@ -91,6 +106,15 @@ pub struct LoaderData { pub metadata: Value, } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/tag/loader")] +async fn loader_list_route( + pool: web::Data, + redis: web::Data, +) -> Result { + loader_list(pool, redis).await +} + pub async fn loader_list( pool: web::Data, redis: web::Data, @@ -131,6 +155,16 @@ pub struct LoaderFieldsEnumQuery { } // Provides the variants for any enumerable loader field. +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/loader_field")] +async fn loader_fields_list_route( + pool: web::Data, + web::Query(query): web::Query, + redis: web::Data, +) -> Result { + loader_fields_list(pool, web::Query(query), redis).await +} + pub async fn loader_fields_list( pool: web::Data, query: web::Query, @@ -181,6 +215,12 @@ pub struct License { pub name: String, } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/license")] +async fn license_list_route() -> HttpResponse { + license_list().await +} + pub async fn license_list() -> HttpResponse { let licenses = spdx::identifiers::LICENSES; let mut results: Vec = Vec::with_capacity(licenses.len()); @@ -201,6 +241,14 @@ pub struct LicenseText { pub body: String, } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/license/{id}")] +async fn license_text_route( + params: web::Path<(String,)>, +) -> Result { + license_text(params).await +} + pub async fn license_text( params: web::Path<(String,)>, ) -> Result { @@ -231,6 +279,15 @@ pub struct LinkPlatformQueryData { pub donation: bool, } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/link_platform")] +async fn link_platform_list_route( + pool: web::Data, + redis: web::Data, +) -> Result { + link_platform_list(pool, redis).await +} + pub async fn link_platform_list( pool: web::Data, redis: web::Data, @@ -247,6 +304,15 @@ pub async fn link_platform_list( Ok(HttpResponse::Ok().json(results)) } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/report_type")] +async fn report_type_list_route( + pool: web::Data, + redis: web::Data, +) -> Result { + report_type_list(pool, redis).await +} + pub async fn report_type_list( pool: web::Data, redis: web::Data, @@ -255,6 +321,15 @@ pub async fn report_type_list( Ok(HttpResponse::Ok().json(results)) } +#[utoipa::path(tag = "tags", responses((status = OK)))] +#[get("/project_type")] +async fn project_type_list_route( + pool: web::Data, + redis: web::Data, +) -> Result { + project_type_list(pool, redis).await +} + pub async fn project_type_list( pool: web::Data, redis: web::Data, @@ -262,3 +337,18 @@ pub async fn project_type_list( let results = ProjectType::list(&**pool, &redis).await?; Ok(HttpResponse::Ok().json(results)) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + games_list_route, + category_list_route, + loader_list_route, + loader_fields_list_route, + license_list_route, + license_text_route, + link_platform_list_route, + report_type_list_route, + project_type_list_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index f31a8cf0ab..03195d0bb4 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -13,26 +13,20 @@ use crate::models::teams::{OrganizationPermissions, ProjectPermissions}; use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::error::Context; -use actix_web::{HttpRequest, HttpResponse, get, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use ariadne::ids::UserId; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/teams", web::get().to(teams_get)); - - cfg.service( - web::scope("/team") - .route("/{id}/members", web::get().to(team_members_get)) - .route("/{id}/members/{user_id}", web::patch().to(edit_team_member)) - .route( - "/{id}/members/{user_id}", - web::delete().to(remove_team_member), - ) - .route("/{id}/members", web::post().to(add_team_member)) - .route("/{id}/join", web::post().to(join_team)) - .route("/{id}/owner", web::patch().to(transfer_ownership)), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(teams_get_route) + .service(team_members_get_route) + .service(team_members_get_organization) + .service(edit_team_member_route) + .service(remove_team_member_route) + .service(add_team_member_route) + .service(join_team_route) + .service(transfer_ownership_route); } // Returns all members of a project, @@ -137,6 +131,8 @@ pub async fn team_members_get_project_internal( } } +#[utoipa::path(tag = "teams", responses((status = OK, body = Vec)))] +#[get("/organization/{id}/members")] pub async fn team_members_get_organization( req: HttpRequest, info: web::Path<(String,)>, @@ -214,6 +210,18 @@ pub async fn team_members_get_organization( } // Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project) +#[utoipa::path(tag = "teams", responses((status = OK, body = Vec)))] +#[get("/team/{id}/members")] +async fn team_members_get_route( + req: HttpRequest, + info: web::Path<(TeamId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + team_members_get(req, info, pool, redis, session_queue).await +} + pub async fn team_members_get( req: HttpRequest, info: web::Path<(TeamId,)>, @@ -279,6 +287,18 @@ pub struct TeamIds { pub ids: String, } +#[utoipa::path(tag = "teams", responses((status = OK)))] +#[get("/teams")] +async fn teams_get_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + teams_get(req, ids, pool, redis, session_queue).await +} + pub async fn teams_get( req: HttpRequest, web::Query(ids): web::Query, @@ -349,6 +369,18 @@ pub async fn teams_get( Ok(HttpResponse::Ok().json(teams)) } +#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))] +#[post("/team/{id}/join")] +async fn join_team_route( + req: HttpRequest, + info: web::Path<(TeamId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + join_team(req, info, pool, redis, session_queue).await +} + pub async fn join_team( req: HttpRequest, info: web::Path<(TeamId,)>, @@ -418,7 +450,7 @@ fn default_ordering() -> i64 { 0 } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)] pub struct NewTeamMember { pub user_id: UserId, #[serde(default = "default_role")] @@ -434,6 +466,19 @@ pub struct NewTeamMember { pub ordering: i64, } +#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))] +#[post("/team/{id}/members")] +async fn add_team_member_route( + req: HttpRequest, + info: web::Path<(TeamId,)>, + pool: web::Data, + new_member: web::Json, + redis: web::Data, + session_queue: web::Data, +) -> Result { + add_team_member(req, info, pool, new_member, redis, session_queue).await +} + pub async fn add_team_member( req: HttpRequest, info: web::Path<(TeamId,)>, @@ -679,7 +724,7 @@ pub async fn add_team_member( Ok(HttpResponse::NoContent().body("")) } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)] pub struct EditTeamMember { pub permissions: Option, pub organization_permissions: Option, @@ -688,6 +733,19 @@ pub struct EditTeamMember { pub ordering: Option, } +#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))] +#[patch("/team/{id}/members/{user_id}")] +async fn edit_team_member_route( + req: HttpRequest, + info: web::Path<(TeamId, String)>, + pool: web::Data, + edit_member: web::Json, + redis: web::Data, + session_queue: web::Data, +) -> Result { + edit_team_member(req, info, pool, edit_member, redis, session_queue).await +} + pub async fn edit_team_member( req: HttpRequest, info: web::Path<(TeamId, String)>, @@ -883,11 +941,24 @@ pub async fn edit_team_member( Ok(HttpResponse::NoContent().body("")) } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct TransferOwnership { pub user_id: UserId, } +#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))] +#[patch("/team/{id}/owner")] +async fn transfer_ownership_route( + req: HttpRequest, + info: web::Path<(TeamId,)>, + pool: web::Data, + new_owner: web::Json, + redis: web::Data, + session_queue: web::Data, +) -> Result { + transfer_ownership(req, info, pool, new_owner, redis, session_queue).await +} + pub async fn transfer_ownership( req: HttpRequest, info: web::Path<(TeamId,)>, @@ -1073,6 +1144,18 @@ pub async fn transfer_ownership( Ok(HttpResponse::NoContent().body("")) } +#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))] +#[delete("/team/{id}/members/{user_id}")] +async fn remove_team_member_route( + req: HttpRequest, + info: web::Path<(TeamId, UserId)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + remove_team_member(req, info, pool, redis, session_queue).await +} + pub async fn remove_team_member( req: HttpRequest, info: web::Path<(TeamId, UserId)>, @@ -1226,3 +1309,35 @@ pub async fn remove_team_member( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + team_members_get_project, + team_members_get_organization, + team_members_get_route, + teams_get_route, + join_team_route, + add_team_member_route, + edit_team_member_route, + transfer_ownership_route, + remove_team_member_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(team_members_get_project,))] +pub(crate) struct ProjectRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + team_members_get_organization, + team_members_get_route, + teams_get_route, + join_team_route, + add_team_member_route, + edit_team_member_route, + transfer_ownership_route, + remove_team_member_route, +))] +pub(crate) struct RootRoutesDoc; diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 2f66b06b29..724c84e123 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -16,20 +16,15 @@ use crate::models::threads::{MessageBody, Thread, ThreadType}; use crate::models::users::User; use crate::queue::session::AuthQueue; use crate::routes::ApiError; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use futures::TryStreamExt; use serde::Deserialize; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/thread") - .route("/{id}", web::get().to(thread_get)) - .route("/{id}", web::post().to(thread_send_message)), - ); - cfg.service( - web::scope("/message").route("/{id}", web::delete().to(message_delete)), - ); - cfg.route("/threads", web::get().to(threads_get)); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(thread_get_route) + .service(thread_send_message_route) + .service(message_delete_route) + .service(threads_get_route); } pub async fn is_authorized_thread( @@ -267,6 +262,18 @@ pub async fn filter_authorized_threads( Ok(final_threads) } +#[utoipa::path(tag = "threads", responses((status = OK, body = Thread)))] +#[get("/thread/{id}")] +async fn thread_get_route( + req: HttpRequest, + info: web::Path<(ThreadId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + thread_get(req, info, pool, redis, session_queue).await +} + pub async fn thread_get( req: HttpRequest, info: web::Path<(ThreadId,)>, @@ -324,6 +331,18 @@ pub struct ThreadIds { pub ids: String, } +#[utoipa::path(tag = "threads", responses((status = OK, body = Vec)))] +#[get("/threads")] +async fn threads_get_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + threads_get(req, ids, pool, redis, session_queue).await +} + pub async fn threads_get( req: HttpRequest, web::Query(ids): web::Query, @@ -356,11 +375,25 @@ pub async fn threads_get( Ok(HttpResponse::Ok().json(threads)) } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct NewThreadMessage { pub body: MessageBody, } +#[utoipa::path(tag = "threads", responses((status = NO_CONTENT)))] +#[post("/thread/{id}")] +async fn thread_send_message_route( + req: HttpRequest, + info: web::Path<(ThreadId,)>, + pool: web::Data, + new_message: web::Json, + redis: web::Data, + session_queue: web::Data, +) -> Result { + thread_send_message(req, info, pool, new_message, redis, session_queue) + .await +} + pub async fn thread_send_message( req: HttpRequest, info: web::Path<(ThreadId,)>, @@ -591,6 +624,19 @@ pub async fn thread_send_message_internal( } } +#[utoipa::path(tag = "threads", responses((status = NO_CONTENT)))] +#[delete("/message/{id}")] +async fn message_delete_route( + req: HttpRequest, + info: web::Path<(ThreadMessageId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + file_host: web::Data, +) -> Result { + message_delete(req, info, pool, redis, session_queue, file_host).await +} + pub async fn message_delete( req: HttpRequest, info: web::Path<(ThreadMessageId,)>, @@ -658,3 +704,13 @@ pub async fn message_delete( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + thread_get_route, + threads_get_route, + thread_send_message_route, + message_delete_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index 5da14c7173..305c82aaea 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -31,33 +31,29 @@ use crate::{ validate::validation_errors_to_string, }, }; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use ariadne::ids::UserId; use serde::{Deserialize, Serialize}; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route("/user", web::get().to(user_auth_get)); - cfg.route("/users", web::get().to(users_get)); - cfg.route("/users/search", web::get().to(users_search)); - cfg.route("/user_email", web::get().to(admin_user_email)); - - cfg.service( - web::scope("/user") - .route("/{user_id}/all-projects", web::get().to(all_projects)) - .route("/{user_id}/projects", web::get().to(projects_list)) - .route("/{id}/notes", web::patch().to(user_notes_edit)) - .route("/{id}", web::get().to(user_get)) - .route("/{user_id}/collections", web::get().to(collections_list)) - .route("/{user_id}/organizations", web::get().to(orgs_list)) - .route("/{id}", web::patch().to(user_edit)) - .route("/{id}/icon", web::patch().to(user_icon_edit)) - .route("/{id}/icon", web::delete().to(user_icon_delete)) - .route("/{id}", web::delete().to(user_delete)) - .route("/{id}/follows", web::get().to(user_follows)) - .route("/{id}/notifications", web::get().to(user_notifications)) - .route("/{id}/oauth_apps", web::get().to(get_user_clients)), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(user_auth_get_route) + .service(users_get_route) + .service(users_search) + .service(admin_user_email) + .service(all_projects) + .service(projects_list_route) + .service(user_notes_edit) + .service(user_get_route) + .service(collections_list) + .service(orgs_list) + .service(user_edit_route) + .service(user_icon_edit_route) + .service(user_icon_delete_route) + .service(user_delete_route) + .service(user_follows_route) + .service(user_notifications_route) + .service(get_user_clients); } #[derive(Serialize)] @@ -71,6 +67,8 @@ pub struct UserEmailQuery { pub email: String, } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user/{user_id}/all-projects")] pub async fn all_projects( req: HttpRequest, info: web::Path<(String,)>, @@ -200,6 +198,8 @@ pub async fn all_projects( })) } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user_email")] pub async fn admin_user_email( req: HttpRequest, pool: web::Data, @@ -254,6 +254,18 @@ pub async fn admin_user_email( } } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user/{user_id}/projects")] +async fn projects_list_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + projects_list(req, info, pool, redis, session_queue).await +} + pub async fn projects_list( req: HttpRequest, info: web::Path<(String,)>, @@ -291,6 +303,17 @@ pub async fn projects_list( } } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user")] +async fn user_auth_get_route( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + user_auth_get(req, pool, redis, session_queue).await +} + pub async fn user_auth_get( req: HttpRequest, pool: web::Data, @@ -333,6 +356,8 @@ pub struct UserSearchQuery { pub query: String, } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/users/search")] pub async fn users_search( web::Query(query): web::Query, pool: web::Data, @@ -348,6 +373,18 @@ pub async fn users_search( Ok(web::Json(users)) } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/users")] +async fn users_get_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + users_get(req, ids, pool, redis, session_queue).await +} + pub async fn users_get( req: HttpRequest, web::Query(ids): web::Query, @@ -396,6 +433,18 @@ pub async fn users_get( Ok(HttpResponse::Ok().json(users)) } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user/{id}")] +async fn user_get_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + user_get(req, info, pool, redis, session_queue).await +} + pub async fn user_get( req: HttpRequest, info: web::Path<(String,)>, @@ -439,6 +488,8 @@ pub async fn user_get( } } +#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))] +#[patch("/user/{id}/notes")] pub async fn user_notes_edit( req: HttpRequest, info: web::Path<(String,)>, @@ -506,6 +557,8 @@ pub async fn user_notes_edit( Ok(HttpResponse::NoContent().finish()) } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user/{user_id}/collections")] pub async fn collections_list( req: HttpRequest, info: web::Path<(String,)>, @@ -545,6 +598,8 @@ pub async fn collections_list( } } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user/{user_id}/organizations")] pub async fn orgs_list( req: HttpRequest, info: web::Path<(String,)>, @@ -640,7 +695,7 @@ pub async fn orgs_list( } } -#[derive(Serialize, Deserialize, Validate)] +#[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)] pub struct EditUser { #[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))] pub username: Option, @@ -658,6 +713,19 @@ pub struct EditUser { pub allow_friend_requests: Option, } +#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))] +#[patch("/user/{id}")] +async fn user_edit_route( + req: HttpRequest, + info: web::Path<(String,)>, + new_user: web::Json, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + user_edit(req, info, new_user, pool, redis, session_queue).await +} + pub async fn user_edit( req: HttpRequest, info: web::Path<(String,)>, @@ -827,6 +895,31 @@ pub struct Extension { } #[allow(clippy::too_many_arguments)] +#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))] +#[patch("/user/{id}/icon")] +async fn user_icon_edit_route( + ext: web::Query, + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + file_host: web::Data, + payload: web::Payload, + session_queue: web::Data, +) -> Result { + user_icon_edit( + ext, + req, + info, + pool, + redis, + file_host, + payload, + session_queue, + ) + .await +} + pub async fn user_icon_edit( web::Query(ext): web::Query, req: HttpRequest, @@ -903,6 +996,19 @@ pub async fn user_icon_edit( } } +#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))] +#[delete("/user/{id}/icon")] +async fn user_icon_delete_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + file_host: web::Data, + session_queue: web::Data, +) -> Result { + user_icon_delete(req, info, pool, redis, file_host, session_queue).await +} + pub async fn user_icon_delete( req: HttpRequest, info: web::Path<(String,)>, @@ -957,6 +1063,18 @@ pub async fn user_icon_delete( } } +#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))] +#[delete("/user/{id}")] +async fn user_delete_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result<(), ApiError> { + user_delete(req, info, pool, redis, session_queue).await +} + pub async fn user_delete( req: HttpRequest, info: web::Path<(String,)>, @@ -1005,6 +1123,18 @@ pub async fn user_delete( } } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user/{id}/follows")] +async fn user_follows_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + user_follows(req, info, pool, redis, session_queue).await +} + pub async fn user_follows( req: HttpRequest, info: web::Path<(String,)>, @@ -1047,6 +1177,18 @@ pub async fn user_follows( } } +#[utoipa::path(tag = "users", responses((status = OK)))] +#[get("/user/{id}/notifications")] +async fn user_notifications_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + user_notifications(req, info, pool, redis, session_queue).await +} + pub async fn user_notifications( req: HttpRequest, info: web::Path<(String,)>, @@ -1087,3 +1229,25 @@ pub async fn user_notifications( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + all_projects, + admin_user_email, + projects_list_route, + user_auth_get_route, + users_search, + users_get_route, + user_get_route, + user_notes_edit, + collections_list, + orgs_list, + user_edit_route, + user_icon_edit_route, + user_icon_delete_route, + user_delete_route, + user_follows_route, + user_notifications_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 061a118d4a..7a46a0f95c 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -34,7 +34,7 @@ use crate::util::validate::validation_errors_to_string; use crate::validate::{ValidationResult, validate_file}; use actix_multipart::{Field, Multipart}; use actix_web::web::Data; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, post, web}; use chrono::Utc; use futures::stream::StreamExt; use hex::ToHex; @@ -109,7 +109,6 @@ struct InitialFileData { #[utoipa::path( tag = "versions", post, - path = "/v3/version", request_body( content(("multipart/form-data")), description = "Multipart payload containing `data` and uploaded files" @@ -124,6 +123,32 @@ struct InitialFileData { ), security(("bearer_auth" = ["VERSION_CREATE"])) )] +#[post("/version")] +pub async fn version_create_route( + req: HttpRequest, + payload: Multipart, + client: Data, + redis: Data, + file_host: Data, + session_queue: Data, + moderation_queue: web::Data, + http: web::Data, + search_state: Data, +) -> Result { + version_create( + req, + payload, + client, + redis, + file_host, + session_queue, + moderation_queue, + http, + search_state, + ) + .await +} + pub async fn version_create( req: HttpRequest, mut payload: Multipart, @@ -569,7 +594,6 @@ async fn version_create_inner( #[utoipa::path( tag = "versions", post, - path = "/v3/version/{version_id}/file", params(("version_id" = VersionId, Path, description = "The ID of the version")), request_body( content(("multipart/form-data")), @@ -588,6 +612,32 @@ async fn version_create_inner( ), security(("bearer_auth" = ["VERSION_WRITE"])) )] +#[post("/version/{version_id}/file")] +pub async fn upload_file_to_version_route( + req: HttpRequest, + url_data: web::Path<(VersionId,)>, + payload: Multipart, + client: Data, + redis: Data, + file_host: Data, + session_queue: web::Data, + http: web::Data, + search_state: Data, +) -> Result { + upload_file_to_version( + req, + url_data, + payload, + client, + redis, + file_host, + session_queue, + http, + search_state, + ) + .await +} + pub async fn upload_file_to_version( req: HttpRequest, url_data: web::Path<(VersionId,)>, @@ -1144,3 +1194,8 @@ pub fn try_create_version_fields( } Ok(version_fields) } + +#[derive(utoipa::OpenApi)] +#[openapi(paths(version_create_route, upload_file_to_version_route,))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index 43bd3f73c3..3651a57031 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -11,41 +11,29 @@ use crate::models::teams::ProjectPermissions; use crate::queue::session::AuthQueue; use crate::routes::internal::delphi; use crate::{database, models}; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use dashmap::DashMap; use futures::TryStreamExt; use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/version_file") - .route("/{version_id}", web::get().to(get_version_from_hash)) - .route("/{version_id}/update", web::post().to(get_update_from_hash)) - .route("/project", web::post().to(get_projects_from_hashes)) - .route("/{version_id}", web::delete().to(delete_file)) - .route("/{version_id}/download", web::get().to(download_version)), - ); - cfg.service( - web::scope("/version_files") - // DEPRECATED - use `update_many` instead - // see `fn update_files` comment - .route("/update", web::post().to(update_files)) - .route("/update_many", web::post().to(update_files_many)) - .route( - "/update_individual", - web::post().to(update_individual_files), - ) - .route("", web::post().to(get_versions_from_hashes)), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(get_version_from_hash_route) + .service(get_update_from_hash_route) + .service(get_projects_from_hashes_route) + .service(delete_file_route) + .service(download_version_route) + .service(update_files_route) + .service(update_files_many_route) + .service(update_individual_files_route) + .service(get_versions_from_hashes_route); } /// Get version metadata by file hash. #[utoipa::path( tag = "version files", get, - path = "/v3/version_file/{version_id}", operation_id = "v3VersionFromHash", params( ( @@ -72,6 +60,19 @@ pub fn config(cfg: &mut web::ServiceConfig) { ) ) )] +#[get("/version_file/{version_id}")] +async fn get_version_from_hash_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + hash_query: web::Query, + session_queue: web::Data, +) -> Result { + get_version_from_hash(req, info, pool, redis, hash_query, session_queue) + .await +} + pub async fn get_version_from_hash( req: HttpRequest, info: web::Path<(String,)>, @@ -161,7 +162,6 @@ pub struct UpdateData { #[utoipa::path( tag = "version files", post, - path = "/v3/version_file/{version_id}/update", operation_id = "v3UpdateFromHash", params( ( @@ -189,6 +189,28 @@ pub struct UpdateData { ) ) )] +#[post("/version_file/{version_id}/update")] +async fn get_update_from_hash_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + hash_query: web::Query, + update_data: web::Json, + session_queue: web::Data, +) -> Result { + get_update_from_hash( + req, + info, + pool, + redis, + hash_query, + update_data, + session_queue, + ) + .await +} + pub async fn get_update_from_hash( req: HttpRequest, info: web::Path<(String,)>, @@ -284,7 +306,6 @@ pub struct FileHashes { #[utoipa::path( tag = "version files", post, - path = "/v3/version_files", operation_id = "v3VersionsFromHashes", request_body = FileHashes, responses( @@ -295,6 +316,17 @@ pub struct FileHashes { ) ) )] +#[post("/version_files")] +async fn get_versions_from_hashes_route( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + file_data: web::Json, + session_queue: web::Data, +) -> Result { + get_versions_from_hashes(req, pool, redis, file_data, session_queue).await +} + pub async fn get_versions_from_hashes( req: HttpRequest, pool: web::Data, @@ -354,7 +386,6 @@ pub async fn get_versions_from_hashes( #[utoipa::path( tag = "version files", post, - path = "/v3/version_file/project", operation_id = "v3ProjectsFromHashes", request_body = FileHashes, responses( @@ -365,6 +396,17 @@ pub async fn get_versions_from_hashes( ) ) )] +#[post("/version_file/project")] +async fn get_projects_from_hashes_route( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + file_data: web::Json, + session_queue: web::Data, +) -> Result { + get_projects_from_hashes(req, pool, redis, file_data, session_queue).await +} + pub async fn get_projects_from_hashes( req: HttpRequest, pool: web::Data, @@ -436,13 +478,22 @@ pub struct ManyUpdateData { #[utoipa::path( tag = "version files", post, - path = "/v3/version_files/update_many", operation_id = "v3UpdateFilesMany", request_body = ManyUpdateData, responses( (status = 200, description = "Expected response to a valid request", body = HashMap>) ) )] +#[post("/version_files/update_many")] +async fn update_files_many_route( + pool: web::Data, + redis: web::Data, + update_data: web::Json, +) -> Result>>, ApiError> +{ + update_files_many(pool, redis, update_data).await +} + pub async fn update_files_many( pool: web::Data, redis: web::Data, @@ -479,13 +530,21 @@ pub async fn update_files_many( #[utoipa::path( tag = "version files", post, - path = "/v3/version_files/update", operation_id = "v3UpdateFiles", request_body = ManyUpdateData, responses( (status = 200, description = "Expected response to a valid request", body = HashMap) ) )] +#[post("/version_files/update")] +async fn update_files_route( + pool: web::Data, + redis: web::Data, + update_data: web::Json, +) -> Result>, ApiError> { + update_files(pool, redis, update_data).await +} + pub async fn update_files( pool: web::Data, redis: web::Data, @@ -604,13 +663,23 @@ pub struct ManyFileUpdateData { #[utoipa::path( tag = "version files", post, - path = "/v3/version_files/update_individual", operation_id = "v3UpdateIndividualFiles", request_body = ManyFileUpdateData, responses( (status = 200, description = "Expected response to a valid request", body = HashMap) ) )] +#[post("/version_files/update_individual")] +async fn update_individual_files_route( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + update_data: web::Json, + session_queue: web::Data, +) -> Result { + update_individual_files(req, pool, redis, update_data, session_queue).await +} + pub async fn update_individual_files( req: HttpRequest, pool: web::Data, @@ -736,7 +805,6 @@ pub async fn update_individual_files( #[utoipa::path( tag = "version files", delete, - path = "/v3/version_file/{version_id}", operation_id = "v3DeleteFileFromHash", params( ( @@ -767,6 +835,18 @@ pub async fn update_individual_files( ) ) )] +#[delete("/version_file/{version_id}")] +async fn delete_file_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + hash_query: web::Query, + session_queue: web::Data, +) -> Result { + delete_file(req, info, pool, redis, hash_query, session_queue).await +} + pub async fn delete_file( req: HttpRequest, info: web::Path<(String,)>, @@ -914,7 +994,6 @@ pub struct DownloadRedirect { #[utoipa::path( tag = "version files", get, - path = "/v3/version_file/{version_id}/download", operation_id = "v3DownloadVersionFromHash", params( ( @@ -941,6 +1020,18 @@ pub struct DownloadRedirect { ) ) )] +#[get("/version_file/{version_id}/download")] +async fn download_version_route( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + hash_query: web::Query, + session_queue: web::Data, +) -> Result { + download_version(req, info, pool, redis, hash_query, session_queue).await +} + pub async fn download_version( req: HttpRequest, info: web::Path<(String,)>, @@ -995,3 +1086,18 @@ pub async fn download_version( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + get_version_from_hash_route, + get_update_from_hash_route, + get_versions_from_hashes_route, + get_projects_from_hashes_route, + update_files_many_route, + update_files_route, + update_individual_files_route, + delete_file_route, + download_version_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 329430b26f..91d650ceeb 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -31,29 +31,19 @@ use crate::search::{SearchBackend, SearchState}; use crate::util::error::Context; use crate::util::img; use crate::util::validate::validation_errors_to_string; -use actix_web::{HttpRequest, HttpResponse, get, web}; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use ariadne::ids::base62_impl::parse_base62; use itertools::Itertools; use serde::{Deserialize, Serialize}; use validator::Validate; -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.route( - "/version", - web::post().to(super::version_creation::version_create), - ); - cfg.route("/versions", web::get().to(versions_get)); - - cfg.service( - web::scope("/version") - .route("/{id}", web::get().to(version_get)) - .route("/{id}", web::patch().to(version_edit)) - .route("/{id}", web::delete().to(version_delete)) - .route( - "/{version_id}/file", - web::post().to(super::version_creation::upload_file_to_version), - ), - ); +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(super::version_creation::version_create_route) + .service(versions_get_route) + .service(version_get_route) + .service(version_edit_route) + .service(version_delete_route) + .service(super::version_creation::upload_file_to_version_route); } // Given a project ID/slug and a version slug @@ -176,7 +166,6 @@ fn default_true() -> bool { #[utoipa::path( tag = "versions", get, - path = "/v3/versions", params( ("ids" = String, Query, description = "The JSON array of version IDs"), ( @@ -187,6 +176,18 @@ fn default_true() -> bool { ), responses((status = 200, description = "Expected response to a valid request", body = Vec)) )] +#[get("/versions")] +async fn versions_get_route( + req: HttpRequest, + ids: web::Query, + pool: web::Data, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + versions_get(req, ids, pool, ro_pool, redis, session_queue).await +} + pub async fn versions_get( req: HttpRequest, web::Query(ids): web::Query, @@ -240,7 +241,6 @@ pub async fn versions_get( #[utoipa::path( tag = "versions", get, - path = "/v3/version/{id}", params(("id" = VersionId, Path, description = "The ID of the version")), responses( (status = 200, description = "Expected response to a valid request", body = models::projects::Version), @@ -250,6 +250,18 @@ pub async fn versions_get( ) ) )] +#[get("/version/{id}")] +async fn version_get_route( + req: HttpRequest, + info: web::Path<(models::ids::VersionId,)>, + pool: web::Data, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result, ApiError> { + version_get(req, info, pool, ro_pool, redis, session_queue).await +} + pub async fn version_get( req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, @@ -373,7 +385,6 @@ pub struct EditVersionFileType { #[utoipa::path( tag = "versions", patch, - path = "/v3/version/{id}", params(("id" = VersionId, Path, description = "The ID of the version")), responses( (status = NO_CONTENT, description = "Expected response to a valid request"), @@ -388,6 +399,28 @@ pub struct EditVersionFileType { ), security(("bearer_auth" = ["VERSION_WRITE"])) )] +#[patch("/version/{id}")] +async fn version_edit_route( + req: HttpRequest, + info: web::Path<(VersionId,)>, + pool: web::Data, + redis: web::Data, + new_version: web::Json, + session_queue: web::Data, + search_state: web::Data, +) -> Result { + version_edit( + req, + info, + pool, + redis, + new_version, + session_queue, + search_state, + ) + .await +} + pub async fn version_edit( req: HttpRequest, info: web::Path<(VersionId,)>, @@ -1071,7 +1104,6 @@ pub async fn version_list_internal( #[utoipa::path( tag = "versions", delete, - path = "/v3/version/{id}", params(("id" = VersionId, Path, description = "The ID of the version")), responses( (status = NO_CONTENT, description = "Expected response to a valid request"), @@ -1086,6 +1118,28 @@ pub async fn version_list_internal( ), security(("bearer_auth" = ["VERSION_DELETE"])) )] +#[delete("/version/{id}")] +async fn version_delete_route( + req: HttpRequest, + info: web::Path<(VersionId,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + search_backend: web::Data, + search_state: web::Data, +) -> Result { + version_delete( + req, + info, + pool, + redis, + session_queue, + search_backend, + search_state, + ) + .await +} + pub async fn version_delete( req: HttpRequest, info: web::Path<(VersionId,)>, @@ -1211,3 +1265,28 @@ pub async fn version_delete( Err(ApiError::NotFound) } } + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + version_project_get, + versions_get_route, + version_get_route, + version_edit_route, + version_list, + version_delete_route, +))] +#[allow(dead_code)] +pub(crate) struct RouteDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths(version_project_get, version_list,))] +pub(crate) struct ProjectRoutesDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(paths( + versions_get_route, + version_get_route, + version_edit_route, + version_delete_route, +))] +pub(crate) struct RootRoutesDoc; diff --git a/apps/labrinth/src/test/api_v2/mod.rs b/apps/labrinth/src/test/api_v2/mod.rs index e7085910d2..3f00b17f1d 100644 --- a/apps/labrinth/src/test/api_v2/mod.rs +++ b/apps/labrinth/src/test/api_v2/mod.rs @@ -7,7 +7,6 @@ use crate::env::ENV; use actix_web::{App, dev::ServiceResponse, test}; use async_trait::async_trait; use std::rc::Rc; -use utoipa_actix_web::AppExt; pub mod project; pub mod request_data; @@ -26,13 +25,11 @@ impl ApiBuildable for ApiV2 { async fn build(labrinth_config: LabrinthConfig) -> Self { let app = App::new() .configure(|cfg| { - crate::app_base_config(cfg, labrinth_config.clone()) + crate::app_data_config(cfg, labrinth_config.clone()) }) - .into_utoipa_app() .configure(|cfg| { - crate::utoipa_app_config(cfg, labrinth_config.clone()) + crate::app_routes_config(cfg, labrinth_config.clone()) }) - .into_app() .configure(crate::app_fallback_config); let test_app: Rc = Rc::new(test::init_service(app).await); diff --git a/apps/labrinth/src/test/api_v3/mod.rs b/apps/labrinth/src/test/api_v3/mod.rs index e7587f88c7..dfbb8b58e3 100644 --- a/apps/labrinth/src/test/api_v3/mod.rs +++ b/apps/labrinth/src/test/api_v3/mod.rs @@ -7,7 +7,6 @@ use crate::env::ENV; use actix_web::{App, dev::ServiceResponse, test}; use async_trait::async_trait; use std::rc::Rc; -use utoipa_actix_web::AppExt; pub mod collections; pub mod limits; @@ -31,13 +30,11 @@ impl ApiBuildable for ApiV3 { async fn build(labrinth_config: LabrinthConfig) -> Self { let app = App::new() .configure(|cfg| { - crate::app_base_config(cfg, labrinth_config.clone()) + crate::app_data_config(cfg, labrinth_config.clone()) }) - .into_utoipa_app() .configure(|cfg| { - crate::utoipa_app_config(cfg, labrinth_config.clone()) + crate::app_routes_config(cfg, labrinth_config.clone()) }) - .into_app() .configure(crate::app_fallback_config); let test_app: Rc = Rc::new(test::init_service(app).await); diff --git a/apps/labrinth/tests/openapi_route_scan.rs b/apps/labrinth/tests/openapi_route_scan.rs new file mode 100644 index 0000000000..fbacb452d8 --- /dev/null +++ b/apps/labrinth/tests/openapi_route_scan.rs @@ -0,0 +1,117 @@ +use std::collections::BTreeSet; + +use utoipa::OpenApi; + +#[derive(OpenApi)] +struct DocsV3; + +#[derive(OpenApi)] +struct DocsInternal; + +fn collect_v3_paths() -> BTreeSet { + let docs = DocsV3::openapi() + .merge_from(labrinth::routes::PublicApiDoc::openapi()) + .merge_from(labrinth::routes::v3::ApiDoc::openapi()); + + docs.paths.paths.keys().cloned().collect() +} + +fn collect_internal_paths() -> BTreeSet { + let docs = DocsInternal::openapi() + .merge_from(labrinth::routes::internal::ApiDoc::openapi()); + + docs.paths.paths.keys().cloned().collect() +} + +fn assert_paths(paths: &BTreeSet, expected: &[&str]) { + let missing = expected + .iter() + .filter(|path| !paths.contains(**path)) + .copied() + .collect::>(); + + assert!( + missing.is_empty(), + "missing OpenAPI paths: {missing:#?}\nregistered paths:\n{}", + paths.iter().cloned().collect::>().join("\n") + ); +} + +#[test] +fn v3_openapi_includes_configured_routes() { + let paths = collect_v3_paths(); + + assert_paths( + &paths, + &[ + "/analytics/minecraft-server-play", + "/analytics/playtime", + "/analytics/view", + "/maven/maven/modrinth/{id}/maven-metadata.xml", + "/maven/maven/modrinth/{id}/{versionnum}/{file}", + "/updates/{id}/forge_updates.json", + "/v3/analytics", + "/v3/analytics/facets", + "/v3/collection", + "/v3/collection/{id}", + "/v3/content/resolve", + "/v3/games", + "/v3/image", + "/v3/limits/collections", + "/v3/notifications", + "/v3/oauth/app", + "/v3/organization", + "/v3/organization/{id}", + "/v3/project", + "/v3/project/{id}", + "/v3/project/{id}/gallery", + "/v3/project/{project_id}/dependencies", + "/v3/report", + "/v3/reports", + "/v3/shared-instance", + "/v3/shared-instance/{id}/version", + "/v3/statistics", + "/v3/tag/category", + "/v3/team/{id}/members", + "/v3/thread/{id}", + "/v3/user/{id}", + "/v3/users", + "/v3/version", + "/v3/version/{id}", + "/v3/version/{version_id}/file", + "/v3/version_file/{version_id}", + "/v3/version_files", + ], + ); +} + +#[test] +fn internal_openapi_includes_collapsed_route_groups() { + let paths = collect_internal_paths(); + + assert_paths( + &paths, + &[ + "/_internal/admin/_count-download", + "/_internal/affiliate", + "/_internal/attribution/assign", + "/_internal/auth/init", + "/_internal/billing/charge/{id}/refund", + "/_internal/delphi/ingest", + "/_internal/external_notifications", + "/_internal/gdpr/export", + "/_internal/globals", + "/_internal/gotenberg/success", + "/_internal/medal/verify", + "/_internal/moderation/project/{id}", + "/_internal/moderation/tech-review/report/{id}", + "/_internal/mural/bank-details", + "/_internal/pat", + "/_internal/search-management/tasks", + "/_internal/server-ping/minecraft-java", + "/_internal/session/list", + "/_internal/launcher_socket", + "/v3/analytics-event", + ], + ); +}