feat: better api docs (#6586)

* feat: docs

* fix tombi

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

* response schemas

* fix: restore labrinth docs routes

* Fix path parameter docs in routes

* remove utoipa-actix-web

* consistency

* improve version intros

* improve formatting, examples

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

* more utoipa param fixes

* request body docs

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

* fixes

* chore: tweak sources titles

* fix

* fix test

* improve examples

* increase compiler spawned thread stack size

* remove unused tests & script

* test

* bro what

* fix

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
This commit is contained in:
François-Xavier Talbot
2026-07-04 14:19:21 +00:00
committed by GitHub
co-authored by aecsocket
parent b26d048a63
commit 4a6fa9fc3d
103 changed files with 4546 additions and 1387 deletions
+1
View File
@@ -79,6 +79,7 @@ jobs:
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
RUST_MIN_STACK: '16777216'
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Generated
+147 -64
View File
@@ -59,9 +59,9 @@ dependencies = [
[[package]]
name = "actix-http"
version = "3.11.2"
version = "3.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2"
dependencies = [
"actix-codec",
"actix-rt",
@@ -75,7 +75,7 @@ dependencies = [
"derive_more 2.1.1",
"encoding_rs",
"flate2",
"foldhash 0.1.5",
"foldhash 0.2.0",
"futures-core",
"h2 0.3.27",
"http 0.2.12",
@@ -87,8 +87,8 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rand 0.9.2",
"sha1",
"rand 0.10.1",
"sha1 0.11.0",
"smallvec",
"tokio",
"tokio-util",
@@ -146,9 +146,9 @@ dependencies = [
[[package]]
name = "actix-router"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7"
dependencies = [
"bytestring",
"cfg-if",
@@ -209,9 +209,9 @@ dependencies = [
[[package]]
name = "actix-web"
version = "4.11.0"
version = "4.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea"
checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4"
dependencies = [
"actix-codec",
"actix-http",
@@ -228,7 +228,7 @@ dependencies = [
"cookie 0.16.2",
"derive_more 2.1.1",
"encoding_rs",
"foldhash 0.1.5",
"foldhash 0.2.0",
"futures-core",
"futures-util",
"impl-more",
@@ -244,7 +244,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"smallvec",
"socket2 0.5.10",
"socket2 0.6.1",
"time",
"tracing",
"url",
@@ -1127,7 +1127,7 @@ dependencies = [
"http-body-util",
"md-5",
"pin-project-lite",
"sha1",
"sha1 0.10.6",
"sha2",
"tracing",
]
@@ -1499,7 +1499,7 @@ version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
"digest 0.10.7",
]
[[package]]
@@ -1511,6 +1511,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "block2"
version = "0.5.1"
@@ -2224,6 +2233,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "const-random"
version = "0.1.18"
@@ -2424,7 +2439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d"
dependencies = [
"crc",
"digest",
"digest 0.10.7",
"rustversion",
"spin 0.10.0",
]
@@ -2515,6 +2530,15 @@ dependencies = [
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "cssparser"
version = "0.29.6"
@@ -2791,7 +2815,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"const-oid 0.9.6",
"pem-rfc7468",
"zeroize",
]
@@ -2904,12 +2928,23 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"block-buffer 0.10.4",
"const-oid 0.9.6",
"crypto-common 0.1.6",
"subtle",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.1",
"const-oid 0.10.2",
"crypto-common 0.2.2",
]
[[package]]
name = "directories"
version = "6.0.0"
@@ -3127,7 +3162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest",
"digest 0.10.7",
"elliptic-curve",
"rfc6979",
"signature",
@@ -3151,7 +3186,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"digest 0.10.7",
"ff",
"generic-array",
"group",
@@ -4402,7 +4437,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
"digest 0.10.7",
]
[[package]]
@@ -4532,6 +4567,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hybrid-array"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
dependencies = [
"typenum",
]
[[package]]
name = "hyper"
version = "0.14.32"
@@ -4893,9 +4937,9 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
[[package]]
name = "impl-more"
version = "0.1.9"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870"
[[package]]
name = "indenter"
@@ -5390,11 +5434,12 @@ dependencies = [
"rust_iso3166",
"rustls 0.23.32",
"rusty-money",
"scalar_api_reference",
"sentry",
"serde",
"serde_json",
"serde_with",
"sha1",
"sha1 0.10.6",
"sha2",
"spdx",
"sqlx",
@@ -5411,8 +5456,6 @@ dependencies = [
"url",
"urlencoding",
"utoipa",
"utoipa-actix-web",
"utoipa-scalar",
"uuid 1.23.3",
"validator",
"webauthn-rs",
@@ -5822,7 +5865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
"digest 0.10.7",
]
[[package]]
@@ -8494,8 +8537,8 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
dependencies = [
"const-oid",
"digest",
"const-oid 0.9.6",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-traits",
@@ -8508,6 +8551,40 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust-embed"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.106",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@@ -8827,6 +8904,18 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scalar_api_reference"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d67c85edd56c41f364603eff688a330f01dee99bdfc84c005d3bc3d0721d6b2"
dependencies = [
"actix-web",
"rust-embed",
"serde",
"serde_json",
]
[[package]]
name = "scc"
version = "2.4.0"
@@ -9252,15 +9341,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -9455,7 +9544,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
"digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.3",
]
[[package]]
@@ -9472,7 +9572,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
"digest 0.10.7",
]
[[package]]
@@ -9505,7 +9605,7 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"digest 0.10.7",
"rand_core 0.6.4",
]
@@ -9782,7 +9882,7 @@ dependencies = [
"bytes",
"chrono",
"crc",
"digest",
"digest 0.10.7",
"dotenvy",
"either",
"futures-channel",
@@ -9803,7 +9903,7 @@ dependencies = [
"rsa",
"rust_decimal",
"serde",
"sha1",
"sha1 0.10.6",
"sha2",
"smallvec",
"sqlx-core",
@@ -11290,7 +11390,7 @@ dependencies = [
"constant_time_eq",
"hmac",
"rand 0.9.2",
"sha1",
"sha1 0.10.6",
"sha2",
]
@@ -11505,7 +11605,7 @@ dependencies = [
"rand 0.9.2",
"rustls 0.23.32",
"rustls-pki-types",
"sha1",
"sha1 0.10.6",
"thiserror 2.0.17",
"utf-8",
]
@@ -11524,9 +11624,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.19.0"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uds_windows"
@@ -11757,17 +11857,6 @@ dependencies = [
"utoipa-gen",
]
[[package]]
name = "utoipa-actix-web"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f"
dependencies = [
"actix-service",
"actix-web",
"utoipa",
]
[[package]]
name = "utoipa-gen"
version = "5.4.0"
@@ -11782,18 +11871,6 @@ dependencies = [
"uuid 1.23.3",
]
[[package]]
name = "utoipa-scalar"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
dependencies = [
"actix-web",
"serde",
"serde_json",
"utoipa",
]
[[package]]
name = "uuid"
version = "0.8.2"
@@ -13450,6 +13527,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.2"
+1 -2
View File
@@ -158,6 +158,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
] }
rustls = "0.23.32"
rusty-money = "0.4.1"
scalar_api_reference = { version = "0.2.2", default-features = false }
secrecy = "0.10.3"
sentry = { version = "0.45.0", default-features = false, features = [
"backtrace",
@@ -217,8 +218,6 @@ typed-path = "0.12.0"
url = "2.5.7"
urlencoding = "2.1.3"
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
utoipa-actix-web = { version = "0.1.2" }
utoipa-scalar = { version = "0.3.0", default-features = false }
uuid = "1.18.1"
validator = "0.20.0"
webauthn-rs = "0.5.5"
+1 -2
View File
@@ -100,6 +100,7 @@ rust_decimal = { workspace = true, features = [
rust_iso3166 = { workspace = true }
rustls.workspace = true
rusty-money = { workspace = true }
scalar_api_reference = { workspace = true, features = ["actix-web"] }
sentry = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -128,8 +129,6 @@ tracing-actix-web = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
utoipa = { workspace = true, features = ["url"] }
utoipa-actix-web = { workspace = true }
utoipa-scalar = { workspace = true, features = ["actix-web"] }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4", "v7"] }
validator = { workspace = true, features = ["derive"] }
webauthn-rs = { workspace = true, features = [
@@ -0,0 +1,7 @@
Modrinth Internal API.
> [!caution]
> This API is internal and is subject to breaking changes without notice. Please avoid using it in production.
> [!tip]
> You're probably looking for [Modrinth's public API v2](/docs?api=v2)
+101
View File
@@ -0,0 +1,101 @@
> [!success]
> API v2 is Modrinth's current, official API. It is the recommended API for integrations and applications and 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
```
> [!tip] 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/code/blob/main/apps/labrinth/src/models/v3/pats.rs#L9). 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.
### Personal access tokens
Personal access tokens (PATs) can be generated 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 per IP. If your application is hitting rate limits, try caching responses, using batch requests, or spreading out your requests over time.
Higher rate limits are granted only in very rare cases. If you believe your use case requires an exception, 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.
### 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.
<details><summary>API v1 to API v2</summary>
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.
<br />
The above snippet about User Agents was adapted from https://crates.io/policies, copyright (c) 2014 The Rust Project Developers under MIT license.
+8
View File
@@ -0,0 +1,8 @@
> [!caution]
> API v3 is Modrinth's experimental API.
>
> **Please do not use this API in production. It will be deprecated in the future.**
> [!tip]
> You're probably looking for [Modrinth's public API v2](/docs?api=v2)
+28 -4
View File
@@ -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,18 @@ pub struct OAuthClientAccessRequest {
pub requested_scopes: Scopes,
}
#[utoipa::path(
context_path = "/oauth",
path = "/authorize",
tag = "oauth",
params(
("client_id" = OAuthClientId, Query),
("redirect_uri" = Option<String>, Query),
("scope" = Option<String>, Query),
("state" = Option<String>, Query)
),
responses((status = OK))
)]
#[get("authorize")]
pub async fn init_oauth(
req: HttpRequest,
@@ -165,11 +177,15 @@ pub async fn init_oauth(
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct RespondToOAuthClientScopes {
pub flow: String,
}
#[utoipa::path(
context_path = "/oauth",
path = "/accept", tag = "oauth", responses((status = OK))
)]
#[post("accept")]
pub async fn accept_client_scopes(
req: HttpRequest,
@@ -189,6 +205,10 @@ pub async fn accept_client_scopes(
.await
}
#[utoipa::path(
context_path = "/oauth",
path = "/reject", tag = "oauth", responses((status = OK))
)]
#[post("reject")]
pub async fn reject_client_scopes(
req: HttpRequest,
@@ -201,7 +221,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 +236,10 @@ pub struct TokenResponse {
pub expires_in: i64,
}
#[utoipa::path(
context_path = "/oauth",
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
+42 -11
View File
@@ -343,6 +343,14 @@ pub fn app_setup(
pub fn app_config(
cfg: &mut web::ServiceConfig,
labrinth_config: LabrinthConfig,
) {
app_data_config(cfg, labrinth_config);
app_fallback_config(cfg);
}
pub fn app_data_config(
cfg: &mut web::ServiceConfig,
labrinth_config: LabrinthConfig,
) {
cfg.app_data(web::FormConfig::default().error_handler(|err, _req| {
routes::ApiError::Validation(err.to_string()).into()
@@ -379,16 +387,17 @@ pub fn app_config(
.app_data(labrinth_config.rate_limiter.clone())
.app_data(labrinth_config.kafka_client.clone())
.app_data(labrinth_config.search_state.clone())
.app_data(labrinth_config.webauthn.clone())
.configure(routes::v3::config)
.configure(routes::internal::config)
.configure(routes::root_config)
.default_service(web::get().wrap(default_cors()).to(routes::not_found));
.app_data(labrinth_config.webauthn.clone());
}
pub fn utoipa_app_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
_labrinth_config: LabrinthConfig,
pub fn app_fallback_config(cfg: &mut web::ServiceConfig) {
cfg.configure(routes::root_config)
.default_service(web::get().to(routes::not_found).wrap(default_cors()));
}
pub fn app_routes_config(
cfg: &mut web::ServiceConfig,
labrinth_config: LabrinthConfig,
) {
cfg.configure({
#[cfg(target_os = "linux")]
@@ -400,7 +409,29 @@ pub fn utoipa_app_config(
|_cfg| ()
}
})
.configure(routes::v2::utoipa_config)
.configure(routes::v3::utoipa_config)
.configure(routes::internal::utoipa_config);
.configure(|cfg| app_routes_config_v2(cfg, labrinth_config.clone()))
.configure(|cfg| app_routes_config_v3(cfg, labrinth_config.clone()))
.configure(|cfg| app_routes_config_internal(cfg, labrinth_config));
}
pub fn app_routes_config_v2(
cfg: &mut web::ServiceConfig,
_labrinth_config: LabrinthConfig,
) {
cfg.configure(routes::v2::config);
}
pub fn app_routes_config_v3(
cfg: &mut web::ServiceConfig,
_labrinth_config: LabrinthConfig,
) {
cfg.configure(routes::public_config)
.configure(routes::v3::config);
}
pub fn app_routes_config_internal(
cfg: &mut web::ServiceConfig,
_labrinth_config: LabrinthConfig,
) {
cfg.configure(routes::internal::config);
}
+169 -24
View File
@@ -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,22 @@ use labrinth::search;
use labrinth::util::anrok;
use labrinth::util::gotenberg::GotenbergClient;
use labrinth::util::ratelimit::rate_limit_middleware;
use labrinth::utoipa_app_config;
use labrinth::{app_config, env};
use labrinth::{app_data_config, app_fallback_config, env};
use labrinth::{
app_routes_config_internal, app_routes_config_v2, app_routes_config_v3,
};
use labrinth::{clickhouse, database, file_hosting};
use scalar_api_reference::actix_web::config as scalar_config;
use serde_json::json;
use std::ffi::CStr;
use std::sync::Arc;
use tracing::{Instrument, info, info_span};
use tracing_actix_web::TracingLogger;
use utoipa::OpenApi;
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
use utoipa_actix_web::AppExt;
use utoipa_scalar::Servable;
use utoipa::PartialSchema;
use utoipa::openapi::Content;
use utoipa::openapi::response::Response;
use utoipa::openapi::schema::Components;
#[cfg(target_os = "linux")]
#[global_allocator]
@@ -223,7 +228,16 @@ async fn app() -> std::io::Result<()> {
info!("Starting Actix HTTP server!");
HttpServer::new(move || {
App::new()
let mut docs_v2 = labrinth::routes::v2::ApiDoc::openapi();
let mut docs_v3 = labrinth::routes::v3::ApiDoc::openapi();
let mut docs_internal = labrinth::routes::internal::ApiDoc::openapi();
#[cfg(target_os = "linux")]
docs_v3.merge(labrinth::routes::debug::ApiDoc::openapi());
document_error_responses(&mut docs_v2);
document_error_responses(&mut docs_v3);
document_error_responses(&mut docs_internal);
let app = App::new()
.wrap(TracingLogger::default())
.wrap_fn(|req, srv| {
// We capture the same fields as `tracing-actix-web`'s `RootSpanBuilder`.
@@ -260,31 +274,162 @@ async fn app() -> std::io::Result<()> {
// transactions out of HTTP requests. However, we have to use our
// own - See `sentry::SentryErrorReporting` for why.
.wrap(labrinth::util::sentry::SentryErrorReporting)
// Use `utoipa` for OpenAPI generation
.into_utoipa_app()
.configure(|cfg| utoipa_app_config(cfg, labrinth_config.clone()))
.openapi_service(|api| utoipa_scalar::Scalar::with_url("/docs", ApiDoc::openapi().merge_from(api)))
.into_app()
.configure(|cfg| app_config(cfg, labrinth_config.clone()))
.configure(|cfg| app_data_config(cfg, labrinth_config.clone()))
.configure(|cfg| app_routes_config_v2(cfg, labrinth_config.clone()))
.configure(|cfg| app_routes_config_v3(cfg, labrinth_config.clone()))
.configure(|cfg| {
app_routes_config_internal(cfg, labrinth_config.clone())
});
let scalar_configuration = json!({
"sources": [
{
"title": "API v2",
"slug": "v2",
"url": "/openapi/v2.json",
"default": true
},
{
"title": "API v3 (UNSTABLE)",
"slug": "v3",
"url": "/openapi/v3.json"
},
{
"title": "Internal API (HIGHLY UNSTABLE)",
"slug": "internal",
"url": "/openapi/internal.json"
}
],
"agent": {
"disabled": true
},
"mcp": {
"disabled": true
},
"telemetry": false,
"metaData": {
"title": "Modrinth API Documentation",
"description": "Reference documentation for the Modrinth API.",
"ogTitle": "Modrinth API Documentation",
"ogDescription": "Reference documentation for the Modrinth API."
},
"modelsSectionLabel": "Schemas",
"defaultOpenFirstTag": true,
"defaultOpenAllTags": false,
"expandAllResponses": false,
"expandAllSchemaProperties": false,
"expandAllModelSections": false,
"orderSchemaPropertiesBy": "preserve",
"orderRequiredPropertiesFirst": true,
"hideSearch": false,
"searchHotKey": "k",
"showOperationId": false,
"defaultHttpClient": {
"targetKey": "shell",
"clientKey": "curl"
},
"persistAuth": false,
"showDeveloperTools": "never",
});
app.service(openapi_json_service("/openapi/v2.json", docs_v2))
.service(openapi_json_service("/openapi/v3.json", docs_v3))
.service(openapi_json_service(
"/openapi/internal.json",
docs_internal,
))
.configure(scalar_config("/docs", &scalar_configuration))
.configure(app_fallback_config)
})
.bind(&ENV.BIND_ADDR)?
.run()
.await
}
#[derive(utoipa::OpenApi)]
#[openapi(info(title = "Labrinth"), modifiers(&SecurityAddon))]
struct ApiDoc;
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) }
}))
}
struct SecurityAddon;
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()
}
}
}
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.as_mut().unwrap();
components.add_security_scheme(
"bearer_auth",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new(
"authorization",
fn document_error_responses(openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.get_or_insert_with(Components::new);
components.schemas.insert(
"ApiError".to_string(),
labrinth::models::error::ApiError::schema(),
);
for path_item in openapi.paths.paths.values_mut() {
add_default_error_response(&mut path_item.get);
add_default_error_response(&mut path_item.put);
add_default_error_response(&mut path_item.post);
add_default_error_response(&mut path_item.delete);
add_default_error_response(&mut path_item.options);
add_default_error_response(&mut path_item.head);
add_default_error_response(&mut path_item.patch);
add_default_error_response(&mut path_item.trace);
}
}
fn add_default_error_response(
operation: &mut Option<utoipa::openapi::path::Operation>,
) {
if let Some(operation) = operation {
for (status, response) in &mut operation.responses.responses {
if !is_error_response_status(status) {
continue;
}
if let utoipa::openapi::RefOr::T(response) = response {
add_error_content(response);
}
}
operation
.responses
.responses
.entry("500".to_string())
.or_insert_with(|| error_response().into());
}
}
fn is_error_response_status(status: &str) -> bool {
matches!(status.as_bytes().first(), Some(b'4' | b'5'))
|| status == "default"
}
fn error_response() -> Response {
let mut response = Response::new("Error response");
add_error_content(&mut response);
response
}
fn add_error_content(response: &mut Response) {
if response.content.is_empty() {
response.content.insert(
"application/json".to_string(),
Content::new(Some(utoipa::openapi::Ref::from_schema_name(
"ApiError",
))),
);
}
+3 -1
View File
@@ -1,10 +1,12 @@
use serde::{Deserialize, Serialize};
/// An error returned by the API
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct ApiError<'a> {
#[schema(value_type = String)]
pub error: &'a str,
pub description: String,
#[schema(value_type = Option<serde_json::Value>)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
+5 -3
View File
@@ -1,3 +1,5 @@
#![allow(clippy::large_stack_arrays)]
use crate::models::ids::{ThreadMessageId, VersionId};
use crate::models::v3::billing::PriceDuration;
use crate::models::{
@@ -13,7 +15,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct LegacyNotification {
pub id: NotificationId,
pub user_id: UserId,
@@ -30,14 +32,14 @@ pub struct LegacyNotification {
pub actions: Vec<LegacyNotificationAction>,
}
#[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 {
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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<LegacyResultSearchProject>,
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,
+1 -1
View File
@@ -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,
+6 -4
View File
@@ -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<User>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct LegacyThreadMessage {
pub id: ThreadMessageId,
pub author_id: Option<UserId>,
@@ -26,7 +26,7 @@ pub struct LegacyThreadMessage {
pub created: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LegacyMessageBody {
Text {
@@ -49,7 +49,9 @@ pub enum LegacyMessageBody {
},
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
#[derive(
Serialize, Deserialize, Eq, PartialEq, Copy, Clone, utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum LegacyThreadType {
Report,
+1 -1
View File
@@ -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,
+11 -1
View File
@@ -75,7 +75,17 @@ impl Price {
}
}
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)]
#[derive(
Serialize,
Deserialize,
Hash,
Eq,
PartialEq,
Debug,
Copy,
Clone,
utoipa::ToSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum PriceDuration {
FiveDays,
@@ -28,7 +28,7 @@ impl From<crate::database::models::DBModerationNote> for ModerationNote {
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct PatchModerationNote {
pub notes: Option<String>,
pub user_rating: Option<i32>,
+1 -1
View File
@@ -706,7 +706,7 @@ impl From<DBNotification> 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
+4 -4
View File
@@ -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<String>,
}
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct OAuthClientAuthorization {
pub id: OAuthClientAuthorizationId,
pub app_id: OAuthClientId,
+1 -1
View File
@@ -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,
+14 -3
View File
@@ -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<Decimal>,
}
#[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::schema::Schema> {
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::Type::Number)
.build()
.into()
}
}
impl utoipa::ToSchema for PayoutDecimal {}
impl Serialize for PayoutDecimal {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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 {
+2
View File
@@ -8,6 +8,7 @@ use crate::models::exp;
use crate::models::ids::{
FileId, OrganizationId, ProjectId, TeamId, ThreadId, VersionId,
};
use crate::routes::{FileHash, HashAlgorithm};
use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use itertools::Itertools;
@@ -970,6 +971,7 @@ pub struct VersionFile {
pub id: Option<FileId>,
/// A map of hashes of the file. The key is the hashing algorithm
/// and the value is the string version of the hash.
#[schema(value_type = std::collections::HashMap<HashAlgorithm, FileHash>)]
pub hashes: std::collections::HashMap<String, String>,
/// A direct link to the file for downloading it.
pub url: String,
+1 -1
View File
@@ -17,7 +17,7 @@ pub struct Report {
pub thread_id: ThreadId,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ItemType {
Project,
@@ -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,
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+28 -10
View File
@@ -46,20 +46,26 @@ pub const FILTERED_HEADERS: &[&str] = &[
"x-vercel-ip-country",
];
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(page_view_ingest)
.service(playtime_ingest)
.service(minecraft_server_play_ingest);
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct UrlInput {
url: String,
}
//this route should be behind the cloudflare WAF to prevent non-browsers from calling it
#[post("view")]
async fn page_view_ingest(
#[utoipa::path(
context_path = "/analytics",
tag = "analytics",
request_body = UrlInput,
responses((status = NO_CONTENT))
)]
#[post("/view")]
pub async fn page_view_ingest(
req: HttpRequest,
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
session_queue: web::Data<AuthQueue>,
@@ -171,7 +177,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,8 +185,14 @@ pub struct PlaytimeInput {
parent: Option<crate::models::ids::VersionId>,
}
#[post("playtime")]
async fn playtime_ingest(
#[utoipa::path(
context_path = "/analytics",
tag = "analytics",
request_body = serde_json::Value,
responses((status = NO_CONTENT))
)]
#[post("/playtime")]
pub async fn playtime_ingest(
req: HttpRequest,
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
session_queue: web::Data<AuthQueue>,
@@ -249,7 +261,7 @@ struct MinecraftProfile {
name: String,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct MinecraftJavaServerPlayInput {
project_id: ProjectId,
username: String,
@@ -258,8 +270,14 @@ pub struct MinecraftJavaServerPlayInput {
pub const MINECRAFT_SERVER_PLAYS: &str = "minecraft_server_plays";
#[post("minecraft-server-play")]
async fn minecraft_server_play_ingest(
#[utoipa::path(
context_path = "/analytics",
tag = "analytics",
request_body = MinecraftJavaServerPlayInput,
responses((status = NO_CONTENT))
)]
#[post("/minecraft-server-play")]
pub async fn minecraft_server_play_ingest(
req: HttpRequest,
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
session_queue: web::Data<AuthQueue>,
+33 -16
View File
@@ -1,29 +1,46 @@
use std::time::Duration;
use actix_web::web;
use eyre::Context;
use eyre::eyre;
use prometheus::IntGauge;
#[cfg(target_os = "linux")]
use super::SecurityAddon;
use crate::util::cors::default_cors;
#[cfg(target_os = "linux")]
mod pprof;
pub(crate) 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| ()
}
}),
);
#[cfg(target_os = "linux")]
#[derive(utoipa::OpenApi)]
#[openapi(
paths(pprof::heap, pprof::flame_graph),
modifiers(&DebugPathModifier, &SecurityAddon)
)]
pub struct ApiDoc;
#[cfg(target_os = "linux")]
struct DebugPathModifier;
#[cfg(target_os = "linux")]
impl utoipa::Modify for DebugPathModifier {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
super::prefix_openapi_paths(openapi, "/debug", |_| false);
}
}
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(
+15 -3
View File
@@ -5,11 +5,19 @@ use eyre::{Context, eyre};
use prometheus::{IntGauge, Registry};
use std::time::Duration;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(heap).service(flame_graph);
}
#[utoipa::path]
/// Get a heap profile.
#[utoipa::path(
tag = "debug",
responses((
status = OK,
body = Vec<u8>,
content_type = "application/octet-stream"
))
)]
#[get("/pprof/heap", guard = "admin_key_guard")]
pub async fn heap() -> Result<HttpResponse, ApiError> {
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
@@ -23,7 +31,11 @@ pub async fn heap() -> Result<HttpResponse, ApiError> {
.body(pprof))
}
#[utoipa::path]
/// Get a heap flame graph.
#[utoipa::path(
tag = "debug",
responses((status = OK, body = String, content_type = "image/svg+xml"))
)]
#[get("/pprof/heap/flamegraph", guard = "admin_key_guard")]
pub async fn flame_graph() -> Result<HttpResponse, ApiError> {
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
+11 -2
View File
@@ -23,9 +23,9 @@ use std::str::FromStr;
use std::sync::Arc;
use tracing::trace;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
utoipa_actix_web::scope("/admin")
web::scope("/admin")
.service(count_download)
.service(force_reindex)
.service(force_reindex_project),
@@ -130,7 +130,10 @@ async fn resolve_download_attribution_version(
}
// This is an internal route, cannot be used without key
/// Count a download.
#[utoipa::path(
context_path = "/admin",
tag = "v2 admin",
patch,
operation_id = "countDownload",
responses(
@@ -308,7 +311,10 @@ pub async fn count_download(
Ok(HttpResponse::NoContent().body(""))
}
/// Reindex all projects.
#[utoipa::path(
context_path = "/admin",
tag = "v2 admin",
post,
operation_id = "forceReindex",
responses(
@@ -330,7 +336,10 @@ pub async fn force_reindex(
Ok(HttpResponse::NoContent().finish())
}
/// Reindex a project.
#[utoipa::path(
context_path = "/admin",
tag = "v2 admin",
post,
operation_id = "forceReindexProject",
responses(
+34 -10
View File
@@ -25,7 +25,7 @@ use url::Url;
use crate::routes::ApiError;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(ingest_click)
.service(get_all)
.service(create)
@@ -40,9 +40,14 @@ pub struct IngestClick {
pub affiliate_code_id: AffiliateCodeId,
}
#[utoipa::path]
/// Ingest an affiliate click.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = NO_CONTENT))
)]
#[post("/ingest-click")]
async fn ingest_click(
pub async fn ingest_click(
req: HttpRequest,
web::Json(ingest_click): web::Json<IngestClick>,
pool: web::Data<PgPool>,
@@ -136,11 +141,14 @@ async fn ingest_click(
Ok(())
}
/// List affiliate codes.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = OK, body = inline(Vec<AffiliateCode>)))
)]
#[get("")]
async fn get_all(
pub async fn get_all(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -187,11 +195,14 @@ pub struct CreateRequest {
pub source_name: String,
}
/// Create an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = OK, body = inline(AffiliateCode)))
)]
#[put("")]
async fn create(
pub async fn create(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -263,11 +274,14 @@ async fn create(
Ok(web::Json(AffiliateCode::from(code, is_admin)))
}
/// Get an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = OK, body = inline(AffiliateCode)))
)]
#[get("/{id}")]
async fn get(
pub async fn get(
req: HttpRequest,
path: web::Path<(AffiliateCodeId,)>,
pool: web::Data<PgPool>,
@@ -302,9 +316,14 @@ async fn get(
}
}
#[utoipa::path]
/// Delete an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
async fn delete(
pub async fn delete(
req: HttpRequest,
path: web::Path<(AffiliateCodeId,)>,
pool: web::Data<PgPool>,
@@ -350,9 +369,14 @@ pub struct PatchRequest {
pub source_name: String,
}
#[utoipa::path]
/// Update an affiliate code.
#[utoipa::path(
context_path = "/affiliate",
tag = "affiliates",
responses((status = NO_CONTENT))
)]
#[patch("/{id}")]
async fn patch(
pub async fn patch(
req: HttpRequest,
path: web::Path<(AffiliateCodeId,)>,
pool: web::Data<PgPool>,
@@ -28,7 +28,7 @@ use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::error::Context;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(list)
.service(update_group)
.service(scan)
@@ -37,7 +37,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
.service(split);
}
#[derive(Serialize)]
#[derive(Serialize, utoipa::ToSchema)]
struct AttributionGroupResponse {
id: crate::models::ids::AttributionGroupId,
flame_project: Option<FlameProject>,
@@ -48,7 +48,7 @@ struct AttributionGroupResponse {
versions: Vec<VersionInfo>,
}
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, utoipa::ToSchema)]
struct VersionInfo {
id: VersionId,
name: String,
@@ -56,7 +56,7 @@ struct VersionInfo {
date_created: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize)]
#[derive(Serialize, utoipa::ToSchema)]
struct AttributionFileResponse {
name: String,
sha1: String,
@@ -67,7 +67,7 @@ struct AttributionFileResponse {
moderation_external_license: Option<ModerationExternalLicenseResponse>,
}
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, utoipa::ToSchema)]
struct ModerationExternalLicenseResponse {
id: i64,
title: Option<String>,
@@ -92,9 +92,14 @@ struct ScanResponse {
queued_files: u64,
}
#[utoipa::path]
/// Queue an attribution scan.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = OK, body = ScanResponse))
)]
#[post("/scan")]
async fn scan(
pub async fn scan(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -278,9 +283,17 @@ async fn force_scan_file(
Ok(web::Json(scan_summary))
}
#[utoipa::path]
/// List project attribution groups.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
params(
("project_id" = ProjectId, Path)
),
responses((status = OK, body = inline(Vec<AttributionGroupResponse>)))
)]
#[get("/{project_id}")]
async fn list(
pub async fn list(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -528,9 +541,14 @@ struct UpdateGroupBody {
attribution: AttributionResolution,
}
#[utoipa::path]
/// Update an attribution group.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = NO_CONTENT))
)]
#[patch("/group/{group_id}")]
async fn update_group(
pub async fn update_group(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -609,9 +627,14 @@ struct AssignBody {
project_id: ProjectId,
}
#[utoipa::path]
/// Move a file to an attribution group.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = NO_CONTENT))
)]
#[post("/assign")]
async fn assign(
pub async fn assign(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -765,9 +788,14 @@ struct SplitBody {
project_id: ProjectId,
}
#[utoipa::path]
/// Split a file into a new attribution group.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = NO_CONTENT))
)]
#[post("/split")]
async fn split(
pub async fn split(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+145 -38
View File
@@ -42,7 +42,7 @@ use stripe::{
};
use tracing::warn;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
web::scope("/billing")
.service(products)
@@ -58,11 +58,18 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(active_servers)
.service(initiate_payment)
.service(stripe_webhook)
.service(refund_charge),
.service(refund_charge)
.service(reprocess_charge_tax),
);
}
#[get("products")]
/// List products.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[get("/products")]
pub async fn products(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -99,7 +106,14 @@ struct SubscriptionsQuery {
pub user_id: Option<ariadne::ids::UserId>,
}
#[get("subscriptions")]
/// List subscriptions.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("user_id" = Option<ariadne::ids::UserId>, Query)),
responses((status = OK, body = serde_json::Value))
)]
#[get("/subscriptions")]
pub async fn subscriptions(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -141,7 +155,7 @@ pub async fn subscriptions(
Ok(HttpResponse::Ok().json(subscriptions))
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChargeRefundAmount {
Full,
@@ -149,14 +163,21 @@ pub enum ChargeRefundAmount {
None,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ChargeRefund {
#[serde(flatten)]
pub amount: ChargeRefundAmount,
pub unprovision: Option<bool>,
}
#[post("charge/{id}/refund")]
/// Refund a charge.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
request_body = ChargeRefund,
responses((status = NO_CONTENT))
)]
#[post("/charge/{id}/refund")]
#[allow(clippy::too_many_arguments)]
pub async fn refund_charge(
req: HttpRequest,
@@ -419,7 +440,13 @@ pub async fn refund_charge(
Ok(HttpResponse::NoContent().finish())
}
#[post("charge/{id}/tax/reprocess")]
/// Reprocess tax for a charge.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[post("/charge/{id}/tax/reprocess")]
pub async fn reprocess_charge_tax(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -587,8 +614,9 @@ pub async fn reprocess_charge_tax(
Ok(HttpResponse::NoContent().finish())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct SubscriptionEdit {
#[schema(value_type = String)]
pub interval: Option<PriceDuration>,
pub payment_method: Option<String>,
pub cancelled: Option<bool>,
@@ -601,7 +629,17 @@ pub struct SubscriptionEditQuery {
pub dry: Option<bool>,
}
#[patch("subscription/{id}")]
/// Update a subscription.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("dry" = Option<bool>, Query)),
responses(
(status = OK, body = serde_json::Value),
(status = NO_CONTENT),
)
)]
#[patch("/subscription/{id}")]
#[allow(clippy::too_many_arguments)]
pub async fn edit_subscription(
req: HttpRequest,
@@ -1091,7 +1129,13 @@ pub async fn edit_subscription(
}
}
#[get("customer")]
/// Get the current customer.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[get("/customer")]
pub async fn user_customer(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1129,7 +1173,14 @@ pub struct ChargesQuery {
pub user_id: Option<ariadne::ids::UserId>,
}
#[get("payments")]
/// List payments.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("user_id" = Option<ariadne::ids::UserId>, Query)),
responses((status = OK, body = serde_json::Value))
)]
#[get("/payments")]
pub async fn charges(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1188,7 +1239,13 @@ pub async fn charges(
))
}
#[post("payment_method")]
/// Start a payment method flow.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[post("/payment_method")]
pub async fn add_payment_method_flow(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1241,7 +1298,13 @@ pub struct EditPaymentMethod {
pub primary: bool,
}
#[patch("payment_method/{id}")]
/// Update a payment method.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[patch("/payment_method/{id}")]
pub async fn edit_payment_method(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1305,7 +1368,13 @@ pub async fn edit_payment_method(
}
}
#[delete("payment_method/{id}")]
/// Remove a payment method.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[delete("/payment_method/{id}")]
pub async fn remove_payment_method(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1388,7 +1457,16 @@ pub async fn remove_payment_method(
}
}
#[get("payment_methods")]
/// List payment methods.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses(
(status = OK, body = serde_json::Value),
(status = NO_CONTENT),
)
)]
#[get("/payment_methods")]
pub async fn payment_methods(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1432,7 +1510,24 @@ pub struct ActiveServersQuery {
pub subscription_status: Option<SubscriptionStatus>,
}
#[get("active_servers")]
#[derive(Serialize, utoipa::ToSchema)]
struct ActiveServerResponse {
pub user_id: ariadne::ids::UserId,
pub server_id: String,
pub price_id: crate::models::ids::ProductPriceId,
#[schema(value_type = String)]
pub interval: PriceDuration,
pub region: Option<String>,
}
/// List active servers.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
params(("subscription_status" = Option<String>, Query)),
responses((status = OK, body = inline(Vec<ActiveServerResponse>)))
)]
#[get("/active_servers")]
pub async fn active_servers(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1457,21 +1552,12 @@ pub async fn active_servers(
)
.await?;
#[derive(Serialize)]
struct ActiveServer {
pub user_id: ariadne::ids::UserId,
pub server_id: String,
pub price_id: crate::models::ids::ProductPriceId,
pub interval: PriceDuration,
pub region: Option<String>,
}
let server_ids = servers
.into_iter()
.filter_map(|x| {
x.metadata.as_ref().and_then(|metadata| match metadata {
SubscriptionMetadata::Pyro { id, region } => {
Some(ActiveServer {
Some(ActiveServerResponse {
user_id: x.user_id.into(),
server_id: id.clone(),
price_id: x.price_id.into(),
@@ -1482,12 +1568,12 @@ pub async fn active_servers(
SubscriptionMetadata::Medal { .. } => None,
})
})
.collect::<Vec<ActiveServer>>();
.collect::<Vec<ActiveServerResponse>>();
Ok(HttpResponse::Ok().json(server_ids))
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PaymentRequestType {
PaymentMethod { id: String },
@@ -1507,7 +1593,7 @@ impl PaymentRequestType {
}
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChargeRequestType {
Existing {
@@ -1515,11 +1601,12 @@ pub enum ChargeRequestType {
},
New {
product_id: crate::models::ids::ProductId,
#[schema(value_type = String)]
interval: Option<PriceDuration>,
},
}
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct PaymentRequestMetadata {
#[serde(flatten)]
@@ -1527,7 +1614,7 @@ pub struct PaymentRequestMetadata {
pub affiliate_code: Option<AffiliateCodeId>,
}
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PaymentRequestMetadataKind {
Pyro {
@@ -1537,16 +1624,23 @@ pub enum PaymentRequestMetadataKind {
},
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct PaymentRequest {
#[serde(flatten)]
pub type_: PaymentRequestType,
pub charge: ChargeRequestType,
#[schema(value_type = String)]
pub existing_payment_intent: Option<stripe::PaymentIntentId>,
pub metadata: Option<PaymentRequestMetadata>,
}
#[post("payment")]
/// Initiate a payment.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = OK, body = serde_json::Value))
)]
#[post("/payment")]
pub async fn initiate_payment(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1610,7 +1704,14 @@ pub async fn initiate_payment(
}
}
#[post("_stripe")]
/// Receive a Stripe webhook.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
request_body(content = String, content_type = "text/plain"),
responses((status = NO_CONTENT))
)]
#[post("/_stripe")]
pub async fn stripe_webhook(
req: HttpRequest,
payload: String,
@@ -2503,7 +2604,7 @@ async fn apply_credit_many(
Ok(())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct CreditRequest {
#[serde(flatten)]
pub target: CreditTarget,
@@ -2512,7 +2613,7 @@ pub struct CreditRequest {
pub message: String,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(untagged)]
pub enum CreditTarget {
Subscriptions {
@@ -2526,7 +2627,13 @@ pub enum CreditTarget {
},
}
#[post("credit")]
/// Credit subscriptions.
#[utoipa::path(
context_path = "/billing",
tag = "billing",
responses((status = NO_CONTENT))
)]
#[post("/credit")]
pub async fn credit(
req: HttpRequest,
pool: web::Data<PgPool>,
+14 -3
View File
@@ -27,7 +27,7 @@ use crate::{
util::{error::Context, http::HttpClient, tiltify::TiltifyClient},
};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(tiltify_webhook).service(pride_26);
}
@@ -143,7 +143,13 @@ impl CampaignDonation {
}
}
#[utoipa::path]
/// Receive a Tiltify webhook.
#[utoipa::path(
context_path = "/campaign",
tag = "campaigns",
request_body(content = String, content_type = "text/plain"),
responses((status = NO_CONTENT))
)]
#[post("/webhook")]
pub async fn tiltify_webhook(
req: HttpRequest,
@@ -301,7 +307,12 @@ fn verify_tiltify_webhook_signature(
Ok(())
}
#[utoipa::path]
/// Get Pride campaign data.
#[utoipa::path(
context_path = "/campaign",
tag = "campaigns",
responses((status = OK, body = CampaignInfo))
)]
#[get("/pride-26")]
pub async fn pride_26(
http: web::Data<HttpClient>,
@@ -35,7 +35,7 @@ use crate::{
pub mod rescan;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
web::scope("/delphi")
.service(ingest_report)
@@ -141,8 +141,14 @@ pub struct DelphiRunParameters {
pub file_id: crate::models::ids::FileId,
}
#[post("ingest", guard = "admin_key_guard")]
async fn ingest_report(
/// Ingest a Delphi report.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
responses((status = NO_CONTENT))
)]
#[post("/ingest", guard = "admin_key_guard")]
pub async fn ingest_report(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
web::Json(report): web::Json<serde_json::Value>,
@@ -466,8 +472,15 @@ pub async fn send_tech_review_exit_file_deleted_message_if_exited(
Ok(())
}
#[post("run")]
async fn _run(
/// Run Delphi.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
params(("file_id" = crate::models::ids::FileId, Query)),
responses((status = NO_CONTENT))
)]
#[post("/run")]
pub async fn _run(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -487,8 +500,14 @@ async fn _run(
run(&**pool, run_parameters.into_inner(), &http).await
}
#[get("version")]
async fn version(
/// Get the Delphi version.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
responses((status = OK, body = inline(Option<i32>)))
)]
#[get("/version")]
pub async fn version(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -510,8 +529,14 @@ async fn version(
))
}
#[get("issue_type/schema")]
async fn issue_type_schema(
/// Get the Delphi issue type schema.
#[utoipa::path(
context_path = "/delphi",
tag = "delphi",
responses((status = OK, body = serde_json::Value))
)]
#[get("/issue_type/schema")]
pub async fn issue_type_schema(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -28,14 +28,14 @@ use eyre::eyre;
use lettre::message::Mailbox;
use serde::{Deserialize, Serialize};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(create)
.service(create_email_sync)
.service(remove)
.service(send_custom_email);
}
#[derive(Deserialize, PartialEq, Default)]
#[derive(Deserialize, PartialEq, Default, utoipa::ToSchema)]
enum EmailStrategy {
#[default]
Async,
@@ -43,8 +43,9 @@ enum EmailStrategy {
None,
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
struct CreateNotification {
#[schema(value_type = serde_json::Value)]
pub body: NotificationBody,
pub user_ids: Vec<UserId>,
#[serde(default)]
@@ -78,7 +79,12 @@ where
error.as_api_error().serialize(serializer)
}
#[post("external_notifications", guard = "external_notification_key_guard")]
/// Create external notifications.
#[utoipa::path(
tag = "external notifications",
responses((status = ACCEPTED))
)]
#[post("/external_notifications", guard = "external_notification_key_guard")]
pub async fn create(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -89,6 +95,46 @@ pub async fn create(
.await
}
/// Create notifications and send emails.
///
/// Responds with the user IDs that could not be emailed:
/// - `200` if every recipient was emailed (empty list)
/// - `207` if some recipients could not be emailed (list of failed IDs)
/// Create email sync.
#[utoipa::path(
tag = "external notifications",
responses(
(status = OK, body = inline(Vec<UserId>)),
(status = 207, body = inline(Vec<UserId>)),
)
)]
#[post(
"external_notifications/email-sync",
guard = "external_notification_key_guard"
)]
pub async fn create_email_sync(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
email_queue: web::Data<EmailQueue>,
data: web::Json<CreateNotification>,
) -> Result<(web::Json<Vec<UserId>>, StatusCode), ApiError> {
let data = data.into_inner();
create_impl(
pool,
redis,
email_queue,
CreateNotification {
body: data.body,
user_ids: data.user_ids,
email: EmailStrategy::Sync,
},
)
.await
.map(|(res, code)| {
(web::Json(res.into_inner().into_keys().collect()), code)
})
}
async fn create_impl(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -224,44 +270,19 @@ async fn create_impl(
Ok((web::Json(HashMap::new()), StatusCode::ACCEPTED))
}
/// Inserts notifications for all users and tries to send emails immediately.
///
/// Responds with the user IDs that could not be emailed and a reason why
#[post(
"external_notifications/email-sync",
guard = "external_notification_key_guard"
)]
pub async fn create_email_sync(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
email_queue: web::Data<EmailQueue>,
data: web::Json<CreateNotification>,
) -> Result<(web::Json<Vec<UserId>>, StatusCode), ApiError> {
let data = data.into_inner();
create_impl(
pool,
redis,
email_queue,
CreateNotification {
body: data.body,
user_ids: data.user_ids,
email: EmailStrategy::Sync,
},
)
.await
.map(|(res, code)| {
(web::Json(res.into_inner().into_keys().collect()), code)
})
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
struct NotificationFilter {
pub user_ids: Vec<UserId>,
#[serde(flatten)]
pub body: serde_json::Map<String, serde_json::Value>,
}
#[delete("external_notifications", guard = "external_notification_key_guard")]
/// Remove external notifications.
#[utoipa::path(
tag = "external notifications",
responses((status = NO_CONTENT))
)]
#[delete("/external_notifications", guard = "external_notification_key_guard")]
pub async fn remove(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -301,7 +322,7 @@ pub async fn remove(
Ok(HttpResponse::NoContent().finish())
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
struct SendEmail {
pub users: Vec<UserId>,
pub key: String,
@@ -309,7 +330,12 @@ struct SendEmail {
pub title: String,
}
#[post("external_notifications/send_custom_email")]
/// Send a custom email.
#[utoipa::path(
tag = "external notifications",
responses((status = ACCEPTED))
)]
#[post("/external_notifications/send_custom_email")]
pub async fn send_custom_email(
req: HttpRequest,
pool: web::Data<PgPool>,
+127 -56
View File
@@ -60,9 +60,9 @@ use webauthn_rs::prelude::{
};
use zxcvbn::Score;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
utoipa_actix_web::scope("/auth")
web::scope("/auth")
.service(init)
.service(auth_callback)
.service(delete_auth_provider)
@@ -1068,12 +1068,15 @@ pub struct Authorization {
// Init link takes us to GitHub API and calls back to callback endpoint with a code and state
// http://localhost:8000/auth/init?url=https://modrinth.com
#[utoipa::path(
get,
operation_id = "authInit",
responses(
(status = 307, description = "Redirect to OAuth provider"),
(status = 400, description = "Invalid input")
)
context_path = "/auth",
tag = "auth",
params(
("url" = Url, Query),
("provider" = Option<AuthProvider>, Query),
("token" = Option<String>, Query),
("auth_token" = Option<String>, Query)
),
responses((status = TEMPORARY_REDIRECT), (status = OK))
)]
#[get("/init")]
pub async fn init(
@@ -1166,12 +1169,8 @@ pub async fn init(
}
#[utoipa::path(
get,
operation_id = "authCallback",
responses(
(status = 307, description = "Redirect with auth code"),
(status = 401, description = "Authentication failed")
)
context_path = "/auth",
tag = "auth", responses((status = OK))
)]
#[get("/callback")]
pub async fn auth_callback(
@@ -1441,16 +1440,19 @@ struct NewOAuthAccount {
pub sign_up_newsletter: bool,
}
/// Create account with OAuth.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "createOAuthAccount",
responses(
(status = 200, description = "OAuth account created"),
(status = 400, description = "Invalid input")
)
operation_id = "createOAuthAccount",
responses(
(status = 200, description = "OAuth account created", body = serde_json::Value),
(status = 400, description = "Invalid input")
)
)]
#[post("/create/oauth")]
async fn create_oauth_account(
pub async fn create_oauth_account(
req: HttpRequest,
db: Data<PgPool>,
file_host: Data<dyn FileHost>,
@@ -1525,7 +1527,10 @@ struct DiscordCommunityHandoffPayload {
nonce: String,
}
/// Link Discord community.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
operation_id = "discordCommunityLink",
responses(
(status = 200, description = "Discord community bot handoff URL", body = DiscordCommunityLinkResponse),
@@ -1599,7 +1604,10 @@ pub async fn discord_community_link(
Ok(web::Json(DiscordCommunityLinkResponse { url }))
}
/// Remove an auth provider.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
delete,
operation_id = "deleteAuthProvider",
responses(
@@ -1939,11 +1947,14 @@ impl ReadyAccountRegisterFlow {
}
}
/// Validate password account creation.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "validateCreateAccountWithPassword",
responses(
(status = 200, description = "Account input is valid"),
(status = NO_CONTENT, description = "Account input is valid"),
(status = 400, description = "Invalid input")
)
)]
@@ -1964,13 +1975,16 @@ pub async fn validate_create_account_with_password(
Ok(())
}
/// Create account with a password.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "createAccountPassword",
responses(
(status = 200, description = "Account created"),
(status = 400, description = "Invalid input")
)
operation_id = "createAccountPassword",
responses(
(status = 200, description = "Account created", body = serde_json::Value),
(status = 400, description = "Invalid input")
)
)]
#[post("/create")]
pub async fn create_account_with_password(
@@ -2010,13 +2024,16 @@ pub struct Login {
pub challenge: String,
}
/// Log in with a password.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "loginPassword",
responses(
(status = 200, description = "Login successful"),
(status = 401, description = "Invalid credentials")
)
operation_id = "loginPassword",
responses(
(status = 200, description = "Login successful", body = serde_json::Value),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login")]
pub async fn login_password(
@@ -2168,13 +2185,16 @@ async fn validate_2fa_code(
}
}
/// Complete login with 2FA.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "login2fa",
responses(
(status = 200, description = "2FA login successful"),
(status = 401, description = "Invalid credentials")
)
operation_id = "login2fa",
responses(
(status = 200, description = "2FA login successful", body = serde_json::Value),
(status = 401, description = "Invalid credentials")
)
)]
#[post("/login/2fa")]
pub async fn login_2fa(
@@ -2225,13 +2245,16 @@ pub async fn login_2fa(
}
}
/// Start 2FA setup.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "begin2faFlow",
responses(
(status = 200, description = "2FA secret generated"),
(status = 401, description = "Unauthorized")
),
operation_id = "begin2faFlow",
responses(
(status = 200, description = "2FA secret generated", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa/get_secret")]
@@ -2273,13 +2296,16 @@ pub async fn begin_2fa_flow(
}
}
/// Finish 2FA setup.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "finish2faFlow",
responses(
(status = 200, description = "2FA enabled"),
(status = 401, description = "Unauthorized")
),
operation_id = "finish2faFlow",
responses(
(status = 200, description = "2FA enabled", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[post("/2fa")]
@@ -2405,7 +2431,10 @@ pub struct Remove2FA {
pub code: String,
}
/// Remove 2FA.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
delete,
operation_id = "remove2fa",
responses(
@@ -2502,7 +2531,10 @@ pub struct ResetPassword {
pub challenge: String,
}
/// Start password reset.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "resetPasswordBegin",
responses(
@@ -2605,7 +2637,10 @@ pub struct ChangePassword {
pub new_password: Option<String>,
}
/// Change password.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
patch,
operation_id = "changePassword",
responses(
@@ -2768,7 +2803,10 @@ pub struct SetEmail {
pub email: String,
}
/// Set email address.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
patch,
operation_id = "setEmail",
responses(
@@ -2887,7 +2925,10 @@ pub async fn set_email(
Ok(HttpResponse::Ok().finish())
}
/// Resend verification email.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "resendVerifyEmail",
responses(
@@ -2959,7 +3000,10 @@ pub struct VerifyEmail {
pub flow: String,
}
/// Verify email address.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "verifyEmail",
responses(
@@ -3022,7 +3066,10 @@ pub async fn verify_email(
}
}
/// Subscribe to the newsletter.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "subscribeNewsletter",
responses(
@@ -3068,13 +3115,16 @@ pub async fn subscribe_newsletter(
Ok(HttpResponse::NoContent().finish())
}
/// Get newsletter subscription status.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
get,
operation_id = "getNewsletterSubscriptionStatus",
responses(
(status = 200, description = "Subscription status"),
(status = 401, description = "Unauthorized")
),
operation_id = "getNewsletterSubscriptionStatus",
responses(
(status = 200, description = "Subscription status", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = []))
)]
#[get("/email/subscribe")]
@@ -3115,7 +3165,10 @@ pub struct RegisterPasskeyResponse {
pub flow: String,
}
/// Start passkey registration.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "registerPasskeyStart",
responses(
@@ -3212,7 +3265,10 @@ pub struct PasskeyResponse {
pub last_used: Option<chrono::DateTime<Utc>>,
}
/// Finish passkey registration.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "registerPasskeyFinish",
responses(
@@ -3318,7 +3374,10 @@ pub struct AuthenticatePasskeyResponse {
pub flow: String,
}
/// Start passkey authentication.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "authenticatePasskeyStart",
responses(
@@ -3358,13 +3417,16 @@ pub struct AuthenticatePasskeyFinish {
pub credential: PublicKeyCredential,
}
/// Finish passkey authentication.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
post,
operation_id = "authenticatePasskeyFinish",
responses(
(status = 200, description = "Passkey authentication successful"),
(status = 400, description = "Invalid input")
)
operation_id = "authenticatePasskeyFinish",
responses(
(status = 200, description = "Passkey authentication successful", body = serde_json::Value),
(status = 400, description = "Invalid input")
)
)]
#[post("/passkey/finish")]
pub async fn authenticate_passkey_finish(
@@ -3472,7 +3534,10 @@ pub async fn authenticate_passkey_finish(
}
}
/// List passkeys.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
get,
operation_id = "listPasskeys",
responses(
@@ -3519,7 +3584,10 @@ pub struct RenamePasskey {
pub name: String,
}
/// Rename a passkey.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
patch,
operation_id = "renamePasskey",
responses(
@@ -3571,7 +3639,10 @@ pub async fn rename_passkey(
Ok(HttpResponse::NoContent().finish())
}
/// Delete a passkey.
#[utoipa::path(
context_path = "/auth",
tag = "auth",
delete,
operation_id = "deletePasskey",
responses(
+7 -1
View File
@@ -6,10 +6,16 @@ use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use actix_web::{HttpRequest, HttpResponse, post, web};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(web::scope("/gdpr").service(export));
}
/// Export GDPR data.
#[utoipa::path(
context_path = "/gdpr",
tag = "GDPR",
responses((status = OK, body = serde_json::Value))
)]
#[post("/export")]
pub async fn export(
req: HttpRequest,
+7 -3
View File
@@ -10,7 +10,7 @@ use chrono::{Datelike, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_globals);
}
@@ -89,8 +89,12 @@ pub fn tax_compliance_payout_threshold_for_year(
value
}
/// Gets configured global non-secret variables for this backend instance.
#[utoipa::path]
/// Get backend globals.
#[utoipa::path(
context_path = "/globals",
tag = "globals",
responses((status = OK, body = Globals))
)]
#[get("")]
pub async fn get_globals() -> web::Json<Globals> {
web::Json(GLOBALS.clone())
+12 -1
View File
@@ -28,6 +28,12 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(success_callback).service(error_callback);
}
/// Receive a Gotenberg success callback.
#[utoipa::path(
tag = "gotenberg",
request_body = Vec<u8>,
responses((status = NO_CONTENT))
)]
#[post("/gotenberg/success", guard = "internal_network_guard")]
pub async fn success_callback(
web::Header(header::ContentDisposition {
@@ -82,7 +88,7 @@ pub async fn success_callback(
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
#[derive(Debug, Clone, Serialize, Deserialize, Error, utoipa::ToSchema)]
pub struct GotenbergError {
pub status: Option<String>,
pub message: Option<String>,
@@ -101,6 +107,11 @@ impl fmt::Display for GotenbergError {
}
}
/// Receive a Gotenberg error callback.
#[utoipa::path(
tag = "gotenberg",
responses((status = NO_CONTENT))
)]
#[post("/gotenberg/error", guard = "internal_network_guard")]
pub async fn error_callback(
web::Header(GotenbergTrace(trace)): web::Header<GotenbergTrace>,
+21 -9
View File
@@ -13,7 +13,7 @@ use crate::queue::billing::try_process_user_redeemal;
use crate::routes::ApiError;
use crate::util::guards::medal_key_guard;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(web::scope("/medal").service(verify).service(redeem));
}
@@ -22,7 +22,19 @@ struct MedalQuery {
username: String,
}
#[post("verify", guard = "medal_key_guard")]
#[derive(Serialize, utoipa::ToSchema)]
struct VerifyResponse {
user_id: UserId,
redeemed: bool,
}
/// Verify Medal credentials.
#[utoipa::path(
context_path = "/medal",
tag = "medal",
responses((status = OK, body = VerifyResponse))
)]
#[post("/verify", guard = "medal_key_guard")]
pub async fn verify(
pool: web::Data<PgPool>,
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
@@ -35,12 +47,6 @@ pub async fn verify(
)
.await?;
#[derive(Serialize)]
struct VerifyResponse {
user_id: UserId,
redeemed: bool,
}
match maybe_fields {
None => Err(ApiError::NotFound),
Some(fields) => Ok(HttpResponse::Ok().json(VerifyResponse {
@@ -50,7 +56,13 @@ pub async fn verify(
}
}
#[post("redeem", guard = "medal_key_guard")]
/// Redeem Medal credit.
#[utoipa::path(
context_path = "/medal",
tag = "medal",
responses((status = ACCEPTED), (status = CREATED))
)]
#[post("/redeem", guard = "medal_key_guard")]
pub async fn redeem(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+154 -81
View File
@@ -19,99 +19,172 @@ pub mod session;
pub mod statuses;
pub use super::ApiError;
use super::SecurityAddon;
use super::v3::oauth_clients;
use crate::util::cors::default_cors;
use actix_web::web;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
actix_web::web::scope("/_internal")
web::scope("/_internal")
.wrap(default_cors())
.configure(|cfg| {
cfg.service(
actix_web::web::scope("/admin")
.service(admin::count_download)
.service(admin::force_reindex)
.service(admin::force_reindex_project),
);
cfg.service(
actix_web::web::scope("/session")
.service(session::list)
.service(session::delete)
.service(session::refresh),
);
cfg.service(
actix_web::web::scope("/auth")
.service(flows::init)
.service(flows::auth_callback)
.service(flows::delete_auth_provider)
.service(flows::create_account_with_password)
.service(flows::login_password)
.service(flows::login_2fa)
.service(flows::begin_2fa_flow)
.service(flows::finish_2fa_flow)
.service(flows::remove_2fa)
.service(flows::reset_password_begin)
.service(flows::change_password)
.service(flows::resend_verify_email)
.service(flows::set_email)
.service(flows::verify_email)
.service(flows::subscribe_newsletter)
.service(flows::get_newsletter_subscription_status)
.service(flows::discord_community_link),
);
cfg.service(pats::get_pats);
cfg.service(pats::create_pat);
cfg.service(pats::edit_pat);
cfg.service(pats::delete_pat);
})
.configure(admin::config)
.configure(session::config)
.configure(flows::config)
.configure(pats::config)
.configure(oauth_clients::config)
.service(web::scope("/moderation").configure(moderation::config))
.service(web::scope("/affiliate").configure(affiliate::config))
.service(web::scope("/campaign").configure(campaign::config))
.service(web::scope("/search-management").configure(search::config))
.service(web::scope("/globals").configure(globals::config))
.service(web::scope("/server-ping").configure(server_ping::config))
.service(web::scope("/attribution").configure(attribution::config))
.configure(billing::config)
.configure(delphi::config)
.configure(external_notifications::config)
.configure(gdpr::config)
.configure(gotenberg::config)
.configure(statuses::config)
.configure(medal::config)
.configure(external_notifications::config)
.configure(mural::config)
.configure(delphi::config),
.configure(statuses::config),
)
.service(
web::scope("/v3/analytics-event")
.wrap(default_cors())
.configure(super::v3::analytics_event::config),
);
}
pub fn utoipa_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
) {
cfg.service(
utoipa_actix_web::scope("/_internal/moderation")
.wrap(default_cors())
.configure(moderation::config),
)
.service(
utoipa_actix_web::scope("/_internal/affiliate")
.wrap(default_cors())
.configure(affiliate::config),
)
.service(
utoipa_actix_web::scope("/_internal/campaign")
.wrap(default_cors())
.configure(campaign::config),
)
.service(
utoipa_actix_web::scope("/_internal/search-management")
.wrap(default_cors())
.configure(search::config),
)
.service(
utoipa_actix_web::scope("/_internal/globals")
.wrap(default_cors())
.configure(globals::config),
)
.service(
utoipa_actix_web::scope("/_internal/server-ping")
.wrap(default_cors())
.configure(server_ping::config),
)
.service(
utoipa_actix_web::scope("/_internal/attribution")
.wrap(default_cors())
.configure(attribution::config),
);
#[derive(utoipa::OpenApi)]
#[openapi(
info(
title = "Internal API (UNSTABLE)",
version = "internal",
description = include_str!("../../api_internal_description.md"),
),
paths(
admin::count_download,
admin::force_reindex,
admin::force_reindex_project,
session::list,
session::delete,
session::refresh,
flows::init,
flows::auth_callback,
flows::create_oauth_account,
flows::discord_community_link,
flows::delete_auth_provider,
flows::validate_create_account_with_password,
flows::create_account_with_password,
flows::login_password,
flows::login_2fa,
flows::begin_2fa_flow,
flows::finish_2fa_flow,
flows::remove_2fa,
flows::reset_password_begin,
flows::change_password,
flows::set_email,
flows::resend_verify_email,
flows::verify_email,
flows::subscribe_newsletter,
flows::get_newsletter_subscription_status,
flows::register_passkey_start,
flows::register_passkey_finish,
flows::authenticate_passkey_start,
flows::authenticate_passkey_finish,
flows::list_passkeys,
flows::rename_passkey,
flows::delete_passkey,
pats::get_pats,
pats::create_pat,
pats::edit_pat,
pats::delete_pat,
moderation::get_projects,
moderation::get_project_meta,
moderation::set_project_meta,
moderation::acquire_lock,
moderation::override_lock,
moderation::get_lock_status,
moderation::release_lock,
moderation::release_lock_beacon,
moderation::delete_all_locks,
moderation::tech_review::get_issue,
moderation::tech_review::get_report,
moderation::tech_review::search_projects,
moderation::tech_review::get_project_report,
moderation::tech_review::submit_report,
moderation::tech_review::update_issue_details,
moderation::tech_review::add_report,
moderation::external_license::search,
moderation::external_license::lookup,
moderation::external_license::get_by_sha1,
moderation::external_license::get_by_sha1_bulk,
moderation::external_license::add_file,
moderation::external_license::reassign_file,
moderation::external_license::update_license,
affiliate::ingest_click,
affiliate::get_all,
affiliate::create,
affiliate::get,
affiliate::delete,
affiliate::patch,
campaign::tiltify_webhook,
campaign::pride_26,
search::tasks,
search::tasks_cancel,
globals::get_globals,
server_ping::ping_minecraft_java,
attribution::scan,
attribution::list,
attribution::update_group,
attribution::assign,
attribution::split,
billing::products,
billing::subscriptions,
billing::refund_charge,
billing::reprocess_charge_tax,
billing::edit_subscription,
billing::user_customer,
billing::charges,
billing::add_payment_method_flow,
billing::edit_payment_method,
billing::remove_payment_method,
billing::payment_methods,
billing::active_servers,
billing::initiate_payment,
billing::stripe_webhook,
billing::credit,
delphi::ingest_report,
delphi::_run,
delphi::version,
delphi::issue_type_schema,
external_notifications::create,
external_notifications::create_email_sync,
external_notifications::remove,
external_notifications::send_custom_email,
gdpr::export,
gotenberg::success_callback,
gotenberg::error_callback,
medal::verify,
medal::redeem,
mural::get_bank_details,
statuses::ws_init,
super::v3::analytics_event::analytics_events_get,
super::v3::analytics_event::analytics_event_create,
super::v3::analytics_event::analytics_event_edit,
super::v3::analytics_event::analytics_event_delete,
),
modifiers(&InternalPathModifier, &SecurityAddon)
)]
pub struct ApiDoc;
struct InternalPathModifier;
impl utoipa::Modify for InternalPathModifier {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
super::prefix_openapi_paths(openapi, "/_internal", |path| {
path.starts_with("/v3/")
});
}
}
@@ -13,7 +13,7 @@ use crate::queue::moderation::ApprovalType;
use crate::routes::ApiError;
use crate::{auth::check_is_moderator_from_headers, queue::session::AuthQueue};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(search)
.service(get_by_sha1)
.service(get_by_sha1_bulk)
@@ -329,9 +329,14 @@ async fn fetch_by_flame_ids(
Ok(results)
}
#[utoipa::path]
/// Search external licenses.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = inline(Vec<ExternalProject>)))
)]
#[post("/search")]
async fn search(
pub async fn search(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -393,9 +398,14 @@ async fn search(
Ok(web::Json(results))
}
#[utoipa::path]
/// Look up external license metadata.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalLicenseLookupResponse))
)]
#[post("/lookup")]
async fn lookup(
pub async fn lookup(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -422,9 +432,14 @@ async fn lookup(
}))
}
#[utoipa::path]
/// Get external license by SHA-1.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[get("/by-sha1/{sha1}")]
async fn get_by_sha1(
pub async fn get_by_sha1(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -448,9 +463,14 @@ async fn get_by_sha1(
Ok(web::Json(result))
}
#[utoipa::path]
/// Get external licenses by SHA-1.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = inline(HashMap<String, ExternalProject>)))
)]
#[post("/by-sha1")]
async fn get_by_sha1_bulk(
pub async fn get_by_sha1_bulk(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -472,9 +492,14 @@ async fn get_by_sha1_bulk(
Ok(web::Json(results))
}
#[utoipa::path]
/// Add an external license file.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[post("/file")]
async fn add_file(
pub async fn add_file(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -484,9 +509,14 @@ async fn add_file(
upsert_file_license(req, pool, redis, session_queue, body).await
}
#[utoipa::path]
/// Reassign an external license file.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[post("/file/reassign")]
async fn reassign_file(
pub async fn reassign_file(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -584,9 +614,14 @@ async fn upsert_file_license(
))
}
#[utoipa::path]
/// Update an external license.
#[utoipa::path(
context_path = "/moderation/external-license",
tag = "moderation",
responses((status = OK, body = ExternalProject))
)]
#[patch("/{id}")]
async fn update_license(
pub async fn update_license(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -21,11 +21,11 @@ use ownership::get_projects_ownership;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
mod external_license;
pub mod external_license;
mod ownership;
mod tech_review;
pub mod tech_review;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_projects)
.service(get_project_meta)
.service(set_project_meta)
@@ -35,13 +35,9 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
.service(release_lock)
.service(release_lock_beacon)
.service(delete_all_locks)
.service(web::scope("/tech-review").configure(tech_review::config))
.service(
utoipa_actix_web::scope("/tech-review")
.configure(tech_review::config),
)
.service(
utoipa_actix_web::scope("/external-license")
.configure(external_license::config),
web::scope("/external-license").configure(external_license::config),
);
}
@@ -162,12 +158,19 @@ pub struct DeleteAllLocksResponse {
pub deleted_count: u64,
}
/// Fetch all projects which are in the moderation queue.
/// List projects in the moderation queue.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
params(
("count" = Option<u16>, Query),
("offset" = Option<u32>, Query),
("has_external_dependencies" = Option<bool>, Query)
),
responses((status = OK, body = inline(Vec<FetchedProject>)))
)]
#[get("/projects")]
async fn get_projects(
pub async fn get_projects(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -291,12 +294,14 @@ pub async fn get_projects_internal(
Ok(web::Json(projects))
}
/// Fetch moderation metadata for a specific project.
/// Get project moderation metadata.
#[utoipa::path(
responses((status = OK, body = inline(Vec<Project>)))
context_path = "/moderation",
tag = "moderation",
responses((status = OK, body = MissingMetadata))
)]
#[get("/project/{id}")]
async fn get_project_meta(
pub async fn get_project_meta(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -447,10 +452,14 @@ pub enum Judgement {
},
}
/// Update moderation judgements for projects in the review queue.
#[utoipa::path]
/// Update project moderation judgements.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses((status = NO_CONTENT))
)]
#[post("/project")]
async fn set_project_meta(
pub async fn set_project_meta(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -536,16 +545,18 @@ async fn set_project_meta(
Ok(())
}
/// Acquire or refresh a moderation lock on a project.
/// Acquire a moderation lock.
/// Returns success if acquired, or info about who holds the lock if blocked.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockAcquireResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[post("/lock/{project_id}")]
async fn acquire_lock(
pub async fn acquire_lock(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -594,15 +605,17 @@ async fn acquire_lock(
}
}
/// Force-acquire a moderation lock on a project (moderator override).
/// Override a moderation lock.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockAcquireResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[post("/lock/{project_id}/override")]
async fn override_lock(
pub async fn override_lock(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -639,15 +652,17 @@ async fn override_lock(
}))
}
/// Check the lock status for a project
/// Get moderation lock status.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockStatusResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[get("/lock/{project_id}")]
async fn get_lock_status(
pub async fn get_lock_status(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -699,15 +714,17 @@ async fn get_lock_status(
}
}
/// Release a moderation lock on a project
/// Release a moderation lock.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = LockReleaseResponse),
(status = NOT_FOUND, description = "Project not found")
)
)]
#[delete("/lock/{project_id}")]
async fn release_lock(
pub async fn release_lock(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -740,12 +757,14 @@ async fn release_lock(
Ok(web::Json(LockReleaseResponse { success: released }))
}
/// Release a moderation lock using credentials in the request body.
/// Release a moderation lock by beacon.
///
/// For use with `navigator.sendBeacon`, which cannot set `Authorization` or send `DELETE`.
/// The body must be `text/plain` containing the same token value as the `Authorization` header
/// (optional `Bearer ` prefix). This avoids a CORS preflight compared to `application/json`.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
request_body(
content = String,
description = "Token value (same as Authorization header)",
@@ -757,7 +776,7 @@ async fn release_lock(
)
)]
#[post("/lock/{project_id}/release")]
async fn release_lock_beacon(
pub async fn release_lock_beacon(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -811,15 +830,17 @@ async fn release_lock_beacon(
Ok(web::Json(LockReleaseResponse { success: released }))
}
/// Delete all moderation locks (admin only)
/// Delete all moderation locks.
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
responses(
(status = OK, body = DeleteAllLocksResponse),
(status = UNAUTHORIZED, description = "Not an admin")
)
)]
#[delete("/locks")]
async fn delete_all_locks(
pub async fn delete_all_locks(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -37,7 +37,7 @@ use crate::{
};
use eyre::eyre;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(search_projects)
.service(get_project_report)
.service(get_report)
@@ -193,13 +193,15 @@ pub enum FlagReason {
Delphi,
}
/// Get info on an issue in a Delphi report.
/// Get a Delphi report issue.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(FileIssue)))
)]
#[get("/issue/{issue_id}")]
async fn get_issue(
pub async fn get_issue(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -252,13 +254,15 @@ async fn get_issue(
Ok(web::Json(row.data.0))
}
/// Get info on a specific report for a project.
/// Get a project technical report.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(FileReport)))
)]
#[get("/report/{id}")]
async fn get_report(
pub async fn get_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -662,13 +666,15 @@ async fn fetch_project_reports(
Ok(project_reports)
}
/// Searches all projects which are awaiting technical review.
/// Search projects awaiting technical review.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(Vec<SearchResponse>)))
responses((status = OK, body = SearchResponse))
)]
#[post("/search")]
async fn search_projects(
pub async fn search_projects(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -872,13 +878,15 @@ async fn search_projects(
}))
}
/// Gets the technical review report for a specific project.
/// Get a project technical review report.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = inline(ProjectReportResponse)))
)]
#[get("/project/{id}")]
async fn get_project_report(
pub async fn get_project_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -963,18 +971,20 @@ pub struct SubmitReport {
pub message: Option<String>,
}
/// Submits a verdict for a project based on its technical reports.
/// Submit a technical review verdict.
///
/// Before this is called, all issues for this project's reports must have been
/// marked as either safe or unsafe. Otherwise, this will error with
/// [`ApiError::TechReviewIssuesWithNoVerdict`], providing the issue IDs which
/// are still unmarked.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = NO_CONTENT))
)]
#[post("/submit/{project_id}")]
async fn submit_report(
pub async fn submit_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -1163,16 +1173,18 @@ pub struct UpdateIssue {
pub verdict: DelphiVerdict,
}
/// Updates the state of a technical review issue detail.
/// Update technical review issue details.
///
/// This will not automatically reject the project for malware, but just flag
/// this issue with a verdict.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = NO_CONTENT))
)]
#[patch("/issue-detail")]
async fn update_issue_details(
pub async fn update_issue_details(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -1273,11 +1285,15 @@ pub struct AddReport {
pub file_id: FileId,
}
/// Adds a file to the technical review queue by adding an empty report, if one
/// Add a technical review report.
/// does not already exist for it.
#[utoipa::path]
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
responses((status = OK, body = DelphiReportId))
)]
#[put("/report")]
async fn add_report(
pub async fn add_report(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+7 -2
View File
@@ -6,12 +6,17 @@ use crate::{
queue::payouts::PayoutsQueue, routes::ApiError, util::error::Context,
};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_bank_details);
}
/// Get bank details.
#[utoipa::path(
tag = "mural",
responses((status = OK, body = serde_json::Value))
)]
#[get("/mural/bank-details")]
async fn get_bank_details(
pub async fn get_bank_details(
payouts_queue: web::Data<PayoutsQueue>,
) -> Result<web::Json<muralpay::BankDetailsResponse>, ApiError> {
let mural = payouts_queue.muralpay.load();
+26 -14
View File
@@ -22,20 +22,22 @@ use crate::util::validate::validation_errors_to_string;
use serde::Deserialize;
use validator::Validate;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_pats);
cfg.service(create_pat);
cfg.service(edit_pat);
cfg.service(delete_pat);
}
/// List personal access tokens.
#[utoipa::path(
tag = "personal access tokens",
get,
operation_id = "getPats",
responses(
(status = 200, description = "List of PATs"),
(status = 401, description = "Unauthorized")
),
operation_id = "getPats",
responses(
(status = 200, description = "List of PATs", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_READ"]))
)]
#[get("/pat")]
@@ -82,14 +84,16 @@ pub struct NewPersonalAccessToken {
pub expires: DateTime<Utc>,
}
/// Create a personal access token.
#[utoipa::path(
tag = "personal access tokens",
post,
operation_id = "createPat",
responses(
(status = 200, description = "PAT created"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
operation_id = "createPat",
responses(
(status = 200, description = "PAT created", body = serde_json::Value),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["PAT_CREATE"]))
)]
#[post("/pat")]
@@ -185,10 +189,14 @@ pub struct ModifyPersonalAccessToken {
pub expires: Option<DateTime<Utc>>,
}
/// Update a personal access token.
#[utoipa::path(
tag = "personal access tokens",
patch,
operation_id = "editPat",
params(("id" = String, Path, description = "The PAT ID")),
params(
("id" = String, Path, description = "The PAT ID")
),
responses(
(status = 204, description = "PAT updated"),
(status = 400, description = "Invalid input"),
@@ -293,10 +301,14 @@ pub async fn edit_pat(
Ok(HttpResponse::NoContent().finish())
}
/// Delete a personal access token.
#[utoipa::path(
tag = "personal access tokens",
delete,
operation_id = "deletePat",
params(("id" = String, Path, description = "The PAT ID")),
params(
("id" = String, Path, description = "The PAT ID")
),
responses(
(status = 204, description = "PAT deleted"),
(status = 401, description = "Unauthorized")
+15 -5
View File
@@ -5,20 +5,30 @@ use crate::{
};
use actix_web::{delete, get, web};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(tasks).service(tasks_cancel);
}
#[utoipa::path]
#[get("tasks", guard = "admin_key_guard")]
/// List search tasks.
#[utoipa::path(
context_path = "/search-management",
tag = "search",
responses((status = OK, body = serde_json::Value))
)]
#[get("/tasks", guard = "admin_key_guard")]
pub async fn tasks(
search: web::Data<dyn SearchBackend>,
) -> Result<web::Json<serde_json::Value>, ApiError> {
Ok(web::Json(search.tasks().await.map_err(ApiError::Internal)?))
}
#[utoipa::path]
#[delete("tasks", guard = "admin_key_guard")]
/// Cancel search tasks.
#[utoipa::path(
context_path = "/search-management",
tag = "search",
responses((status = NO_CONTENT))
)]
#[delete("/tasks", guard = "admin_key_guard")]
pub async fn tasks_cancel(
search: web::Data<dyn SearchBackend>,
body: web::Json<TasksCancelFilter>,
@@ -12,7 +12,7 @@ use crate::{
util::error::Context,
};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(ping_minecraft_java);
}
@@ -22,7 +22,12 @@ pub struct PingRequest {
pub timeout_ms: Option<u64>,
}
#[utoipa::path]
/// Ping Minecraft server.
#[utoipa::path(
context_path = "/server-ping",
tag = "server ping",
responses((status = NO_CONTENT))
)]
#[post("/minecraft-java")]
pub async fn ping_minecraft_java(
req: HttpRequest,
+24 -13
View File
@@ -19,9 +19,9 @@ use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use woothee::parser::Parser;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
utoipa_actix_web::scope("/session")
web::scope("/session")
.service(list)
.service(delete)
.service(refresh),
@@ -133,13 +133,16 @@ pub async fn issue_session(
Ok(session)
}
/// List sessions.
#[utoipa::path(
context_path = "/session",
tag = "sessions",
get,
operation_id = "listSessions",
responses(
(status = 200, description = "List of active sessions"),
(status = 401, description = "Unauthorized")
),
operation_id = "listSessions",
responses(
(status = 200, description = "List of active sessions", body = serde_json::Value),
(status = 401, description = "Unauthorized")
),
security(("bearer_auth" = ["SESSION_READ"]))
)]
#[get("/list")]
@@ -178,10 +181,15 @@ pub async fn list(
Ok(HttpResponse::Ok().json(sessions))
}
/// Delete a session.
#[utoipa::path(
context_path = "/session",
tag = "sessions",
delete,
operation_id = "deleteSession",
params(("id" = String, Path, description = "The session ID")),
params(
("id" = String, Path, description = "The session ID")
),
responses(
(status = 204, description = "Session deleted"),
(status = 401, description = "Unauthorized")
@@ -228,13 +236,16 @@ pub async fn delete(
Ok(HttpResponse::NoContent().body(""))
}
/// Refresh a session.
#[utoipa::path(
context_path = "/session",
tag = "sessions",
post,
operation_id = "refreshSession",
responses(
(status = 200, description = "Session refreshed"),
(status = 401, description = "Unauthorized")
)
operation_id = "refreshSession",
responses(
(status = 200, description = "Session refreshed", body = serde_json::Value),
(status = 401, description = "Unauthorized")
)
)]
#[post("/refresh")]
pub async fn refresh(
@@ -35,7 +35,7 @@ use std::sync::atomic::Ordering;
use tokio::sync::oneshot::error::TryRecvError;
use tokio::time::{Duration, sleep};
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(ws_init);
}
@@ -45,7 +45,12 @@ struct LauncherHeartbeatInit {
}
// TODO: Move launcher-specific tunnel traffic to a proper launcher websocket endpoint.
#[get("launcher_socket")]
/// Start launcher socket.
#[utoipa::path(
tag = "statuses",
responses((status = 101))
)]
#[get("/launcher_socket")]
pub async fn ws_init(
req: HttpRequest,
pool: Data<PgPool>,
+40 -5
View File
@@ -18,7 +18,7 @@ use quick_xml::escape::escape;
use std::collections::HashSet;
use yaserde::YaSerialize;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(maven_metadata);
cfg.service(version_file_sha512);
cfg.service(version_file_sha1);
@@ -70,7 +70,12 @@ pub struct MavenPom {
description: String,
}
#[get("maven/modrinth/{id}/maven-metadata.xml")]
#[utoipa::path(
tag = "maven",
params(("id" = String, Path)),
responses((status = OK, body = String, content_type = "text/xml"))
)]
#[get("/maven/modrinth/{id}/maven-metadata.xml")]
pub async fn maven_metadata(
req: HttpRequest,
params: web::Path<(String,)>,
@@ -279,8 +284,20 @@ fn find_file<'a>(
None
}
#[utoipa::path(
tag = "maven",
params(
("id" = String, Path),
("versionnum" = String, Path),
("file" = String, Path)
),
responses(
(status = OK, body = String, content_type = "text/xml"),
(status = TEMPORARY_REDIRECT)
)
)]
#[route(
"maven/modrinth/{id}/{versionnum}/{file}",
"/maven/modrinth/{id}/{versionnum}/{file}",
method = "GET",
method = "HEAD"
)]
@@ -349,7 +366,16 @@ pub async fn version_file(
Err(ApiError::NotFound)
}
#[get("maven/modrinth/{id}/{versionnum}/{file}.sha1")]
#[utoipa::path(
tag = "maven",
params(
("id" = String, Path),
("versionnum" = String, Path),
("file" = String, Path)
),
responses((status = OK, body = String))
)]
#[get("/maven/modrinth/{id}/{versionnum}/{file}.sha1")]
pub async fn version_file_sha1(
req: HttpRequest,
params: web::Path<(String, String, String)>,
@@ -396,7 +422,16 @@ pub async fn version_file_sha1(
))
}
#[get("maven/modrinth/{id}/{versionnum}/{file}.sha512")]
#[utoipa::path(
tag = "maven",
params(
("id" = String, Path),
("versionnum" = String, Path),
("file" = String, Path)
),
responses((status = OK, body = String))
)]
#[get("/maven/modrinth/{id}/{versionnum}/{file}.sha512")]
pub async fn version_file_sha512(
req: HttpRequest,
params: web::Path<(String, String, String)>,
+119 -19
View File
@@ -8,6 +8,8 @@ use actix_web::http::StatusCode;
use actix_web::{HttpResponse, web};
use futures::FutureExt;
use serde_json::json;
use utoipa::openapi::extensions::ExtensionsBuilder;
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
pub mod debug;
pub mod internal;
@@ -23,7 +25,124 @@ mod updates;
pub use self::not_found::not_found;
// utoipa-specific struct to use a value_type for docs.
/// A sha1 or sha512 hash.
pub struct FileHash;
impl utoipa::PartialSchema for FileHash {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::Type::String)
.min_length(Some(40))
.max_length(Some(128))
.pattern(Some("^([A-Fa-f0-9]{40}|[A-Fa-f0-9]{128})$"))
.examples([serde_json::json!(
"2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
)])
.description(Some("A sha1 or sha512 hash."))
.build()
.into()
}
}
impl utoipa::ToSchema for FileHash {}
// utoipa-specific struct to use a value_type for docs.
/// A hashing algorithm (sha1 or sha256)
pub struct HashAlgorithm;
impl utoipa::PartialSchema for HashAlgorithm {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::ObjectBuilder::new()
.enum_values(Some([
serde_json::json!("sha1"),
serde_json::json!("sha512"),
]))
.examples([serde_json::json!("sha1")])
.description(Some("A supported hashing algorithm."))
.build()
.into()
}
}
impl utoipa::ToSchema for HashAlgorithm {}
pub(crate) fn prefix_openapi_paths(
openapi: &mut utoipa::openapi::OpenApi,
prefix: &str,
should_skip: impl Fn(&str) -> bool,
) {
let paths = std::mem::take(&mut openapi.paths.paths);
openapi.paths.paths = paths
.into_iter()
.map(|(path, item)| {
if should_skip(&path) || path.starts_with(prefix) {
(path, item)
} else {
(format!("{prefix}{}", normalize_openapi_path(&path)), item)
}
})
.collect();
}
fn normalize_openapi_path(path: &str) -> String {
if path.starts_with('/') {
path.to_string()
} else {
format!("/{path}")
}
}
pub(crate) 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::Http(bearer_auth),
);
}
}
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,25 +177,6 @@ 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/")),
);
}
/// Error when calling an HTTP endpoint.
+12 -3
View File
@@ -17,11 +17,11 @@ use crate::queue::session::AuthQueue;
use super::ApiError;
pub fn config(cfg: &mut web::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(forge_updates);
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct NeoForge {
#[serde(default = "default_neoforge")]
pub neoforge: String,
@@ -31,7 +31,16 @@ fn default_neoforge() -> String {
"none".into()
}
#[get("{id}/forge_updates.json")]
#[utoipa::path(
context_path = "/updates",
tag = "updates",
params(
("id" = String, Path),
("neoforge" = Option<String>, Query)
),
responses((status = OK))
)]
#[get("/{id}/forge_updates.json")]
pub async fn forge_updates(
req: HttpRequest,
web::Query(neo): web::Query<NeoForge>,
+7 -6
View File
@@ -1,5 +1,6 @@
mod moderation;
pub(crate) mod moderation;
mod notifications;
mod openapi;
pub(crate) mod project_creation;
mod projects;
mod reports;
@@ -14,17 +15,17 @@ mod versions;
pub use super::ApiError;
use crate::util::cors::default_cors;
use actix_web::web;
pub use openapi::ApiDoc;
pub fn utoipa_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
) {
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
utoipa_actix_web::scope("/v2")
web::scope("/v2")
.wrap(default_cors())
.configure(super::internal::admin::config)
.configure(super::internal::session::config)
.configure(super::internal::flows::config)
.configure(super::internal::pats::config)
.configure(super::internal::admin::config)
.configure(moderation::config)
.configure(notifications::config)
.configure(project_creation::config)
+7 -9
View File
@@ -8,8 +8,8 @@ use crate::{database::redis::RedisPool, routes::v2_reroute};
use actix_web::{HttpRequest, HttpResponse, get, web};
use serde::Deserialize;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(utoipa_actix_web::scope("/moderation").service(get_projects));
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(web::scope("/moderation").service(get_projects));
}
#[derive(Deserialize)]
@@ -22,19 +22,17 @@ fn default_count() -> u16 {
100
}
/// Get projects in the moderation queue.
/// List projects in the moderation queue.
#[utoipa::path(
context_path = "/moderation",
tag = "v2 moderation",
get,
operation_id = "getModerationProjects",
params(
(
"count" = Option<u16>,
Query,
description = "Maximum number of projects to return"
)
("count" = Option<u16>, Query, description = "Maximum number of projects to return")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = Vec<LegacyProject>),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
+31 -28
View File
@@ -10,12 +10,12 @@ use crate::routes::v3;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::{Deserialize, Serialize};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(notifications_get);
cfg.service(notifications_delete);
cfg.service(notifications_read);
cfg.service(
utoipa_actix_web::scope("/notification")
web::scope("/notification")
.service(notification_get)
.service(notification_read)
.service(notification_delete),
@@ -27,19 +27,16 @@ 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(
(
"ids" = String,
Query,
description = "The JSON array of notification IDs"
)
("ids" = String, Query, description = "The JSON array of notification IDs")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = Vec<LegacyNotification>),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -80,13 +77,17 @@ pub async fn notifications_get(
}
}
/// Get a notification by ID.
/// Get a notification by ID.
#[utoipa::path(
context_path = "/notification",
tag = "notifications",
get,
operation_id = "getNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
params(
("id" = NotificationId, Path, description = "The ID of the notification")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = LegacyNotification),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -124,11 +125,15 @@ pub async fn notification_get(
}
}
/// Mark a notification as read.
/// Mark a notification as read.
#[utoipa::path(
context_path = "/notification",
tag = "notifications",
patch,
operation_id = "readNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
params(
("id" = NotificationId, Path, description = "The ID of the notification")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
@@ -156,11 +161,15 @@ pub async fn notification_read(
.or_else(v2_reroute::flatten_404_error)
}
/// Delete a notification by ID.
/// Delete a notification by ID.
#[utoipa::path(
context_path = "/notification",
tag = "notifications",
delete,
operation_id = "deleteNotification",
params(("id" = NotificationId, Path, description = "The ID of the notification")),
params(
("id" = NotificationId, Path, description = "The ID of the notification")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
@@ -194,16 +203,13 @@ 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(
(
"ids" = String,
Query,
description = "The JSON array of notification IDs"
)
("ids" = String, Query, description = "The JSON array of notification IDs")
),
responses(
(status = 204, description = "Expected response to a valid request"),
@@ -238,16 +244,13 @@ 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(
(
"ids" = String,
Query,
description = "The JSON array of notification IDs"
)
("ids" = String, Query, description = "The JSON array of notification IDs")
),
responses(
(status = 204, description = "Expected response to a valid request"),
+235
View File
@@ -0,0 +1,235 @@
use crate::routes::SecurityAddon;
use crate::routes::prefix_openapi_paths;
use crate::routes::v2::{
notifications, project_creation, projects, reports, statistics, tags,
teams, threads, users, version_creation, version_file, versions,
};
use utoipa::openapi::extensions::{Extensions, ExtensionsBuilder};
use utoipa::openapi::path::{Operation, PathItem};
use utoipa::openapi::schema::{AdditionalProperties, AllOf, Schema};
use utoipa::openapi::{RefOr, Response};
#[derive(utoipa::OpenApi)]
#[openapi(
info(
title = "Modrinth API v2",
version = "2.0.0",
description = include_str!("../../api_v2_description.md"),
),
paths(
notifications::notifications_get,
notifications::notifications_read,
notifications::notifications_delete,
notifications::notification_get,
notifications::notification_read,
notifications::notification_delete,
project_creation::project_create,
projects::project_search,
projects::random_projects_get,
projects::projects_get,
projects::projects_edit,
projects::project_get,
projects::project_get_check,
projects::project_edit,
projects::project_icon_edit,
projects::delete_project_icon,
projects::add_gallery_item,
projects::edit_gallery_item,
projects::delete_gallery_item,
projects::project_delete,
projects::project_follow,
projects::project_unfollow,
projects::dependency_list,
reports::report_create,
reports::reports,
reports::reports_get,
reports::report_get,
reports::report_edit,
reports::report_delete,
statistics::get_stats,
tags::category_list,
tags::loader_list,
tags::game_version_list,
tags::license_list,
tags::license_text,
tags::donation_platform_list,
tags::report_type_list,
tags::project_type_list,
tags::side_type_list,
teams::teams_get,
teams::team_members_get_project,
teams::team_members_get,
teams::join_team,
teams::add_team_member,
teams::edit_team_member,
teams::transfer_ownership,
teams::remove_team_member,
threads::threads_get,
threads::thread_get,
threads::thread_send_message,
threads::message_delete,
users::users_get,
users::user_auth_get,
users::user_get,
users::projects_list,
users::user_edit,
users::user_icon_edit,
users::user_icon_delete,
users::user_delete,
users::user_follows,
users::user_notifications,
version_file::get_version_from_hash,
version_file::download_version,
version_file::delete_file,
version_file::get_update_from_hash,
version_file::get_projects_from_hashes,
version_file::get_versions_from_hashes,
version_file::update_files,
version_file::update_files_many,
version_file::update_individual_files,
versions::versions_get,
version_creation::version_create,
versions::version_get,
versions::version_edit,
versions::version_delete,
versions::version_list,
versions::version_project_get,
version_creation::upload_file_to_version,
),
modifiers(&V2PathModifier, &SecurityAddon, &V2Modifier)
)]
pub struct ApiDoc;
const FILE_HASH_EXAMPLE: &str = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed";
const FILE_HASH_MAP_RESPONSE_OPERATION_IDS: [&str; 5] = [
"versionsFromHashes",
"projectsFromHashes",
"getLatestVersionsFromHashes",
"getLatestVersionsFromHashesMany",
"getLatestVersionsFromHashesIndividual",
];
struct V2PathModifier;
impl utoipa::Modify for V2PathModifier {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
prefix_openapi_paths(openapi, "/v2", |_| false);
}
}
struct V2Modifier;
impl utoipa::Modify for V2Modifier {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
// We need to add an extension field to some routes to give a clearer example of a file hash in responses. Else,
// it'll render as "additionalProperty"
for path_item in openapi.paths.paths.values_mut() {
add_file_hash_map_response_extension_to_path_item(path_item);
}
}
}
fn add_file_hash_map_response_extension_to_path_item(path_item: &mut PathItem) {
add_file_hash_map_response_extension(&mut path_item.get);
add_file_hash_map_response_extension(&mut path_item.put);
add_file_hash_map_response_extension(&mut path_item.post);
add_file_hash_map_response_extension(&mut path_item.delete);
add_file_hash_map_response_extension(&mut path_item.options);
add_file_hash_map_response_extension(&mut path_item.head);
add_file_hash_map_response_extension(&mut path_item.patch);
add_file_hash_map_response_extension(&mut path_item.trace);
}
fn add_file_hash_map_response_extension(operation: &mut Option<Operation>) {
let Some(operation) = operation else {
return;
};
let Some(operation_id) = operation.operation_id.as_deref() else {
return;
};
if !FILE_HASH_MAP_RESPONSE_OPERATION_IDS.contains(&operation_id) {
return;
}
let Some(RefOr::T(response)) = operation.responses.responses.get_mut("200")
else {
return;
};
add_file_hash_map_response_extension_to_response(response);
}
fn add_file_hash_map_response_extension_to_response(response: &mut Response) {
let Some(content) = response.content.get_mut("application/json") else {
return;
};
let Some(RefOr::T(Schema::Object(schema))) = content.schema.as_mut() else {
return;
};
let Some(additional_properties) = schema.additional_properties.as_mut()
else {
return;
};
match additional_properties.as_mut() {
AdditionalProperties::RefOr(RefOr::T(schema)) => {
add_file_hash_map_response_extension_to_schema(schema);
}
AdditionalProperties::RefOr(RefOr::Ref(reference)) => {
let mut all_of = AllOf::new();
all_of.items.push(RefOr::Ref(reference.clone()));
let mut schema = Schema::AllOf(all_of);
add_file_hash_map_response_extension_to_schema(&mut schema);
**additional_properties =
AdditionalProperties::RefOr(RefOr::T(schema));
}
AdditionalProperties::FreeForm(_) => {}
}
}
fn add_file_hash_map_response_extension_to_schema(schema: &mut Schema) {
match schema {
Schema::Array(schema) => {
add_file_hash_map_response_extension_to_extensions(
&mut schema.extensions,
);
}
Schema::Object(schema) => {
add_file_hash_map_response_extension_to_extensions(
&mut schema.extensions,
);
}
Schema::OneOf(schema) => {
add_file_hash_map_response_extension_to_extensions(
&mut schema.extensions,
);
}
Schema::AllOf(schema) => {
add_file_hash_map_response_extension_to_extensions(
&mut schema.extensions,
);
}
Schema::AnyOf(schema) => {
add_file_hash_map_response_extension_to_extensions(
&mut schema.extensions,
);
}
_ => {}
}
}
fn add_file_hash_map_response_extension_to_extensions(
extensions: &mut Option<Extensions>,
) {
let file_hash_extension = ExtensionsBuilder::new()
.add("x-additionalPropertiesName", FILE_HASH_EXAMPLE)
.build();
if let Some(extensions) = extensions {
extensions.merge(file_hash_extension);
} else {
*extensions = Some(file_hash_extension);
}
}
@@ -25,7 +25,7 @@ use validator::Validate;
use super::version_creation::InitialVersionData;
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(project_create);
}
@@ -134,8 +134,9 @@ struct ProjectCreateData {
pub organization_id: Option<models::ids::OrganizationId>,
}
/// Create a new project with initial versions.
/// Create a new project with initial versions.
#[utoipa::path(
tag = "project creation",
post,
operation_id = "createProject",
request_body(
@@ -143,7 +144,7 @@ struct ProjectCreateData {
description = "Multipart payload containing `data` and uploaded files"
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = LegacyProject),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
+110 -133
View File
@@ -19,13 +19,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)
@@ -39,7 +39,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),
@@ -47,39 +47,25 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
);
}
/// Search projects.
/// Search projects.
#[utoipa::path(
tag = "search",
get,
operation_id = "searchProjects",
params(
(
"query" = Option<String>,
Query,
description = "The query to search for"
),
(
"facets" = Option<String>,
Query,
description = "Search facets JSON"
),
(
"index" = Option<String>,
Query,
description = "Search index to use"
),
(
"offset" = Option<String>,
Query,
description = "Search result offset"
),
(
"limit" = Option<String>,
Query,
description = "Maximum number of search results"
)
("query" = Option<String>, Query, description = "The query to search for"),
("facets" = Option<String>, Query, description = "Search facets JSON"),
("index" = Option<String>, Query, description = "Search index to use"),
("offset" = Option<String>, Query, description = "Search result offset"),
("limit" = Option<String>, Query, description = "Maximum number of search results"),
("show_metadata" = Option<bool>, Query, description = "Whether to include search metadata"),
("typesense_config" = Option<String>, Query, description = "Typesense request configuration"),
("new_filters" = Option<String>, Query, description = "Search filters"),
("filters" = Option<String>, Query, description = "Legacy search filters"),
("version" = Option<String>, Query, description = "Legacy search version")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = LegacySearchResults),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -180,19 +166,16 @@ pub struct RandomProjects {
pub count: u32,
}
/// Get random projects.
/// Get random projects.
#[utoipa::path(
tag = "projects",
get,
operation_id = "randomProjects",
params(
(
"count" = u32,
Query,
description = "Number of projects to return"
)
("count" = u32, Query, description = "Number of projects to return")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = Vec<LegacyProject>),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -223,18 +206,15 @@ 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(
(
"ids" = String,
Query,
description = "The JSON array of project IDs or slugs"
)
("ids" = String, Query, 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<LegacyProject>))
)]
#[get("/projects")]
pub async fn projects_get(
@@ -267,13 +247,17 @@ pub async fn projects_get(
}
}
/// Get a project by ID or slug.
/// Get a project by ID or slug.
#[utoipa::path(
context_path = "/project",
tag = "projects",
get,
operation_id = "getProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
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)"
@@ -316,13 +300,17 @@ 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(
context_path = "/project",
tag = "projects",
get,
operation_id = "checkProjectValidity",
params(("id" = String, Path, description = "The ID or slug of the project")),
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)"
@@ -347,13 +335,17 @@ struct DependencyInfo {
pub versions: Vec<LegacyVersion>,
}
/// Get dependency projects and versions for a project.
/// Get dependency projects and versions for a project.
#[utoipa::path(
context_path = "/project/{project_id}",
tag = "projects",
get,
operation_id = "getDependencies",
params(("id" = String, Path, description = "The ID or slug of the project")),
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)"
@@ -507,14 +499,18 @@ pub struct EditProject {
pub monetization_status: Option<MonetizationStatus>,
}
/// Modify a project.
/// Update a project.
#[utoipa::path(
context_path = "/project",
tag = "projects",
patch,
operation_id = "modifyProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
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)"
@@ -763,20 +759,17 @@ pub struct BulkEditProject {
pub discord_url: Option<Option<String>>,
}
/// Bulk-edit multiple projects.
/// Bulk-edit multiple projects.
#[utoipa::path(
tag = "projects",
patch,
operation_id = "patchProjects",
params(
(
"ids" = String,
Query,
description = "The JSON array of project IDs or slugs"
)
("ids" = String, Query, description = "The JSON array of project IDs or slugs")
),
request_body = BulkEditProject,
responses(
(status = 204, description = "Expected response to a valid request"),
(status = NO_CONTENT, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
(
status = 401,
@@ -889,17 +882,15 @@ pub struct Extension {
pub ext: String,
}
/// Change a project's icon.
/// Change a project's icon.
#[utoipa::path(
context_path = "/project",
tag = "projects",
patch,
operation_id = "changeProjectIcon",
params(
("id" = String, Path, description = "The ID or slug of the project"),
(
"ext" = String,
Query,
description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)"
)
("ext" = String, Query, description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)")
),
request_body(
content(
@@ -912,7 +903,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"]))
@@ -946,13 +937,17 @@ 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(
context_path = "/project",
tag = "projects",
delete,
operation_id = "deleteProjectIcon",
params(("id" = String, Path, description = "The ID or slug of the project")),
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,
@@ -995,37 +990,19 @@ pub struct GalleryCreateQuery {
pub ordering: Option<i64>,
}
/// Add a gallery image to a project.
/// Add a gallery image to a project.
#[utoipa::path(
context_path = "/project",
tag = "projects",
post,
operation_id = "addGalleryImage",
params(
("id" = String, Path, description = "The ID or slug of the project"),
(
"ext" = String,
Query,
description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)"
),
(
"featured" = bool,
Query,
description = "Whether this image is featured"
),
(
"title" = Option<String>,
Query,
description = "Image title"
),
(
"description" = Option<String>,
Query,
description = "Image description"
),
(
"ordering" = Option<i64>,
Query,
description = "Image ordering"
)
("ext" = String, Query, description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)"),
("featured" = bool, Query, description = "Whether this image is featured"),
("title" = Option<String>, Query, description = "Image title"),
("description" = Option<String>, Query, description = "Image description"),
("ordering" = Option<i64>, Query, description = "Image ordering")
),
request_body(
content(
@@ -1038,7 +1015,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,
@@ -1109,36 +1086,22 @@ pub struct GalleryEditQuery {
pub ordering: Option<i64>,
}
/// Modify a gallery image.
/// Update a gallery image.
#[utoipa::path(
context_path = "/project",
tag = "projects",
patch,
operation_id = "modifyGalleryImage",
params(
("id" = String, Path, description = "The ID or slug of the project"),
("url" = String, Query, description = "URL of the image to edit"),
(
"featured" = Option<bool>,
Query,
description = "Whether this image is featured"
),
(
"title" = Option<Option<String>>,
Query,
description = "Image title"
),
(
"description" = Option<Option<String>>,
Query,
description = "Image description"
),
(
"ordering" = Option<i64>,
Query,
description = "Image ordering"
)
("featured" = Option<bool>, Query, description = "Whether this image is featured"),
("title" = Option<Option<String>>, Query, description = "Image title"),
("description" = Option<Option<String>>, Query, description = "Image description"),
("ordering" = Option<i64>, Query, description = "Image ordering")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = NO_CONTENT, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -1183,8 +1146,10 @@ pub struct GalleryDeleteQuery {
pub url: String,
}
/// Delete a gallery image.
/// Delete a gallery image.
#[utoipa::path(
context_path = "/project",
tag = "projects",
delete,
operation_id = "deleteGalleryImage",
params(
@@ -1192,7 +1157,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,
@@ -1225,13 +1190,17 @@ 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(
context_path = "/project",
tag = "projects",
delete,
operation_id = "deleteProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
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,
@@ -1263,13 +1232,17 @@ pub async fn project_delete(
.or_else(v2_reroute::flatten_404_error)
}
/// Follow a project.
/// Follow a project.
#[utoipa::path(
context_path = "/project",
tag = "projects",
post,
operation_id = "followProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
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,
@@ -1292,13 +1265,17 @@ pub async fn project_follow(
.or_else(v2_reroute::flatten_404_error)
}
/// Unfollow a project.
/// Unfollow a project.
#[utoipa::path(
context_path = "/project",
tag = "projects",
delete,
operation_id = "unfollowProject",
params(("id" = String, Path, description = "The ID or slug of the project")),
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,
+29 -24
View File
@@ -8,7 +8,7 @@ use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use serde::Deserialize;
use validator::Validate;
pub fn config(cfg: &mut 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);
@@ -17,12 +17,14 @@ 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",
request_body = v3::reports::CreateReport,
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,
@@ -69,19 +71,16 @@ 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(
(
"count" = Option<u16>,
Query,
description = "Maximum number of reports to return"
)
("count" = Option<u16>, Query, description = "Maximum number of reports to return")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = Vec<LegacyReport>),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -131,19 +130,16 @@ 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(
(
"ids" = String,
Query,
description = "The JSON array of report IDs"
)
("ids" = String, Query, description = "The JSON array of report IDs")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = Vec<LegacyReport>),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -184,13 +180,16 @@ 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")),
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)"
@@ -232,11 +231,14 @@ pub struct EditReport {
pub closed: Option<bool>,
}
/// 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")),
params(
("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")
),
request_body = EditReport,
responses(
(status = 204, description = "Expected response to a valid request"),
@@ -278,11 +280,14 @@ 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")),
params(
("id" = crate::models::ids::ReportId, Path, description = "The ID of the report")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
+3 -2
View File
@@ -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);
}
@@ -17,8 +17,9 @@ pub struct V2Stats {
pub files: Option<i64>,
}
/// Get aggregate instance statistics.
/// Get aggregate instance statistics.
#[utoipa::path(
tag = "statistics",
get,
operation_id = "statistics",
responses(
+34 -22
View File
@@ -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)
@@ -35,8 +35,10 @@ pub struct CategoryData {
pub header: String,
}
/// Get the list of project categories.
/// List project categories.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "categoryList",
responses(
@@ -81,8 +83,10 @@ pub struct LoaderData {
pub supported_project_types: Vec<String>,
}
/// Get the list of loaders.
/// List loaders.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "loaderList",
responses(
@@ -155,21 +159,15 @@ pub struct GameVersionQuery {
major: Option<bool>,
}
/// Get the list of game versions.
/// List game versions.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "versionList",
params(
(
"type" = Option<String>,
Query,
description = "Optional game version type filter"
),
(
"major" = Option<bool>,
Query,
description = "Whether to return only major versions"
)
("type" = Option<String>, Query, description = "Optional game version type filter"),
("major" = Option<bool>, Query, description = "Whether to return only major versions")
),
responses(
(
@@ -239,8 +237,10 @@ pub struct License {
pub name: String,
}
/// Get SPDX license identifiers and names.
/// List SPDX license identifiers and names.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "licenseList",
responses(
@@ -278,11 +278,15 @@ pub struct LicenseText {
pub body: String,
}
/// Get full license text by SPDX ID.
/// Get full license text by SPDX ID.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "licenseText",
params(("id" = String, Path, description = "The license ID to get the text for")),
params(
("id" = String, Path, description = "The license ID to get the text for")
),
responses(
(
status = 200,
@@ -325,8 +329,10 @@ pub struct DonationPlatformQueryData {
pub name: String,
}
/// Get available donation platforms.
/// List donation platforms.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "donationPlatformList",
responses(
@@ -383,8 +389,10 @@ pub async fn donation_platform_list(
.or_else(v2_reroute::flatten_404_error)
}
/// Get valid report types.
/// List valid report types.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "reportTypeList",
responses(
@@ -406,8 +414,10 @@ pub async fn report_type_list(
.or_else(v2_reroute::flatten_404_error)
}
/// Get valid project types.
/// List valid project types.
#[utoipa::path(
context_path = "/tag",
tag = "tags",
get,
operation_id = "projectTypeList",
responses(
@@ -429,8 +439,10 @@ 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(
context_path = "/tag",
tag = "tags",
get,
operation_id = "sideTypeList",
responses(
+48 -29
View File
@@ -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)
@@ -30,13 +30,17 @@ 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(
context_path = "/project",
tag = "teams",
get,
operation_id = "getProjectTeamMembers",
params(("id" = String, Path, description = "The ID or slug of the project")),
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<LegacyTeamMember>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
@@ -74,12 +78,16 @@ 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(
context_path = "/team",
tag = "teams",
get,
operation_id = "getTeamMembers",
params(("id" = TeamId, Path, description = "The ID of the team")),
responses((status = 200, description = "Expected response to a valid request")),
params(
("id" = TeamId, Path, description = "The ID of the team")
),
responses((status = 200, description = "Expected response to a valid request", body = Vec<LegacyTeamMember>)),
security(("bearer_auth" = ["PROJECT_READ"]))
)]
#[get("/{id}/members")]
@@ -112,12 +120,15 @@ 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")),
responses((status = 200, description = "Expected response to a valid request"))
params(
("ids" = String, Query, description = "The JSON array of team IDs")
),
responses((status = 200, description = "Expected response to a valid request", body = Vec<Vec<LegacyTeamMember>>))
)]
#[get("/teams")]
pub async fn teams_get(
@@ -154,11 +165,15 @@ pub async fn teams_get(
}
}
/// Join a team with a pending invite.
/// Join a team with a pending invite.
#[utoipa::path(
context_path = "/team",
tag = "teams",
post,
operation_id = "joinTeam",
params(("id" = TeamId, Path, description = "The ID of the team")),
params(
("id" = TeamId, Path, description = "The ID of the team")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
@@ -210,11 +225,15 @@ pub struct NewTeamMember {
pub ordering: i64,
}
/// Add a member to a team.
/// Add a member to a team.
#[utoipa::path(
context_path = "/team",
tag = "teams",
post,
operation_id = "addTeamMember",
params(("id" = TeamId, Path, description = "The ID of the team")),
params(
("id" = TeamId, Path, description = "The ID of the team")
),
request_body = NewTeamMember,
responses(
(status = 204, description = "Expected response to a valid request"),
@@ -267,17 +286,15 @@ pub struct EditTeamMember {
pub ordering: Option<i64>,
}
/// Modify a team member.
/// Update a team member.
#[utoipa::path(
context_path = "/team",
tag = "teams",
patch,
operation_id = "modifyTeamMember",
params(
("id" = TeamId, Path, description = "The ID of the team"),
(
"user_id" = UserId,
Path,
description = "The ID of the user to modify"
)
("user_id" = UserId, Path, description = "The ID of the user to modify")
),
request_body = EditTeamMember,
responses(
@@ -326,11 +343,15 @@ pub struct TransferOwnership {
pub user_id: UserId,
}
/// Transfer team ownership.
/// Transfer team ownership.
#[utoipa::path(
context_path = "/team",
tag = "teams",
patch,
operation_id = "transferTeamOwnership",
params(("id" = TeamId, Path, description = "The ID of the team")),
params(
("id" = TeamId, Path, description = "The ID of the team")
),
request_body = TransferOwnership,
responses(
(status = 204, description = "Expected response to a valid request"),
@@ -369,17 +390,15 @@ 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(
context_path = "/team",
tag = "teams",
delete,
operation_id = "deleteTeamMember",
params(
("id" = TeamId, Path, description = "The ID of the team"),
(
"user_id" = UserId,
Path,
description = "The ID of the user to remove"
)
("user_id" = UserId, Path, description = "The ID of the user to remove")
),
responses(
(status = 204, description = "Expected response to a valid request"),
+28 -13
View File
@@ -9,23 +9,27 @@ 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);
}
/// Get a thread by ID.
/// Get a thread by ID.
#[utoipa::path(
context_path = "/thread",
tag = "threads",
get,
operation_id = "getThread",
params(("id" = ThreadId, Path, description = "The ID of the thread")),
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)"
@@ -51,13 +55,16 @@ 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")),
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<LegacyThread>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
@@ -101,11 +108,15 @@ pub struct NewThreadMessage {
pub body: MessageBody,
}
/// Send a message to a thread.
/// Send a message to a thread.
#[utoipa::path(
context_path = "/thread",
tag = "threads",
post,
operation_id = "sendThreadMessage",
params(("id" = ThreadId, Path, description = "The ID of the thread")),
params(
("id" = ThreadId, Path, description = "The ID of the thread")
),
request_body = NewThreadMessage,
responses(
(status = 204, description = "Expected response to a valid request"),
@@ -142,11 +153,15 @@ 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(
context_path = "/message",
tag = "threads",
delete,
operation_id = "deleteThreadMessage",
params(("id" = ThreadMessageId, Path, description = "The ID of the message")),
params(
("id" = ThreadMessageId, Path, description = "The ID of the message")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
+62 -31
View File
@@ -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)
@@ -29,12 +29,14 @@ 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(
context_path = "/user",
tag = "users",
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)"
@@ -68,12 +70,15 @@ 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")),
responses((status = 200, description = "Expected response to a valid request"))
params(
("ids" = String, Query, description = "The JSON array of user IDs")
),
responses((status = 200, description = "Expected response to a valid request", body = Vec<LegacyUser>))
)]
#[get("/users")]
pub async fn users_get(
@@ -104,13 +109,17 @@ pub async fn users_get(
}
}
/// Get a user by ID or username.
/// Get a user by ID or username.
#[utoipa::path(
context_path = "/user",
tag = "users",
get,
operation_id = "getUser",
params(("id" = String, Path, description = "The ID or username of the user")),
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)"
@@ -139,13 +148,17 @@ pub async fn user_get(
}
}
/// Get a user's projects.
/// Get a user's projects.
#[utoipa::path(
context_path = "/user",
tag = "users",
get,
operation_id = "getUserProjects",
params(("user_id" = String, Path, description = "The ID or username of the user")),
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<LegacyProject>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
@@ -204,11 +217,15 @@ pub struct EditUser {
pub allow_friend_requests: Option<bool>,
}
/// Modify a user.
/// Update a user.
#[utoipa::path(
context_path = "/user",
tag = "users",
patch,
operation_id = "modifyUser",
params(("id" = String, Path, description = "The ID or username of the user")),
params(
("id" = String, Path, description = "The ID or username of the user")
),
request_body = EditUser,
responses(
(status = 204, description = "Expected response to a valid request"),
@@ -258,17 +275,15 @@ pub struct Extension {
pub ext: String,
}
/// Change a user's avatar.
/// Change a user's avatar.
#[utoipa::path(
context_path = "/user",
tag = "users",
patch,
operation_id = "changeUserIcon",
params(
("id" = String, Path, description = "The ID or username of the user"),
(
"ext" = String,
Query,
description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)"
)
("ext" = String, Query, description = "Image extension (png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb)")
),
request_body(
content(
@@ -317,11 +332,15 @@ 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(
context_path = "/user",
tag = "users",
delete,
operation_id = "deleteUserIcon",
params(("id" = String, Path, description = "The ID or username of the user")),
params(
("id" = String, Path, description = "The ID or username of the user")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = 400, description = "Request was invalid, see given error"),
@@ -354,11 +373,15 @@ 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(
context_path = "/user",
tag = "users",
delete,
operation_id = "deleteUser",
params(("id" = String, Path, description = "The ID or username of the user")),
params(
("id" = String, Path, description = "The ID or username of the user")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(
@@ -387,13 +410,17 @@ 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(
context_path = "/user",
tag = "users",
get,
operation_id = "getFollowedProjects",
params(("id" = String, Path, description = "The ID or username of the user")),
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<LegacyProject>),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -434,13 +461,17 @@ pub async fn user_follows(
}
}
/// Get notifications for a user.
/// Get notifications for a user.
#[utoipa::path(
context_path = "/user",
tag = "users",
get,
operation_id = "getUserNotifications",
params(("id" = String, Path, description = "The ID or username of the user")),
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<LegacyNotification>),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -75,8 +75,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(
@@ -84,7 +85,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,
@@ -302,17 +303,20 @@ 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")),
params(
("version_id" = VersionId, Path, description = "The ID of the version")
),
request_body(
content(("multipart/form-data")),
description = "Multipart payload containing files to upload"
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = NO_CONTENT, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
+52 -74
View File
@@ -5,15 +5,16 @@ 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::{v2_reroute, v3};
use crate::routes::HashAlgorithm;
use crate::routes::v3::version_file::{DownloadRedirect, HashQuery};
use crate::routes::{FileHash, v2_reroute, v3};
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 +23,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)
@@ -33,27 +34,17 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
// under /api/v1/version_file/{hash}
/// Get version metadata by file hash.
#[utoipa::path(
context_path = "/version_file",
tag = "version files",
get,
operation_id = "versionFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID when hash maps to multiple files"
)
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<HashAlgorithm>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<crate::models::ids::VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 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)"
@@ -93,27 +84,17 @@ pub async fn get_version_from_hash(
// under /api/v1/version_file/{hash}/download
/// Download a file by hash.
#[utoipa::path(
context_path = "/version_file",
tag = "version files",
get,
operation_id = "downloadVersionFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID when hash maps to multiple files"
)
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<HashAlgorithm>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<crate::models::ids::VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
responses(
(status = 302, description = "Temporary redirect to file URL"),
(status = 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)"
@@ -145,27 +126,17 @@ pub async fn download_version(
// under /api/v1/version_file/{hash}
/// Delete a file by hash.
#[utoipa::path(
context_path = "/version_file",
tag = "version files",
delete,
operation_id = "deleteFileFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID to delete from"
)
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<HashAlgorithm>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<crate::models::ids::VersionId>, Query, description = "Optional version ID to delete from")
),
responses(
(status = 204, description = "Expected response to a valid request"),
(status = NO_CONTENT, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
@@ -208,28 +179,18 @@ pub struct UpdateData {
/// Get the latest compatible version from a file hash.
#[utoipa::path(
context_path = "/version_file",
tag = "version files",
post,
operation_id = "getLatestVersionFromHash",
params(
(
"version_id" = String,
Path,
description = "The hexadecimal file hash"
),
(
"algorithm" = Option<String>,
Query,
description = "Hash algorithm to use (sha1 or sha512)"
),
(
"version_id" = Option<crate::models::ids::VersionId>,
Query,
description = "Optional version ID when hash maps to multiple files"
)
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<HashAlgorithm>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<crate::models::ids::VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
request_body = UpdateData,
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = LegacyVersion),
(status = 400, description = "Request was invalid, see given error"),
(
status = 404,
@@ -287,18 +248,23 @@ pub async fn get_update_from_hash(
// Requests above with multiple versions below
#[derive(Deserialize, utoipa::ToSchema)]
pub struct FileHashes {
/// Hash algorithm to use (sha1 or sha512)
#[schema(value_type = Option<HashAlgorithm>)]
pub algorithm: Option<String>,
#[schema(value_type = Vec<FileHash>)]
pub hashes: Vec<String>,
}
// under /api/v2/version_files
/// Get versions from file hashes.
#[utoipa::path(
context_path = "/version_files",
tag = "version files",
post,
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<FileHash, LegacyVersion>),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -345,11 +311,13 @@ pub async fn get_versions_from_hashes(
/// Get projects from file hashes.
#[utoipa::path(
context_path = "/version_file",
tag = "version files",
post,
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<FileHash, LegacyProject>),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -413,7 +381,10 @@ pub async fn get_projects_from_hashes(
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ManyUpdateData {
/// Hash algorithm to use (sha1 or sha512)
#[schema(value_type = Option<HashAlgorithm>)]
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
#[schema(value_type = Vec<FileHash>)]
pub hashes: Vec<String>,
pub loaders: Option<Vec<String>>,
pub game_versions: Option<Vec<String>>,
@@ -422,11 +393,13 @@ pub struct ManyUpdateData {
/// Get latest compatible versions for multiple hashes.
#[utoipa::path(
context_path = "/version_files",
tag = "version files",
post,
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<FileHash, LegacyVersion>),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -471,11 +444,13 @@ pub async fn update_files(
/// Get all latest compatible versions for multiple hashes.
#[utoipa::path(
context_path = "/version_files",
tag = "version files",
post,
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<FileHash, Vec<LegacyVersion>>),
(status = 400, description = "Request was invalid, see given error")
)
)]
@@ -531,17 +506,20 @@ pub struct FileUpdateData {
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ManyFileUpdateData {
#[schema(value_type = Option<HashAlgorithm>)]
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<FileUpdateData>,
}
/// Get latest versions with per-hash filters.
#[utoipa::path(
context_path = "/version_files",
tag = "version files",
post,
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<FileHash, LegacyVersion>),
(status = 400, description = "Request was invalid, see given error")
)
)]
+48 -53
View File
@@ -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)
@@ -44,39 +44,24 @@ fn default_true() -> bool {
true
}
/// List versions for a project.
/// List versions for a project.
#[utoipa::path(
context_path = "/project/{project_id}",
tag = "versions",
get,
operation_id = "getProjectVersions",
params(
(
"project_id" = String,
Path,
description = "The ID or slug of the project"
),
(
"loaders" = Option<String>,
Query,
description = "JSON array of loaders to filter by"
),
(
"game_versions" = Option<String>,
Query,
description = "JSON array of game versions to filter by"
),
(
"featured" = Option<bool>,
Query,
description = "Filter by featured status"
),
(
"include_changelog" = Option<bool>,
Query,
description = "Whether to include changelog fields"
)
("project_id" = String, Path, description = "The ID or slug of the project"),
("loaders" = Option<String>, Query, description = "JSON array of loaders to filter by"),
("game_versions" = Option<String>, Query, description = "JSON array of game versions to filter by"),
("featured" = Option<bool>, Query, description = "Filter by featured status"),
("version_type" = Option<VersionType>, Query, description = "Filter by version type"),
("limit" = Option<usize>, Query, description = "Maximum number of versions"),
("offset" = Option<usize>, Query, description = "Version offset"),
("include_changelog" = Option<bool>, Query, description = "Whether to include changelog fields")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 200, description = "Expected response to a valid request", body = Vec<LegacyVersion>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
@@ -169,24 +154,18 @@ 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(
context_path = "/project/{project_id}",
tag = "versions",
get,
operation_id = "getVersionFromIdOrNumber",
params(
(
"project_id" = String,
Path,
description = "The ID or slug of the project"
),
(
"slug" = String,
Path,
description = "The version ID or version number"
)
("project_id" = String, Path, description = "The ID or slug of the project"),
("slug" = String, Path, description = "The version ID or version number")
),
responses(
(status = 200, description = "Expected response to a valid request"),
(status = 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)"
@@ -230,12 +209,16 @@ 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")),
responses((status = 200, description = "Expected response to a valid request"))
params(
("ids" = String, Query, description = "The JSON array of version IDs"),
("include_changelog" = Option<bool>, Query, description = "Whether to include changelog fields")
),
responses((status = 200, description = "Expected response to a valid request", body = Vec<LegacyVersion>))
)]
#[get("/versions")]
pub async fn versions_get(
@@ -274,13 +257,17 @@ pub async fn versions_get(
}
}
/// Get a version by ID.
/// Get a version by ID.
#[utoipa::path(
context_path = "/version",
tag = "versions",
get,
operation_id = "getVersion",
params(("version_id" = models::ids::VersionId, Path, description = "The ID of the version")),
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)"
@@ -353,14 +340,18 @@ pub struct EditVersionFileType {
pub file_type: Option<FileType>,
}
/// Modify an existing version.
/// Update an existing version.
#[utoipa::path(
context_path = "/version",
tag = "versions",
patch,
operation_id = "modifyVersion",
params(("id" = VersionId, Path, description = "The ID of the version")),
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)"
@@ -468,13 +459,17 @@ pub async fn version_edit(
Ok(response)
}
/// Delete a version by ID.
/// Delete a version by ID.
#[utoipa::path(
context_path = "/version",
tag = "versions",
delete,
operation_id = "deleteVersion",
params(("version_id" = VersionId, Path, description = "The ID of the version")),
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)"
+21 -9
View File
@@ -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)
@@ -37,8 +37,11 @@ pub struct AnalyticsEventUpsert {
pub ends: DateTime<Utc>,
}
/// Fetches all analytics events.
#[utoipa::path(responses((status = OK, body = Vec<AnalyticsEvent>)))]
/// List analytics events.
#[utoipa::path(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = OK, body = Vec<AnalyticsEvent>))
)]
#[get("")]
pub async fn analytics_events_get(
pool: web::Data<PgPool>,
@@ -54,8 +57,11 @@ 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(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = OK, body = AnalyticsEvent))
)]
#[post("")]
pub async fn analytics_event_create(
req: HttpRequest,
@@ -110,8 +116,11 @@ 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(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = OK, body = AnalyticsEvent))
)]
#[patch("/{id}")]
pub async fn analytics_event_edit(
req: HttpRequest,
@@ -158,8 +167,11 @@ 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(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
pub async fn analytics_event_delete(
req: HttpRequest,
@@ -14,7 +14,7 @@ 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(fetch_facets);
}
@@ -58,7 +58,10 @@ pub struct ProjectPlaytimeFacets {
pub country: Vec<String>,
}
/// Get analytics facets.
#[utoipa::path(
context_path = "/analytics",
tag = "analytics",
responses((status = OK, body = inline(FacetsResponse))),
)]
#[post("/facets")]
@@ -7,9 +7,9 @@
//! requests, you have to zip together M arrays of N elements
//! - this makes it inconvenient to have separate endpoints
mod facets;
pub mod facets;
mod metrics;
mod old;
pub mod old;
use std::{
collections::{HashMap, HashSet},
@@ -54,7 +54,7 @@ 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);
@@ -172,8 +172,10 @@ pub enum ProjectAnalyticsEventKind {
// logic
/// Fetches analytics data for the authorized user's projects.
/// Fetch analytics data.
#[utoipa::path(
context_path = "/analytics",
tag = "analytics",
responses((status = OK, body = inline(GetResponse))),
)]
#[post("")]
@@ -21,7 +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) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(playtimes_get)
.service(views_get)
.service(downloads_get)
@@ -48,7 +48,18 @@ pub struct GetData {
pub resolution_minutes: Option<NonZeroU32>, // defaults to 1 day. Ignored in routes that do not aggregate over a resolution (eg: /countries)
}
#[utoipa::path]
/// Get playtime data.
#[utoipa::path(
context_path = "/analytics",
tag = "analytics",
params(
("project_ids" = Option<String>, Query),
("start_date" = Option<String>, Query),
("end_date" = Option<String>, Query),
("resolution_minutes" = Option<u32>, Query)
),
responses((status = OK, body = HashMap<String, HashMap<u32, u64>>)),
)]
#[get("/playtime")]
pub async fn playtimes_get(
req: HttpRequest,
@@ -110,7 +121,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 +131,17 @@ 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(
context_path = "/analytics",
tag = "analytics",
params(
("project_ids" = Option<String>, Query),
("start_date" = Option<String>, Query),
("end_date" = Option<String>, Query),
("resolution_minutes" = Option<u32>, Query)
),
responses((status = OK, body = HashMap<String, HashMap<u32, u64>>)),
)]
#[get("/views")]
pub async fn views_get(
req: HttpRequest,
@@ -182,7 +203,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 +213,17 @@ 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(
context_path = "/analytics",
tag = "analytics",
params(
("project_ids" = Option<String>, Query),
("start_date" = Option<String>, Query),
("end_date" = Option<String>, Query),
("resolution_minutes" = Option<u32>, Query)
),
responses((status = OK, body = HashMap<String, HashMap<u32, u64>>)),
)]
#[get("/downloads")]
pub async fn downloads_get(
req: HttpRequest,
@@ -255,7 +286,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 +296,17 @@ pub async fn downloads_get(
/// }
///}
/// ONLY project IDs can be used. Unauthorized projects will be filtered out.
#[utoipa::path]
#[utoipa::path(
context_path = "/analytics",
tag = "analytics",
params(
("project_ids" = Option<String>, Query),
("start_date" = Option<String>, Query),
("end_date" = Option<String>, Query),
("resolution_minutes" = Option<u32>, Query)
),
responses((status = OK, body = HashMap<String, HashMap<i64, rust_decimal::Decimal>>)),
)]
#[get("/revenue")]
pub async fn revenue_get(
req: HttpRequest,
@@ -394,7 +435,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 +448,17 @@ 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(
context_path = "/analytics",
tag = "analytics",
params(
("project_ids" = Option<String>, Query),
("start_date" = Option<String>, Query),
("end_date" = Option<String>, Query),
("resolution_minutes" = Option<u32>, Query)
),
responses((status = OK, body = HashMap<String, HashMap<String, u64>>)),
)]
#[get("/countries/downloads")]
pub async fn countries_downloads_get(
req: HttpRequest,
@@ -470,7 +521,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 +534,17 @@ 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(
context_path = "/analytics",
tag = "analytics",
params(
("project_ids" = Option<String>, Query),
("start_date" = Option<String>, Query),
("end_date" = Option<String>, Query),
("resolution_minutes" = Option<u32>, Query)
),
responses((status = OK, body = HashMap<String, HashMap<String, u64>>)),
)]
#[get("/countries/views")]
pub async fn countries_views_get(
req: HttpRequest,
+35 -15
View File
@@ -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<String>,
}
#[utoipa::path(tag = "collections", responses((status = OK)))]
#[post("/collection")]
pub async fn collection_create(
req: HttpRequest,
collection_create_data: web::Json<CollectionCreateData>,
@@ -142,6 +140,12 @@ pub async fn collection_create(
pub struct CollectionIds {
pub ids: String,
}
#[utoipa::path(
tag = "collections",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/collections")]
pub async fn collections_get(
req: HttpRequest,
web::Query(ids): web::Query<CollectionIds>,
@@ -178,6 +182,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 +215,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 +229,14 @@ pub struct EditCollection {
with = "::serde_with::rust::double_option"
)]
pub description: Option<Option<String>>,
#[schema(value_type = Option<String>)]
pub status: Option<CollectionStatus>,
#[validate(length(max = 1024))]
pub new_projects: Option<Vec<String>>,
}
#[utoipa::path(tag = "collections", responses((status = NO_CONTENT)))]
#[patch("/collection/{id}")]
pub async fn collection_edit(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -383,6 +392,13 @@ pub struct Extension {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path(
tag = "collections",
params(("ext" = String, Query)),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[patch("/collection/{id}/icon")]
pub async fn collection_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
@@ -468,6 +484,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 +545,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,)>,
+10 -4
View File
@@ -27,12 +27,18 @@ 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 config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(web::scope("/v3").service(resolve_content));
}
#[post("content/resolve")]
async fn resolve_content(
/// Resolve content.
#[utoipa::path(
tag = "content",
request_body = serde_json::Value,
responses((status = OK, body = serde_json::Value)),
)]
#[post("/content/resolve")]
pub async fn resolve_content(
req: HttpRequest,
request: web::Json<ResolveContentRequest>,
pool: web::Data<PgPool>,
+10 -4
View File
@@ -17,13 +17,15 @@ 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);
}
#[post("friend/{id}")]
/// Add a friend.
#[utoipa::path(tag = "friends", responses((status = NO_CONTENT)))]
#[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", responses((status = NO_CONTENT)))]
#[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", responses((status = OK, body = Vec<UserFriend>)))]
#[get("/friends")]
pub async fn friends(
req: HttpRequest,
pool: web::Data<PgPool>,
+17 -3
View File
@@ -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,20 @@ pub struct ImageUpload {
pub report_id: Option<ReportId>,
}
#[utoipa::path(
tag = "images",
params(
("ext" = String, Query),
("context" = String, Query),
("project_id" = Option<String>, Query),
("version_id" = Option<VersionId>, Query),
("thread_message_id" = Option<ThreadMessageId>, Query),
("report_id" = Option<ReportId>, Query)
),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = OK))
)]
#[post("/image")]
pub async fn images_add(
req: HttpRequest,
web::Query(data): web::Query<ImageUpload>,
+14 -11
View File
@@ -6,18 +6,17 @@ 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);
}
async fn get_project_limits(
#[utoipa::path(tag = "limits", responses((status = OK)))]
#[get("/limits/projects")]
pub async fn get_project_limits(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -36,7 +35,9 @@ async fn get_project_limits(
Ok(web::Json(limits))
}
async fn get_organization_limits(
#[utoipa::path(tag = "limits", responses((status = OK)))]
#[get("/limits/organizations")]
pub async fn get_organization_limits(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -55,7 +56,9 @@ async fn get_organization_limits(
Ok(web::Json(limits))
}
async fn get_collection_limits(
#[utoipa::path(tag = "limits", responses((status = OK)))]
#[get("/limits/collections")]
pub async fn get_collection_limits(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+206 -26
View File
@@ -1,4 +1,5 @@
pub use super::ApiError;
use super::SecurityAddon;
use crate::util::cors::default_cors;
use actix_web::{HttpResponse, web};
use serde_json::json;
@@ -31,14 +32,32 @@ pub mod oauth_clients;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("v3")
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)
.service(payouts::paypal_webhook)
.service(payouts::tremendous_webhook)
.configure(projects::config)
.configure(reports::config)
.configure(shared_instance_version_creation::config)
@@ -52,32 +71,193 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.configure(versions::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/analytics-event")
.wrap(default_cors())
.configure(analytics_event::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),
);
#[derive(utoipa::OpenApi)]
#[openapi(
info(
title = "API v3 (UNSTABLE)",
version = "3.0.0",
description = include_str!("../../api_v3_description.md"),
),
paths(
analytics_get::fetch_analytics,
analytics_get::facets::fetch_facets,
analytics_get::old::playtimes_get,
analytics_get::old::views_get,
analytics_get::old::downloads_get,
analytics_get::old::revenue_get,
analytics_get::old::countries_downloads_get,
analytics_get::old::countries_views_get,
payouts::post_compliance_form,
payouts::calculate_fees,
payouts::create_payout,
payouts::transaction_history,
payouts::cancel_payout,
payouts::payment_methods,
payouts::get_balance,
payouts::platform_revenue,
projects::project_get,
projects::project_get_check,
projects::project_delete,
projects::project_edit,
projects::project_icon_edit,
projects::delete_project_icon,
projects::add_gallery_item,
projects::edit_gallery_item,
projects::delete_gallery_item,
projects::project_follow,
projects::project_unfollow,
projects::project_get_organization,
projects::dependency_list,
project_creation::project_create,
project_creation::project_create_with_id,
project_creation::new::create,
teams::team_members_get_project,
versions::version_project_get,
versions::version_list,
limits::get_project_limits,
limits::get_organization_limits,
limits::get_collection_limits,
collections::collection_create,
collections::collections_get,
collections::collection_get,
collections::collection_edit,
collections::collection_icon_edit,
collections::delete_collection_icon,
collections::collection_delete,
images::images_add,
notifications::notifications_get_route,
notifications::notification_get_route,
notifications::notification_read_route,
notifications::notification_delete_route,
notifications::notifications_read_route,
notifications::notifications_delete_route,
oauth_clients::get_user_clients,
oauth_clients::get_client,
oauth_clients::get_clients,
oauth_clients::oauth_client_create,
oauth_clients::oauth_client_delete,
oauth_clients::oauth_client_edit,
oauth_clients::oauth_client_icon_edit,
oauth_clients::oauth_client_icon_delete,
oauth_clients::get_user_oauth_authorizations,
oauth_clients::revoke_oauth_authorization,
super::super::auth::oauth::init_oauth,
super::super::auth::oauth::accept_client_scopes,
super::super::auth::oauth::reject_client_scopes,
super::super::auth::oauth::request_token,
organizations::organization_projects_get,
organizations::organization_create,
organizations::organization_get,
organizations::organization_notes_edit,
organizations::organizations_get,
organizations::organizations_edit,
organizations::organization_delete,
organizations::organization_projects_add,
organizations::organization_projects_remove,
organizations::organization_icon_edit,
organizations::delete_organization_icon,
super::maven::maven_metadata,
super::maven::version_file,
super::maven::version_file_sha1,
super::maven::version_file_sha512,
super::updates::forge_updates,
projects::project_search,
projects::project_search_post,
projects::projects_get_route,
projects::projects_edit_route,
projects::random_projects_get_route,
reports::report_create_route,
reports::reports_route,
reports::reports_get_route,
reports::report_get_route,
reports::report_edit_route,
reports::report_delete_route,
shared_instance_version_creation::shared_instance_version_create,
shared_instances::shared_instance_create,
shared_instances::shared_instance_list,
shared_instances::shared_instance_get,
shared_instances::shared_instance_edit,
shared_instances::shared_instance_delete,
shared_instances::shared_instance_version_list,
shared_instances::shared_instance_version_get,
shared_instances::shared_instance_version_delete,
shared_instances::shared_instance_version_download,
statistics::get_stats_route,
tags::games_list_route,
tags::category_list_route,
tags::loader_list_route,
tags::loader_fields_list_route,
tags::license_list_route,
tags::license_text_route,
tags::link_platform_list_route,
tags::report_type_list_route,
tags::project_type_list_route,
teams::team_members_get_organization,
teams::team_members_get_route,
teams::teams_get_route,
teams::join_team_route,
teams::add_team_member_route,
teams::edit_team_member_route,
teams::transfer_ownership_route,
teams::remove_team_member_route,
threads::thread_get_route,
threads::threads_get_route,
threads::thread_send_message_route,
threads::message_delete_route,
users::all_projects,
users::admin_user_email,
users::projects_list_route,
users::user_auth_get_route,
users::users_search,
users::users_get_route,
users::user_get_route,
users::user_notes_edit,
users::collections_list,
users::orgs_list,
users::user_edit_route,
users::user_icon_edit_route,
users::user_icon_delete_route,
users::user_delete_route,
users::user_follows_route,
users::user_notifications_route,
version_creation::version_create_route,
version_creation::upload_file_to_version_route,
version_file::get_version_from_hash_route,
version_file::get_update_from_hash_route,
version_file::get_versions_from_hashes_route,
version_file::get_projects_from_hashes_route,
version_file::update_files_many_route,
version_file::update_files_route,
version_file::update_individual_files_route,
version_file::delete_file_route,
version_file::download_version_route,
versions::versions_get_route,
versions::version_get_route,
versions::version_edit_route,
versions::version_delete_route,
friends::add_friend,
friends::remove_friend,
friends::friends,
content::resolve_content,
),
modifiers(&V3PathModifier, &SecurityAddon)
)]
pub struct ApiDoc;
struct V3PathModifier;
impl utoipa::Modify for V3PathModifier {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
super::prefix_openapi_paths(openapi, "/v3", |path| {
path.starts_with("/analytics/")
|| path.starts_with("/maven/")
|| path.starts_with("/updates/")
|| path.starts_with("/debug/")
});
}
}
pub async fn hello_world() -> Result<HttpResponse, ApiError> {
+92 -12
View File
@@ -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,22 @@ pub struct NotificationIds {
pub ids: String,
}
#[utoipa::path(
tag = "notifications",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/notifications")]
pub async fn notifications_get_route(
req: HttpRequest,
ids: web::Query<NotificationIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
notifications_get(req, ids, pool, redis, session_queue).await
}
pub async fn notifications_get(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
@@ -70,6 +82,18 @@ pub async fn notifications_get(
Ok(HttpResponse::Ok().json(notifications))
}
#[utoipa::path(tag = "notifications", responses((status = OK)))]
#[get("/notification/{id}")]
pub async fn notification_get_route(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
notification_get(req, info, pool, redis, session_queue).await
}
pub async fn notification_get(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
@@ -107,6 +131,18 @@ pub async fn notification_get(
}
}
#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))]
#[patch("/notification/{id}")]
pub async fn notification_read_route(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
notification_read(req, info, pool, redis, session_queue).await
}
pub async fn notification_read(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
@@ -157,6 +193,18 @@ pub async fn notification_read(
}
}
#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))]
#[delete("/notification/{id}")]
pub async fn notification_delete_route(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
notification_delete(req, info, pool, redis, session_queue).await
}
pub async fn notification_delete(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
@@ -208,6 +256,22 @@ pub async fn notification_delete(
}
}
#[utoipa::path(
tag = "notifications",
params(("ids" = String, Query)),
responses((status = NO_CONTENT))
)]
#[patch("/notifications")]
pub async fn notifications_read_route(
req: HttpRequest,
ids: web::Query<NotificationIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
notifications_read(req, ids, pool, redis, session_queue).await
}
pub async fn notifications_read(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
@@ -261,6 +325,22 @@ pub async fn notifications_read(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path(
tag = "notifications",
params(("ids" = String, Query)),
responses((status = NO_CONTENT))
)]
#[delete("/notifications")]
pub async fn notifications_delete_route(
req: HttpRequest,
ids: web::Query<NotificationIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
notifications_delete(req, ids, pool, redis, session_queue).await
}
pub async fn notifications_delete(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
+84 -17
View File
@@ -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,11 @@ pub fn config(cfg: &mut web::ServiceConfig) {
);
}
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients", responses((status = OK))
)]
#[get("/user/{id}/oauth_apps")]
pub async fn get_user_clients(
req: HttpRequest,
info: web::Path<String>,
@@ -100,7 +102,14 @@ pub async fn get_user_clients(
}
}
#[get("app/{id}")]
/// Get an OAuth client.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
params(("id" = OAuthClientId, Path)),
responses((status = OK, body = models::oauth_clients::OAuthClient)),
)]
#[get("/app/{id}")]
pub async fn get_client(
id: web::Path<OAuthClientId>,
pool: web::Data<PgPool>,
@@ -113,7 +122,14 @@ pub async fn get_client(
}
}
#[get("apps")]
/// List OAuth clients.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
params(("ids" = Vec<String>, Query)),
responses((status = OK, body = Vec<models::oauth_clients::OAuthClient>)),
)]
#[get("/apps")]
pub async fn get_clients(
info: web::Query<GetOAuthClientsRequest>,
pool: web::Data<PgPool>,
@@ -129,7 +145,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,7 +170,13 @@ pub struct NewOAuthApp {
pub description: Option<String>,
}
#[post("app")]
/// Create an OAuth client.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
responses((status = OK, body = OAuthClientCreationResult)),
)]
#[post("/app")]
pub async fn oauth_client_create(
req: HttpRequest,
new_oauth_app: web::Json<NewOAuthApp>,
@@ -215,7 +237,14 @@ pub async fn oauth_client_create(
}))
}
#[delete("app/{id}")]
/// Delete an OAuth client.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
params(("id" = OAuthClientId, Path)),
responses((status = NO_CONTENT))
)]
#[delete("/app/{id}")]
pub async fn oauth_client_delete(
req: HttpRequest,
client_id: web::Path<OAuthClientId>,
@@ -245,7 +274,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,7 +300,14 @@ pub struct OAuthClientEdit {
pub description: Option<Option<String>>,
}
#[patch("app/{id}")]
/// Update an OAuth client.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
params(("id" = OAuthClientId, Path)),
responses((status = NO_CONTENT))
)]
#[patch("/app/{id}")]
pub async fn oauth_client_edit(
req: HttpRequest,
client_id: web::Path<OAuthClientId>,
@@ -346,7 +382,18 @@ pub struct Extension {
pub ext: String,
}
#[patch("app/{id}/icon")]
/// Update an OAuth client icon.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
params(
("id" = OAuthClientId, Path),
("ext" = String, Query)
),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[patch("/app/{id}/icon")]
#[allow(clippy::too_many_arguments)]
pub async fn oauth_client_icon_edit(
web::Query(ext): web::Query<Extension>,
@@ -418,7 +465,14 @@ pub async fn oauth_client_icon_edit(
Ok(HttpResponse::NoContent().body(""))
}
#[delete("app/{id}/icon")]
/// Delete an OAuth client icon.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
params(("id" = OAuthClientId, Path)),
responses((status = NO_CONTENT))
)]
#[delete("/app/{id}/icon")]
pub async fn oauth_client_icon_delete(
req: HttpRequest,
client_id: web::Path<OAuthClientId>,
@@ -468,7 +522,13 @@ pub async fn oauth_client_icon_delete(
Ok(HttpResponse::NoContent().body(""))
}
#[get("authorizations")]
/// List OAuth authorizations.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
responses((status = OK, body = Vec<models::oauth_clients::OAuthClientAuthorization>)),
)]
#[get("/authorizations")]
pub async fn get_user_oauth_authorizations(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -497,7 +557,14 @@ pub async fn get_user_oauth_authorizations(
Ok(HttpResponse::Ok().json(mapped))
}
#[delete("authorizations")]
/// Revoke OAuth authorization.
#[utoipa::path(
context_path = "/oauth",
tag = "oauth clients",
params(("client_id" = OAuthClientId, Query)),
responses((status = NO_CONTENT))
)]
#[delete("/authorizations")]
pub async fn revoke_oauth_authorization(
req: HttpRequest,
info: web::Query<DeleteOAuthClientQueryParam>,
+48 -27
View File
@@ -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<NewOrganization>,
@@ -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,12 @@ pub struct OrganizationIds {
pub ids: String,
}
#[utoipa::path(
tag = "organizations",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/organizations")]
pub async fn organizations_get(
req: HttpRequest,
web::Query(ids): web::Query<OrganizationIds>,
@@ -484,7 +488,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<String>,
@@ -497,6 +501,8 @@ pub struct OrganizationEdit {
pub name: Option<String>,
}
#[utoipa::path(tag = "organizations", responses((status = NO_CONTENT)))]
#[patch("/organization/{id}")]
pub async fn organizations_edit(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -658,6 +664,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 +831,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 +995,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 +1193,13 @@ pub struct Extension {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path(
tag = "organizations",
params(("ext" = String, Query)),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[patch("/organization/{id}/icon")]
pub async fn organization_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
@@ -1288,6 +1307,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,)>,
+83 -30
View File
@@ -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;
@@ -31,10 +33,8 @@ use tracing::error;
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)
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(transaction_history)
.service(calculate_fees)
.service(create_payout)
.service(cancel_payout)
@@ -49,7 +49,12 @@ pub struct RequestForm {
form_type: users_compliance::FormType,
}
#[utoipa::path]
/// Submit a compliance form.
#[utoipa::path(
context_path = "/payout",
tag = "payouts",
responses((status = OK, body = serde_json::Value)),
)]
#[post("/compliance")]
pub async fn post_compliance_form(
req: HttpRequest,
@@ -148,7 +153,12 @@ pub async fn post_compliance_form(
}
}
#[utoipa::path]
/// Receive PayPal webhook.
#[utoipa::path(
tag = "payouts",
request_body(content = String, content_type = "text/plain"),
responses((status = NO_CONTENT))
)]
#[post("/_paypal")]
pub async fn paypal_webhook(
req: HttpRequest,
@@ -306,7 +316,12 @@ pub async fn paypal_webhook(
Ok(HttpResponse::NoContent().finish())
}
#[utoipa::path]
/// Receive Tremendous webhook.
#[utoipa::path(
tag = "payouts",
request_body(content = String, content_type = "text/plain"),
responses((status = NO_CONTENT))
)]
#[post("/_tremendous")]
pub async fn tremendous_webhook(
req: HttpRequest,
@@ -417,14 +432,19 @@ 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,
pub exchange_rate: Option<Decimal>,
}
#[utoipa::path]
/// Calculate payout fees.
#[utoipa::path(
context_path = "/payout",
tag = "payouts",
responses((status = OK, body = WithdrawalFees)),
)]
#[post("/fees")]
pub async fn calculate_fees(
req: HttpRequest,
@@ -457,7 +477,11 @@ pub async fn calculate_fees(
}))
}
#[utoipa::path]
/// Create a payout.
#[utoipa::path(
context_path = "/payout",
tag = "payouts", responses((status = NO_CONTENT))
)]
#[post("")]
pub async fn create_payout(
req: HttpRequest,
@@ -670,9 +694,12 @@ 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<TransactionItem>)))]
#[utoipa::path(
context_path = "/payout",
tag = "payouts", responses((status = OK, body = Vec<TransactionItem>))
)]
#[get("/history")]
pub async fn transaction_history(
req: HttpRequest,
@@ -754,7 +781,11 @@ pub async fn transaction_history(
Ok(web::Json(txn_items))
}
#[utoipa::path]
/// Cancel a payout.
#[utoipa::path(
context_path = "/payout",
tag = "payouts", responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
pub async fn cancel_payout(
info: web::Path<(PayoutId,)>,
@@ -863,7 +894,7 @@ pub struct MethodFilter {
pub country: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
pub enum FormCompletionStatus {
Unknown,
@@ -873,7 +904,15 @@ pub enum FormCompletionStatus {
Complete,
}
#[utoipa::path]
/// List payment methods.
#[utoipa::path(
context_path = "/payout",
tag = "payouts",
params(
("country" = Option<String>, Query)
),
responses((status = OK, body = Vec<PayoutMethod>)),
)]
#[get("/methods")]
pub async fn payment_methods(
payouts_queue: web::Data<PayoutsQueue>,
@@ -897,7 +936,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,
@@ -906,7 +945,20 @@ pub struct UserBalance {
pub dates: HashMap<DateTime<Utc>, Decimal>,
}
#[utoipa::path]
#[derive(Serialize, utoipa::ToSchema)]
pub struct BalanceResponse {
#[serde(flatten)]
balance: UserBalance,
requested_form_type: Option<users_compliance::FormType>,
form_completion_status: Option<FormCompletionStatus>,
}
/// Get account balance.
#[utoipa::path(
context_path = "/payout",
tag = "payouts",
responses((status = OK, body = BalanceResponse)),
)]
#[get("/balance")]
pub async fn get_balance(
req: HttpRequest,
@@ -924,14 +976,6 @@ pub async fn get_balance(
.await?
.1;
#[derive(Serialize)]
struct Response {
#[serde(flatten)]
balance: UserBalance,
requested_form_type: Option<users_compliance::FormType>,
form_completion_status: Option<FormCompletionStatus>,
}
let balance = get_user_balance(user.id.into(), &pool).await?;
let mut requested_form_type = None;
@@ -965,7 +1009,7 @@ pub async fn get_balance(
);
}
Ok(HttpResponse::Ok().json(Response {
Ok(HttpResponse::Ok().json(BalanceResponse {
balance,
requested_form_type,
form_completion_status,
@@ -1123,21 +1167,30 @@ pub struct RevenueQuery {
pub end: Option<DateTime<Utc>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct RevenueResponse {
pub all_time: Decimal,
pub all_time_available: Decimal,
pub data: Vec<RevenueData>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct RevenueData {
pub time: u64,
pub revenue: Decimal,
pub creator_revenue: Decimal,
}
#[utoipa::path]
/// Get platform revenue.
#[utoipa::path(
context_path = "/payout",
tag = "payouts",
params(
("start" = Option<String>, Query),
("end" = Option<String>, Query)
),
responses((status = OK, body = RevenueResponse)),
)]
#[get("/platform_revenue")]
pub async fn platform_revenue(
query: web::Query<RevenueQuery>,
@@ -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};
@@ -44,9 +44,9 @@ use std::collections::HashMap;
use thiserror::Error;
use validator::Validate;
mod new;
pub 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);
@@ -283,7 +283,16 @@ pub async fn undo_uploads(
Ok(())
}
#[utoipa::path]
/// Create a project.
#[utoipa::path(
context_path = "/project",
tag = "projects",
request_body(
content(("multipart/form-data")),
description = "Multipart payload containing project metadata and files"
),
responses((status = OK, body = Project))
)]
#[post("")]
pub async fn project_create(
req: HttpRequest,
@@ -361,10 +370,18 @@ 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(
context_path = "/project",
tag = "projects",
request_body(
content(("multipart/form-data")),
description = "Multipart payload containing project metadata and files"
),
responses((status = OK, body = Project))
)]
#[post("/{id}", guard = "admin_key_guard")]
pub async fn project_create_with_id(
req: HttpRequest,
@@ -36,7 +36,7 @@ use crate::{
},
};
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(create);
}
@@ -110,11 +110,14 @@ 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(
context_path = "/project",
tag = "projects", responses((status = OK, body = ProjectId))
)]
#[put("")]
pub async fn create(
req: HttpRequest,
+203 -39
View File
@@ -42,20 +42,17 @@ 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) {
cfg.route("search", web::get().to(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 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)
@@ -99,6 +96,20 @@ pub struct RandomProjects {
pub count: u32,
}
#[utoipa::path(
tag = "projects",
params(("count" = u32, Query)),
responses((status = OK))
)]
#[get("/projects_random")]
pub async fn random_projects_get_route(
count: web::Query<RandomProjects>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
random_projects_get(count, pool, redis).await
}
pub async fn random_projects_get(
web::Query(count): web::Query<RandomProjects>,
pool: web::Data<PgPool>,
@@ -140,11 +151,32 @@ 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,
}
#[utoipa::path(
tag = "projects",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/projects")]
pub async fn projects_get_route(
req: HttpRequest,
ids: web::Query<ProjectIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
projects_get(req, ids, pool, redis, session_queue).await
}
pub async fn projects_get(
req: HttpRequest,
web::Query(ids): web::Query<ProjectIds>,
@@ -174,9 +206,13 @@ pub async fn projects_get(
Ok(HttpResponse::Ok().json(projects))
}
#[utoipa::path]
/// Get a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = Project))
)]
#[get("/{id}")]
async fn project_get(
pub async fn project_get(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -305,9 +341,13 @@ pub struct EditProject {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path]
/// Update a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[patch("/{id}")]
async fn project_edit(
pub async fn project_edit(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -1205,6 +1245,27 @@ pub async fn edit_project_categories(
// pub total_hits: usize,
// }
/// Search projects.
#[utoipa::path(
tag = "search",
get,
operation_id = "v3SearchProjects",
params(
("query" = Option<String>, Query, description = "The query to search for"),
("facets" = Option<String>, Query, description = "Search facets JSON"),
("filters" = Option<String>, Query, description = "Search filters JSON"),
("new_filters" = Option<String>, Query, description = "Search filters JSON"),
("index" = Option<String>, Query, description = "Search index to use"),
("offset" = Option<String>, Query, description = "Search result offset"),
("limit" = Option<String>, Query, description = "Maximum number of search results"),
("version" = Option<String>, Query, description = "Game version to filter for")
),
responses(
(status = 200, description = "Expected response to a valid request", body = SearchResults),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[get("/search")]
pub async fn project_search(
web::Query(info): web::Query<SearchQuery>,
search_backend: web::Data<dyn SearchBackend>,
@@ -1230,6 +1291,12 @@ pub async fn project_search(
}
// for more complicated search queries
/// Search projects.
#[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<SearchRequest>,
@@ -1241,9 +1308,13 @@ pub async fn project_search_post(
}
//checks the validity of a project id or slug
#[utoipa::path]
/// Check project availability.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = ProjectCheckResponse))
)]
#[get("/{id}/check")]
async fn project_get_check(
pub async fn project_get_check(
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -1262,21 +1333,25 @@ 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<Project>,
pub versions: Vec<models::projects::Version>,
}
#[utoipa::path]
/// List project dependencies.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = DependencyInfo))
)]
#[get("/{project_id}/dependencies")]
pub async fn dependency_list(
req: HttpRequest,
@@ -1400,7 +1475,7 @@ pub struct CategoryChanges<'a> {
pub remove_categories: &'a Option<Vec<String>>,
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct BulkEditProject {
#[validate(length(max = 3))]
pub categories: Option<Vec<String>>,
@@ -1420,6 +1495,33 @@ pub struct BulkEditProject {
pub link_urls: Option<HashMap<String, Option<String>>>,
}
#[utoipa::path(
tag = "projects",
params(("ids" = String, Query)),
responses((status = NO_CONTENT))
)]
#[patch("/projects")]
pub async fn projects_edit_route(
req: HttpRequest,
ids: web::Query<ProjectIds>,
pool: web::Data<PgPool>,
bulk_edit_project: web::Json<BulkEditProject>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
search_state: web::Data<SearchState>,
) -> Result<HttpResponse, ApiError> {
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<ProjectIds>,
@@ -1722,9 +1824,18 @@ pub struct Extension {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path]
/// Update a project icon.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("ext" = String, Query)
),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[patch("/{id}/icon")]
async fn project_icon_edit(
pub async fn project_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1866,9 +1977,13 @@ pub async fn project_icon_edit_internal(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path]
/// Delete a project icon.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[delete("/{id}/icon")]
async fn delete_project_icon(
pub async fn delete_project_icon(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -1992,7 +2107,20 @@ pub struct GalleryCreateQuery {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path]
/// Add a gallery item.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("ext" = String, Query),
("featured" = bool, Query),
("name" = Option<String>, Query),
("description" = Option<String>, Query),
("ordering" = Option<i64>, Query)
),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[post("/{id}/gallery")]
pub async fn add_gallery_item(
web::Query(ext): web::Query<Extension>,
@@ -2190,9 +2318,21 @@ pub struct GalleryEditQuery {
pub ordering: Option<i64>,
}
#[utoipa::path]
/// Update a gallery item.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("url" = String, Query),
("featured" = Option<bool>, Query),
("name" = Option<String>, Query),
("description" = Option<String>, Query),
("ordering" = Option<i64>, Query)
),
responses((status = NO_CONTENT))
)]
#[patch("/{id}/gallery")]
async fn edit_gallery_item(
pub async fn edit_gallery_item(
req: HttpRequest,
web::Query(item): web::Query<GalleryEditQuery>,
pool: web::Data<PgPool>,
@@ -2379,9 +2519,17 @@ pub struct GalleryDeleteQuery {
pub url: String,
}
#[utoipa::path]
/// Delete a gallery item.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("url" = String, Query)
),
responses((status = NO_CONTENT))
)]
#[delete("/{id}/gallery")]
async fn delete_gallery_item(
pub async fn delete_gallery_item(
req: HttpRequest,
web::Query(item): web::Query<GalleryDeleteQuery>,
pool: web::Data<PgPool>,
@@ -2514,9 +2662,13 @@ pub async fn delete_gallery_item_internal(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path]
/// Delete a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
async fn project_delete(
pub async fn project_delete(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -2672,9 +2824,13 @@ pub async fn project_delete_internal(
}
}
#[utoipa::path]
/// Follow a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[post("/{id}/follow")]
async fn project_follow(
pub async fn project_follow(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -2764,9 +2920,13 @@ pub async fn project_follow_internal(
}
}
#[utoipa::path]
/// Unfollow a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[delete("/{id}/follow")]
async fn project_unfollow(
pub async fn project_unfollow(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -2852,7 +3012,11 @@ pub async fn project_unfollow_internal(
}
}
#[utoipa::path]
/// Get a project's organization.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = models::organizations::Organization))
)]
#[get("/{id}/organization")]
pub async fn project_get_organization(
req: HttpRequest,
+99 -10
View File
@@ -18,23 +18,23 @@ 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)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct CreateReport {
pub report_type: String,
pub item_id: String,
@@ -46,6 +46,22 @@ pub struct CreateReport {
pub uploaded_images: Vec<ImageId>,
}
#[utoipa::path(
tag = "reports",
request_body = CreateReport,
responses((status = OK))
)]
#[post("/report")]
pub async fn report_create_route(
req: HttpRequest,
pool: web::Data<PgPool>,
body: web::Payload,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
report_create(req, pool, body, redis, session_queue).await
}
pub async fn report_create(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -247,6 +263,26 @@ fn default_all() -> bool {
true
}
#[utoipa::path(
tag = "reports",
params(
("count" = Option<u16>, Query),
("offset" = Option<u32>, Query),
("all" = Option<bool>, Query)
),
responses((status = OK))
)]
#[get("/report")]
pub async fn reports_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
request_opts: web::Query<ReportsRequestOptions>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
reports(req, pool, redis, request_opts, session_queue).await
}
pub async fn reports(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -322,6 +358,22 @@ pub struct ReportIds {
pub ids: String,
}
#[utoipa::path(
tag = "reports",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/reports")]
pub async fn reports_get_route(
req: HttpRequest,
ids: web::Query<ReportIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
reports_get(req, ids, pool, redis, session_queue).await
}
pub async fn reports_get(
req: HttpRequest,
web::Query(ids): web::Query<ReportIds>,
@@ -361,6 +413,18 @@ pub async fn reports_get(
Ok(HttpResponse::Ok().json(all_reports))
}
#[utoipa::path(tag = "reports", responses((status = OK)))]
#[get("/report/{id}")]
pub async fn report_get_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
info: web::Path<(crate::models::ids::ReportId,)>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
report_get(req, pool, redis, info, session_queue).await
}
pub async fn report_get(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -395,13 +459,26 @@ pub async fn report_get(
}
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct EditReport {
#[validate(length(max = 65536))]
pub body: Option<String>,
pub closed: Option<bool>,
}
#[utoipa::path(tag = "reports", responses((status = NO_CONTENT)))]
#[patch("/report/{id}")]
pub async fn report_edit_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
info: web::Path<(crate::models::ids::ReportId,)>,
session_queue: web::Data<AuthQueue>,
edit_report: web::Json<EditReport>,
) -> Result<HttpResponse, ApiError> {
report_edit(req, pool, redis, info, session_queue, edit_report).await
}
pub async fn report_edit(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -511,6 +588,18 @@ pub async fn report_edit(
}
}
#[utoipa::path(tag = "reports", responses((status = NO_CONTENT)))]
#[delete("/report/{id}")]
pub async fn report_delete_route(
req: HttpRequest,
pool: web::Data<PgPool>,
info: web::Path<(crate::models::ids::ReportId,)>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
report_delete(req, pool, info, redis, session_queue).await
}
pub async fn report_delete(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -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,13 +29,33 @@ 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,
params(
("id" = SharedInstanceId, Path, description = "The ID of the shared instance")
),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
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"]))
)]
#[post("/shared-instance/{id}/version")]
#[allow(clippy::too_many_arguments)]
pub async fn shared_instance_version_create(
req: HttpRequest,
+39 -22
View File
@@ -19,33 +19,24 @@ 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)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct CreateSharedInstance {
#[validate(
length(min = 3, max = 64),
@@ -56,6 +47,12 @@ pub struct CreateSharedInstance {
pub public: bool,
}
#[utoipa::path(
tag = "shared instances",
request_body = CreateSharedInstance,
responses((status = OK))
)]
#[post("/shared-instance")]
pub async fn shared_instance_create(
req: HttpRequest,
pool: Data<PgPool>,
@@ -101,6 +98,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<PgPool>,
@@ -148,6 +147,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<PgPool>,
@@ -209,7 +210,7 @@ fn can_access_instance_privately(
|| users.iter().any(|x| x.user_id == user.id.into())
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct EditSharedInstance {
#[validate(
length(min = 3, max = 64),
@@ -219,6 +220,12 @@ pub struct EditSharedInstance {
pub public: Option<bool>,
}
#[utoipa::path(
tag = "shared instances",
request_body = EditSharedInstance,
responses((status = NO_CONTENT))
)]
#[patch("/shared-instance/{id}")]
pub async fn shared_instance_edit(
req: HttpRequest,
pool: Data<PgPool>,
@@ -299,6 +306,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<PgPool>,
@@ -355,6 +364,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<PgPool>,
@@ -401,6 +412,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<PgPool>,
@@ -461,6 +474,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<PgPool>,
@@ -560,6 +575,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<PgPool>,
+11 -3
View File
@@ -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<i64>,
}
#[utoipa::path(tag = "statistics", responses((status = OK)))]
#[get("/statistics")]
pub async fn get_stats_route(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
get_stats(pool).await
}
pub async fn get_stats(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
+96 -14
View File
@@ -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<String>,
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/games")]
pub async fn games_list_route(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
games_list(pool, redis).await
}
pub async fn games_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -63,6 +69,15 @@ pub struct CategoryData {
pub header: String,
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/tag/category")]
pub async fn category_list_route(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
category_list(pool, redis).await
}
pub async fn category_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -91,6 +106,15 @@ pub struct LoaderData {
pub metadata: Value,
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/tag/loader")]
pub async fn loader_list_route(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
loader_list(pool, redis).await
}
pub async fn loader_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -131,6 +155,23 @@ pub struct LoaderFieldsEnumQuery {
}
// Provides the variants for any enumerable loader field.
#[utoipa::path(
tag = "tags",
params(
("loader_field" = String, Query),
("filters" = Option<HashMap<String, Value>>, Query)
),
responses((status = OK))
)]
#[get("/loader_field")]
pub async fn loader_fields_list_route(
pool: web::Data<PgPool>,
web::Query(query): web::Query<LoaderFieldsEnumQuery>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
loader_fields_list(pool, web::Query(query), redis).await
}
pub async fn loader_fields_list(
pool: web::Data<PgPool>,
query: web::Query<LoaderFieldsEnumQuery>,
@@ -181,6 +222,12 @@ pub struct License {
pub name: String,
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/license")]
pub async fn license_list_route() -> HttpResponse {
license_list().await
}
pub async fn license_list() -> HttpResponse {
let licenses = spdx::identifiers::LICENSES;
let mut results: Vec<License> = Vec::with_capacity(licenses.len());
@@ -201,6 +248,14 @@ pub struct LicenseText {
pub body: String,
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/license/{id}")]
pub async fn license_text_route(
params: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
license_text(params).await
}
pub async fn license_text(
params: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
@@ -231,6 +286,15 @@ pub struct LinkPlatformQueryData {
pub donation: bool,
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/link_platform")]
pub async fn link_platform_list_route(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
link_platform_list(pool, redis).await
}
pub async fn link_platform_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -247,6 +311,15 @@ pub async fn link_platform_list(
Ok(HttpResponse::Ok().json(results))
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/report_type")]
pub async fn report_type_list_route(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
report_type_list(pool, redis).await
}
pub async fn report_type_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -255,6 +328,15 @@ pub async fn report_type_list(
Ok(HttpResponse::Ok().json(results))
}
#[utoipa::path(tag = "tags", responses((status = OK)))]
#[get("/project_type")]
pub async fn project_type_list_route(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
project_type_list(pool, redis).await
}
pub async fn project_type_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
+112 -21
View File
@@ -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,
@@ -40,9 +34,13 @@ 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(
context_path = "/project",
tag = "teams", responses((status = OK, body = Vec<crate::models::teams::TeamMember>))
)]
#[get("/{project_id}/members")]
async fn team_members_get_project(
pub async fn team_members_get_project(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -136,6 +134,8 @@ pub async fn team_members_get_project_internal(
}
}
#[utoipa::path(tag = "teams", responses((status = OK, body = Vec<crate::models::teams::TeamMember>)))]
#[get("/organization/{id}/members")]
pub async fn team_members_get_organization(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -213,6 +213,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<crate::models::teams::TeamMember>)))]
#[get("/team/{id}/members")]
pub async fn team_members_get_route(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
team_members_get(req, info, pool, redis, session_queue).await
}
pub async fn team_members_get(
req: HttpRequest,
info: web::Path<(TeamId,)>,
@@ -278,6 +290,22 @@ pub struct TeamIds {
pub ids: String,
}
#[utoipa::path(
tag = "teams",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/teams")]
pub async fn teams_get_route(
req: HttpRequest,
ids: web::Query<TeamIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
teams_get(req, ids, pool, redis, session_queue).await
}
pub async fn teams_get(
req: HttpRequest,
web::Query(ids): web::Query<TeamIds>,
@@ -348,6 +376,18 @@ pub async fn teams_get(
Ok(HttpResponse::Ok().json(teams))
}
#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))]
#[post("/team/{id}/join")]
pub async fn join_team_route(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
join_team(req, info, pool, redis, session_queue).await
}
pub async fn join_team(
req: HttpRequest,
info: web::Path<(TeamId,)>,
@@ -417,7 +457,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")]
@@ -433,6 +473,19 @@ pub struct NewTeamMember {
pub ordering: i64,
}
#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))]
#[post("/team/{id}/members")]
pub async fn add_team_member_route(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
new_member: web::Json<NewTeamMember>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
add_team_member(req, info, pool, new_member, redis, session_queue).await
}
pub async fn add_team_member(
req: HttpRequest,
info: web::Path<(TeamId,)>,
@@ -678,7 +731,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<ProjectPermissions>,
pub organization_permissions: Option<OrganizationPermissions>,
@@ -687,6 +740,19 @@ pub struct EditTeamMember {
pub ordering: Option<i64>,
}
#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))]
#[patch("/team/{id}/members/{user_id}")]
pub async fn edit_team_member_route(
req: HttpRequest,
info: web::Path<(TeamId, String)>,
pool: web::Data<PgPool>,
edit_member: web::Json<EditTeamMember>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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)>,
@@ -882,11 +948,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")]
pub async fn transfer_ownership_route(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
new_owner: web::Json<TransferOwnership>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
transfer_ownership(req, info, pool, new_owner, redis, session_queue).await
}
pub async fn transfer_ownership(
req: HttpRequest,
info: web::Path<(TeamId,)>,
@@ -1072,6 +1151,18 @@ pub async fn transfer_ownership(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path(tag = "teams", responses((status = NO_CONTENT)))]
#[delete("/team/{id}/members/{user_id}")]
pub async fn remove_team_member_route(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
remove_team_member(req, info, pool, redis, session_queue).await
}
pub async fn remove_team_member(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
+62 -12
View File
@@ -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}")]
pub async fn thread_get_route(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
thread_get(req, info, pool, redis, session_queue).await
}
pub async fn thread_get(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
@@ -324,6 +331,22 @@ pub struct ThreadIds {
pub ids: String,
}
#[utoipa::path(
tag = "threads",
params(("ids" = String, Query)),
responses((status = OK, body = Vec<Thread>))
)]
#[get("/threads")]
pub async fn threads_get_route(
req: HttpRequest,
ids: web::Query<ThreadIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
threads_get(req, ids, pool, redis, session_queue).await
}
pub async fn threads_get(
req: HttpRequest,
web::Query(ids): web::Query<ThreadIds>,
@@ -356,11 +379,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}")]
pub async fn thread_send_message_route(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
pool: web::Data<PgPool>,
new_message: web::Json<NewThreadMessage>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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 +628,19 @@ pub async fn thread_send_message_internal(
}
}
#[utoipa::path(tag = "threads", responses((status = NO_CONTENT)))]
#[delete("/message/{id}")]
pub async fn message_delete_route(
req: HttpRequest,
info: web::Path<(ThreadMessageId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
file_host: web::Data<dyn FileHost>,
) -> Result<HttpResponse, ApiError> {
message_delete(req, info, pool, redis, session_queue, file_host).await
}
pub async fn message_delete(
req: HttpRequest,
info: web::Path<(ThreadMessageId,)>,
+183 -24
View File
@@ -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,12 @@ pub async fn all_projects(
}))
}
#[utoipa::path(
tag = "users",
params(("email" = String, Query)),
responses((status = OK))
)]
#[get("/user_email")]
pub async fn admin_user_email(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -254,6 +258,18 @@ pub async fn admin_user_email(
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{user_id}/projects")]
pub async fn projects_list_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
projects_list(req, info, pool, redis, session_queue).await
}
pub async fn projects_list(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -291,6 +307,17 @@ pub async fn projects_list(
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user")]
pub async fn user_auth_get_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_auth_get(req, pool, redis, session_queue).await
}
pub async fn user_auth_get(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -333,6 +360,12 @@ pub struct UserSearchQuery {
pub query: String,
}
#[utoipa::path(
tag = "users",
params(("query" = String, Query)),
responses((status = OK))
)]
#[get("/users/search")]
pub async fn users_search(
web::Query(query): web::Query<UserSearchQuery>,
pool: web::Data<PgPool>,
@@ -348,6 +381,22 @@ pub async fn users_search(
Ok(web::Json(users))
}
#[utoipa::path(
tag = "users",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/users")]
pub async fn users_get_route(
req: HttpRequest,
ids: web::Query<UserIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
users_get(req, ids, pool, redis, session_queue).await
}
pub async fn users_get(
req: HttpRequest,
web::Query(ids): web::Query<UserIds>,
@@ -396,6 +445,18 @@ pub async fn users_get(
Ok(HttpResponse::Ok().json(users))
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{id}")]
pub async fn user_get_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_get(req, info, pool, redis, session_queue).await
}
pub async fn user_get(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -439,6 +500,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 +569,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 +610,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 +707,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<String>,
@@ -658,6 +725,19 @@ pub struct EditUser {
pub allow_friend_requests: Option<bool>,
}
#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))]
#[patch("/user/{id}")]
pub async fn user_edit_route(
req: HttpRequest,
info: web::Path<(String,)>,
new_user: web::Json<EditUser>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_edit(req, info, new_user, pool, redis, session_queue).await
}
pub async fn user_edit(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -827,6 +907,36 @@ pub struct Extension {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path(
tag = "users",
params(("ext" = String, Query)),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[patch("/user/{id}/icon")]
pub async fn user_icon_edit_route(
ext: web::Query<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<dyn FileHost>,
payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<Extension>,
req: HttpRequest,
@@ -903,6 +1013,19 @@ pub async fn user_icon_edit(
}
}
#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))]
#[delete("/user/{id}/icon")]
pub async fn user_icon_delete_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<dyn FileHost>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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 +1080,18 @@ pub async fn user_icon_delete(
}
}
#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))]
#[delete("/user/{id}")]
pub async fn user_delete_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<(), ApiError> {
user_delete(req, info, pool, redis, session_queue).await
}
pub async fn user_delete(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1005,6 +1140,18 @@ pub async fn user_delete(
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{id}/follows")]
pub async fn user_follows_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_follows(req, info, pool, redis, session_queue).await
}
pub async fn user_follows(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1047,6 +1194,18 @@ pub async fn user_follows(
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{id}/notifications")]
pub async fn user_notifications_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_notifications(req, info, pool, redis, session_queue).await
}
pub async fn user_notifications(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -33,7 +33,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;
@@ -104,6 +104,48 @@ struct InitialFileData {
}
// under `/api/v1/version`
/// Create a version on an existing project.
#[utoipa::path(
tag = "versions",
post,
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"]))
)]
#[post("/version")]
pub async fn version_create_route(
req: HttpRequest,
payload: Multipart,
client: Data<PgPool>,
redis: Data<RedisPool>,
file_host: Data<dyn FileHost>,
session_queue: Data<AuthQueue>,
http: web::Data<HttpClient>,
search_state: Data<SearchState>,
) -> Result<HttpResponse, CreateError> {
version_create(
req,
payload,
client,
redis,
file_host,
session_queue,
http,
search_state,
)
.await
}
pub async fn version_create(
req: HttpRequest,
mut payload: Multipart,
@@ -529,6 +571,56 @@ async fn version_create_inner(
Ok((HttpResponse::Ok().json(response), project_id))
}
/// Add files to an existing version.
#[utoipa::path(
tag = "versions",
post,
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"]))
)]
#[post("/version/{version_id}/file")]
pub async fn upload_file_to_version_route(
req: HttpRequest,
url_data: web::Path<(VersionId,)>,
payload: Multipart,
client: Data<PgPool>,
redis: Data<RedisPool>,
file_host: Data<dyn FileHost>,
session_queue: web::Data<AuthQueue>,
http: web::Data<HttpClient>,
search_state: Data<SearchState>,
) -> Result<HttpResponse, CreateError> {
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,)>,
+268 -26
View File
@@ -10,32 +10,56 @@ use crate::models::projects::{ProjectStatus, VersionStatus, VersionType};
use crate::models::teams::ProjectPermissions;
use crate::queue::session::AuthQueue;
use crate::routes::internal::delphi;
use crate::routes::{FileHash, HashAlgorithm};
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,
operation_id = "v3VersionFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, 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)"
)
)
)]
#[get("/version_file/{version_id}")]
pub async fn get_version_from_hash_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
get_version_from_hash(req, info, pool, redis, hash_query, session_queue)
.await
}
pub async fn get_version_from_hash(
@@ -89,7 +113,7 @@ pub async fn get_version_from_hash(
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct HashQuery {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub version_id: Option<VersionId>,
@@ -110,7 +134,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<Vec<String>>,
pub version_types: Option<Vec<VersionType>>,
@@ -123,6 +147,47 @@ pub struct UpdateData {
pub loader_fields: Option<HashMap<String, Vec<serde_json::Value>>>,
}
/// Get the latest matching version by file hash.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3UpdateFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
request_body = UpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = models::projects::Version),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[post("/version_file/{version_id}/update")]
pub async fn get_update_from_hash_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
update_data: web::Json<UpdateData>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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,)>,
@@ -208,12 +273,40 @@ pub async fn get_update_from_hash(
}
// Requests above with multiple versions below
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct FileHashes {
/// Hash algorithm to use (sha1 or sha512)
#[schema(value_type = Option<HashAlgorithm>)]
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
#[schema(value_type = Vec<FileHash>)]
pub hashes: Vec<String>,
}
/// Get versions by file hashes.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3VersionsFromHashes",
request_body = FileHashes,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Version>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[post("/version_files")]
pub async fn get_versions_from_hashes_route(
req: HttpRequest,
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
file_data: web::Json<FileHashes>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
get_versions_from_hashes(req, pool, redis, file_data, session_queue).await
}
pub async fn get_versions_from_hashes(
req: HttpRequest,
pool: web::Data<ReadOnlyPgPool>,
@@ -269,6 +362,31 @@ pub async fn get_versions_from_hashes(
Ok(HttpResponse::Ok().json(response))
}
/// Get projects by file hashes.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3ProjectsFromHashes",
request_body = FileHashes,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Project>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[post("/version_file/project")]
pub async fn get_projects_from_hashes_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_data: web::Json<FileHashes>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
get_projects_from_hashes(req, pool, redis, file_data, session_queue).await
}
pub async fn get_projects_from_hashes(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -327,7 +445,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<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<String>,
@@ -336,6 +454,26 @@ pub struct ManyUpdateData {
pub version_types: Option<Vec<VersionType>>,
}
/// Get latest matching versions by file hashes.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3UpdateFilesMany",
request_body = ManyUpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, Vec<models::projects::Version>>)
)
)]
#[post("/version_files/update_many")]
pub async fn update_files_many_route(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
update_data: web::Json<ManyUpdateData>,
) -> Result<web::Json<HashMap<String, Vec<models::projects::Version>>>, ApiError>
{
update_files_many(pool, redis, update_data).await
}
pub async fn update_files_many(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
@@ -368,6 +506,25 @@ 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,
operation_id = "v3UpdateFiles",
request_body = ManyUpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Version>)
)
)]
#[post("/version_files/update")]
pub async fn update_files_route(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
update_data: web::Json<ManyUpdateData>,
) -> Result<web::Json<HashMap<String, models::projects::Version>>, ApiError> {
update_files(pool, redis, update_data).await
}
pub async fn update_files(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
@@ -468,7 +625,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<Vec<String>>,
@@ -476,12 +633,33 @@ pub struct FileUpdateData {
pub version_types: Option<Vec<VersionType>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct ManyFileUpdateData {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<FileUpdateData>,
}
/// Get latest matching versions by individual file filters.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3UpdateIndividualFiles",
request_body = ManyFileUpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Version>)
)
)]
#[post("/version_files/update_individual")]
pub async fn update_individual_files_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
update_data: web::Json<ManyFileUpdateData>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
update_individual_files(req, pool, redis, update_data, session_queue).await
}
pub async fn update_individual_files(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -603,6 +781,40 @@ pub async fn update_individual_files(
}
// under /api/v1/version_file/{hash}
/// Delete a file by hash.
#[utoipa::path(
tag = "version files",
delete,
operation_id = "v3DeleteFileFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, Query, description = "Optional version ID to delete from")
),
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)"
)
)
)]
#[delete("/version_file/{version_id}")]
pub async fn delete_file_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
delete_file(req, info, pool, redis, hash_query, session_queue).await
}
pub async fn delete_file(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -740,12 +952,42 @@ 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,
operation_id = "v3DownloadVersionFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
responses(
(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)"
)
)
)]
#[get("/version_file/{version_id}/download")]
pub async fn download_version_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
download_version(req, info, pool, redis, hash_query, session_queue).await
}
pub async fn download_version(
req: HttpRequest,
info: web::Path<(String,)>,
+175 -21
View File
@@ -31,33 +31,34 @@ 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
#[utoipa::path]
/// Get a project version.
#[utoipa::path(
context_path = "/project",
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,
@@ -162,6 +163,28 @@ fn default_true() -> bool {
true
}
/// Get multiple versions by ID.
#[utoipa::path(
tag = "versions",
get,
params(
("ids" = String, Query, description = "The JSON array of version IDs"),
("include_changelog" = Option<bool>, Query, description = "Whether to include changelog fields")
),
responses((status = 200, description = "Expected response to a valid request", body = Vec<models::projects::Version>))
)]
#[get("/versions")]
pub async fn versions_get_route(
req: HttpRequest,
ids: web::Query<VersionIds>,
pool: web::Data<PgPool>,
ro_pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
versions_get(req, ids, pool, ro_pool, redis, session_queue).await
}
pub async fn versions_get(
req: HttpRequest,
web::Query(ids): web::Query<VersionIds>,
@@ -211,6 +234,33 @@ pub async fn versions_get(
Ok(HttpResponse::Ok().json(versions))
}
/// Get a version by ID.
#[utoipa::path(
tag = "versions",
get,
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)"
)
)
)]
#[get("/version/{id}")]
pub async fn version_get_route(
req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
ro_pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<models::projects::Version>, 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,)>,
@@ -330,6 +380,48 @@ pub struct EditVersionFileType {
pub file_type: Option<FileType>,
}
/// Update an existing version.
#[utoipa::path(
tag = "versions",
patch,
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"]))
)]
#[patch("/version/{id}")]
pub async fn version_edit_route(
req: HttpRequest,
info: web::Path<(VersionId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
new_version: web::Json<serde_json::Value>,
session_queue: web::Data<AuthQueue>,
search_state: web::Data<SearchState>,
) -> Result<HttpResponse, ApiError> {
version_edit(
req,
info,
pool,
redis,
new_version,
session_queue,
search_state,
)
.await
}
pub async fn version_edit(
req: HttpRequest,
info: web::Path<(VersionId,)>,
@@ -798,9 +890,29 @@ pub struct VersionListFilters {
pub include_changelog: bool,
}
#[utoipa::path]
/// List project versions.
#[utoipa::path(
context_path = "/project",
tag = "versions",
params(
("loaders" = Option<String>, Query),
("featured" = Option<bool>, Query),
("version_type" = Option<VersionType>, Query),
("limit" = Option<usize>, Query),
("offset" = Option<usize>, Query),
("loader_fields" = Option<String>, Query),
("include_changelog" = Option<bool>, Query)
),
responses(
(status = 200, description = "Expected response to a valid request", body = Vec<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")]
async fn version_list(
pub async fn version_list(
req: HttpRequest,
info: web::Path<(String,)>,
web::Query(filters): web::Query<VersionListFilters>,
@@ -990,6 +1102,48 @@ pub async fn version_list_internal(
}
}
/// Delete a version by ID.
#[utoipa::path(
tag = "versions",
delete,
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"]))
)]
#[delete("/version/{id}")]
pub async fn version_delete_route(
req: HttpRequest,
info: web::Path<(VersionId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
search_backend: web::Data<dyn SearchBackend>,
search_state: web::Data<SearchState>,
) -> Result<HttpResponse, ApiError> {
version_delete(
req,
info,
pool,
redis,
session_queue,
search_backend,
search_state,
)
.await
}
pub async fn version_delete(
req: HttpRequest,
info: web::Path<(VersionId,)>,
+3 -3
View File
@@ -285,7 +285,7 @@ pub struct UploadSearchProject {
pub loader_fields: HashMap<String, Vec<serde_json::Value>>,
}
#[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<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct SearchResults {
pub hits: Vec<ResultSearchProject>,
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,

Some files were not shown because too many files have changed in this diff Show More