mirror of
https://github.com/modrinth/code.git
synced 2026-08-02 14:15:53 +00:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0839657b2 | ||
|
|
13b53a4395 | ||
|
|
b93c6f9321 | ||
|
|
3713ab4d6f | ||
|
|
9ee85ecf34 | ||
|
|
6cc660b70d | ||
|
|
2515dd95cc | ||
|
|
8777543f1a | ||
|
|
cb96a4135a | ||
|
|
80431c1ec1 | ||
|
|
d6b8b9eb16 | ||
|
|
9e441c0fb3 | ||
|
|
97408bb529 | ||
|
|
6c7f8cd3e0 | ||
|
|
5e79114c6a | ||
|
|
2ed1d95dd0 | ||
|
|
55a6ecca80 | ||
|
|
1d5beb8a6b | ||
|
|
40ba23b012 | ||
|
|
f7bb0946cf | ||
|
|
0cb84b029f | ||
|
|
4217f4bdf6 | ||
|
|
df221504b7 | ||
|
|
77d936cec9 | ||
|
|
33fcdeda09 | ||
|
|
665742c6e9 | ||
|
|
98c8e45d02 | ||
|
|
85884493e3 | ||
|
|
948e8af368 | ||
|
|
dfb2dafb21 | ||
|
|
3d43c07c75 | ||
|
|
1e2f55bda0 | ||
|
|
2481479317 | ||
|
|
4c7048e58f | ||
|
|
5ba0bf55d4 | ||
|
|
c460db1145 | ||
|
|
ee79a883ab | ||
|
|
dfb9bb0259 | ||
|
|
0aeb0dc916 | ||
|
|
48b1fb9b11 | ||
|
|
f863f6c9ac | ||
|
|
e0299968c4 | ||
|
|
617e3418dd | ||
|
|
febf5faa54 | ||
|
|
8bcb593723 | ||
|
|
e0015d531d | ||
|
|
3a83373695 | ||
|
|
25b8452cb2 | ||
|
|
06c647a162 | ||
|
|
50671fc2ac | ||
|
|
3916a49309 | ||
|
|
2174cc1f2b |
@@ -7,11 +7,13 @@ on:
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- 'apps/labrinth/**'
|
||||
- 'packages/ariadne/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- 'apps/labrinth/**'
|
||||
- 'packages/ariadne/**'
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
|
||||
@@ -89,4 +89,6 @@ jobs:
|
||||
- name: 🔍 Verify intl:extract has been run
|
||||
run: |
|
||||
pnpm intl:extract
|
||||
git diff --exit-code --color */*/src/locales/en-US/index.json
|
||||
git diff --exit-code --color \
|
||||
*/*/src/locales/en-US/index.json \
|
||||
apps/frontend/src/locales/en-US/labrinth.json
|
||||
|
||||
Generated
+2
-1
@@ -11,10 +11,11 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/apps/ariadne-extract/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/path-util/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
Generated
+184
-35
@@ -481,6 +481,22 @@ dependencies = [
|
||||
"uuid 1.18.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ariadne-extract"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"derive_more 2.0.1",
|
||||
"miette",
|
||||
"proc-macro2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn 2.0.106",
|
||||
"syn-miette",
|
||||
"thiserror 2.0.17",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -969,6 +985,15 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace-ext"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
@@ -1005,6 +1030,15 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
@@ -4357,6 +4391,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_ci"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@@ -4616,6 +4656,7 @@ dependencies = [
|
||||
"const_format",
|
||||
"dashmap",
|
||||
"deadpool-redis",
|
||||
"derive_more 2.0.1",
|
||||
"dotenv-build",
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -4652,6 +4693,7 @@ dependencies = [
|
||||
"sentry",
|
||||
"sentry-actix",
|
||||
"serde",
|
||||
"serde-xml-rs",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"sha1",
|
||||
@@ -4675,7 +4717,6 @@ dependencies = [
|
||||
"validator",
|
||||
"webp",
|
||||
"woothee",
|
||||
"yaserde",
|
||||
"zip 6.0.0",
|
||||
"zxcvbn",
|
||||
]
|
||||
@@ -4843,6 +4884,12 @@ dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -5093,6 +5140,37 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"backtrace-ext",
|
||||
"cfg-if",
|
||||
"miette-derive",
|
||||
"owo-colors",
|
||||
"supports-color",
|
||||
"supports-hyperlinks",
|
||||
"supports-unicode",
|
||||
"syntect",
|
||||
"terminal_size",
|
||||
"textwrap",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette-derive"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -5832,6 +5910,28 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "onig"
|
||||
version = "6.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"onig_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onig_sys"
|
||||
version = "69.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.2"
|
||||
@@ -8067,18 +8167,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_tokenstream"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -8689,6 +8777,27 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "supports-color"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
|
||||
dependencies = [
|
||||
"is_ci",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supports-hyperlinks"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
|
||||
|
||||
[[package]]
|
||||
name = "supports-unicode"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -8722,6 +8831,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn-miette"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd1a2bfae2df81406f8d21baa0253d34ddd0ddafcd1e7aa12aa24279bb76a24b"
|
||||
dependencies = [
|
||||
"miette",
|
||||
"proc-macro2",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -8742,6 +8862,27 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syntect"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"flate2",
|
||||
"fnv",
|
||||
"once_cell",
|
||||
"onig",
|
||||
"plist",
|
||||
"regex-syntax",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"walkdir",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
@@ -9336,6 +9477,26 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
||||
dependencies = [
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"unicode-linebreak",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theseus"
|
||||
version = "1.0.0-local"
|
||||
@@ -10145,6 +10306,12 @@ version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.24"
|
||||
@@ -11449,30 +11616,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
||||
|
||||
[[package]]
|
||||
name = "yaserde"
|
||||
version = "0.12.0"
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bfa0d2b420fd005aa9b6f99f9584ebd964e6865d7ca787304cc1a3366c39231"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"log",
|
||||
"xml-rs",
|
||||
"yaserde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaserde_derive"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f785831c0e09e0f1a83f917054fd59c088f6561db5b2a42c1c3e1687329325f"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.106",
|
||||
"xml-rs",
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+9
-4
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"apps/app",
|
||||
"apps/app-playground",
|
||||
"apps/ariadne-extract",
|
||||
"apps/daedalus_client",
|
||||
"apps/labrinth",
|
||||
"packages/app-lib",
|
||||
@@ -46,7 +47,7 @@ chrono = "0.4.42"
|
||||
cidre = { version = "0.11.3", default-features = false, features = [
|
||||
"macos_15_0",
|
||||
] }
|
||||
clap = "4.5.48"
|
||||
clap = { version = "4.5.48", features = ["derive"] }
|
||||
clickhouse = "0.14.0"
|
||||
color-eyre = "0.6.5"
|
||||
color-thief = "0.2.2"
|
||||
@@ -102,6 +103,7 @@ lettre = { version = "0.11.19", default-features = false, features = [
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||
miette = "7.6.0"
|
||||
murmur2 = "0.1.0"
|
||||
native-dialog = "0.9.2"
|
||||
notify = { version = "8.2.0", default-features = false }
|
||||
@@ -111,6 +113,7 @@ paste = "1.0.15"
|
||||
path-util = { path = "packages/path-util" }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
png = "0.18.0"
|
||||
proc-macro2 = "1.0.101"
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.38.3"
|
||||
@@ -141,18 +144,20 @@ sentry = { version = "0.45.0", default-features = false, features = [
|
||||
"rustls",
|
||||
] }
|
||||
sentry-actix = "0.45.0"
|
||||
serde = "1.0.228"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_bytes = "0.11.19"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
serde_json = "1.0.145"
|
||||
serde_with = "3.15.0"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
serde-xml-rs = "0.8.1"
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
spdx = "0.12.0"
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
syn = { version = "2.0.106", features = ["full"] }
|
||||
syn-miette = "0.3.0"
|
||||
sysinfo = { version = "0.37.2", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tauri = "2.8.5"
|
||||
@@ -187,6 +192,7 @@ url = "2.5.7"
|
||||
urlencoding = "2.1.3"
|
||||
uuid = "1.18.1"
|
||||
validator = "0.20.0"
|
||||
walkdir = "2.5.0"
|
||||
webp = { version = "0.3.1", default-features = false }
|
||||
webview2-com = "0.38.0" # Should be updated in lockstep with wry
|
||||
whoami = "1.6.1"
|
||||
@@ -194,7 +200,6 @@ windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
|
||||
windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
|
||||
winreg = "0.55.0"
|
||||
woothee = "0.13.0"
|
||||
yaserde = "0.12.0"
|
||||
zbus = "5.11.0"
|
||||
zip = { version = "6.0.0", default-features = false, features = [
|
||||
"bzip2",
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ hyper-util = { workspace = true }
|
||||
native-dialog = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
path-util = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
tauri = { workspace = true, features = [
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
**/*.rs
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "ariadne-extract"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
derive_more = { workspace = true, features = ["display"] }
|
||||
miette = { workspace = true, features = ["syntect-highlighter"] }
|
||||
proc-macro2 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
syn = { workspace = true, features = ["visit"] }
|
||||
syn-miette = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@modrinth/ariadne-extract",
|
||||
"scripts": {
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"lint:ancillary": "prettier --check .",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||
"fix:ancillary": "prettier --write .",
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||
},
|
||||
"prettier": "@modrinth/tooling-config/app-lib.prettier.config.cjs"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExtractError {
|
||||
#[error("I/O Error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Serialization: {0}")]
|
||||
SerDe(#[from] serde_json::Error),
|
||||
#[error("Walking directory: {0}")]
|
||||
WalkDir(#[from] walkdir::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ExtractError>;
|
||||
@@ -0,0 +1,657 @@
|
||||
use crate::error::Result;
|
||||
use derive_more::Display;
|
||||
use proc_macro2::Span;
|
||||
use serde::Serialize;
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::visit::Visit;
|
||||
use syn::{
|
||||
Attribute, File, Ident, ItemEnum, ItemMod, LitStr, Macro, Meta, Token,
|
||||
Type, braced, bracketed, parenthesized, token, visit,
|
||||
};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TranslationEntry {
|
||||
pub message: String,
|
||||
#[serde(skip)]
|
||||
key_span: Span,
|
||||
}
|
||||
|
||||
impl TranslationEntry {
|
||||
pub fn new(message: String, key_span: Span) -> Self {
|
||||
Self { message, key_span }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Extractor {
|
||||
include_tests: bool,
|
||||
output: BTreeMap<String, TranslationEntry>,
|
||||
errors: Vec<syn_miette::Error>,
|
||||
}
|
||||
|
||||
impl Extractor {
|
||||
pub fn new(include_tests: bool) -> Self {
|
||||
Self {
|
||||
output: BTreeMap::new(),
|
||||
include_tests,
|
||||
errors: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output(&self) -> &BTreeMap<String, TranslationEntry> {
|
||||
&self.output
|
||||
}
|
||||
|
||||
pub fn errors(&self) -> &Vec<syn_miette::Error> {
|
||||
&self.errors
|
||||
}
|
||||
}
|
||||
|
||||
impl Extractor {
|
||||
pub fn process_package(&mut self, package_path: &Path) -> Result<()> {
|
||||
for file in WalkDir::new(package_path.join("src")).min_depth(1) {
|
||||
let result = self.process_file(file);
|
||||
result?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_file(&mut self, file: walkdir::Result<DirEntry>) -> Result<()> {
|
||||
let file = file?;
|
||||
if !file.file_type().is_file()
|
||||
|| file.path().extension().is_none_or(|x| x != "rs")
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let file_contents = fs::read_to_string(file.path())?;
|
||||
let mut file_extractor = FileExtractor {
|
||||
extractor: self,
|
||||
file: FileInfo {
|
||||
path: file.path(),
|
||||
source: &file_contents,
|
||||
},
|
||||
enum_messages: HashMap::new(),
|
||||
};
|
||||
let parsed = match syn::parse_file(&file_contents) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
file_extractor.add_error(err);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
file_extractor.visit_file(&parsed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn include_item(&self, attrs: &[Attribute]) -> bool {
|
||||
fn include_from_meta(meta: &Meta, include_tests: bool) -> bool {
|
||||
if meta.path().is_ident("test") {
|
||||
include_tests
|
||||
} else if meta.path().is_ident("not") {
|
||||
let Ok(list) = meta.require_list() else {
|
||||
return true;
|
||||
};
|
||||
let Ok(inner) = list.parse_args() else {
|
||||
return true;
|
||||
};
|
||||
!include_from_meta(&inner, include_tests)
|
||||
} else if meta.path().is_ident("all") || meta.path().is_ident("any")
|
||||
{
|
||||
let Ok(list) = meta.require_list() else {
|
||||
return true;
|
||||
};
|
||||
let Ok(nested) = list.parse_args_with(
|
||||
Punctuated::<Meta, Token![,]>::parse_terminated,
|
||||
) else {
|
||||
return true;
|
||||
};
|
||||
if meta.path().is_ident("all") {
|
||||
nested.iter().all(|x| include_from_meta(x, include_tests))
|
||||
} else {
|
||||
nested.iter().any(|x| include_from_meta(x, include_tests))
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
fn include_from_attr(attr: &Attribute, include_tests: bool) -> bool {
|
||||
if !attr.path().is_ident("cfg") {
|
||||
return true;
|
||||
}
|
||||
let Ok(inner) = attr.parse_args() else {
|
||||
return true;
|
||||
};
|
||||
include_from_meta(&inner, include_tests)
|
||||
}
|
||||
attrs
|
||||
.iter()
|
||||
.all(|x| include_from_attr(x, self.include_tests))
|
||||
}
|
||||
|
||||
fn add_error(&mut self, file: &FileInfo, error: syn::Error) {
|
||||
self.errors.push(syn_miette::Error::new_named(
|
||||
error,
|
||||
file.source,
|
||||
file.path.display(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
struct FileExtractor<'a> {
|
||||
extractor: &'a mut Extractor,
|
||||
file: FileInfo<'a>,
|
||||
enum_messages: HashMap<Ident, HashMap<Ident, EnumMessage>>,
|
||||
}
|
||||
|
||||
struct FileInfo<'a> {
|
||||
path: &'a Path,
|
||||
source: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum EnumMessage {
|
||||
Absent {
|
||||
variant_span: Span,
|
||||
},
|
||||
Present {
|
||||
message: LitStr,
|
||||
display_attribute_type: DisplayAttributeType,
|
||||
},
|
||||
}
|
||||
|
||||
impl EnumMessage {
|
||||
fn as_option(&self) -> Option<(&LitStr, DisplayAttributeType)> {
|
||||
match self {
|
||||
Self::Absent { .. } => None,
|
||||
Self::Present {
|
||||
message,
|
||||
display_attribute_type,
|
||||
} => Some((message, *display_attribute_type)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Display)]
|
||||
enum DisplayAttributeType {
|
||||
#[display("#[error]")]
|
||||
ThiserrorError,
|
||||
#[display("#[display]")]
|
||||
DeriveMoreDisplay,
|
||||
}
|
||||
|
||||
impl FileExtractor<'_> {
|
||||
fn add_error(&mut self, error: syn::Error) {
|
||||
self.extractor.add_error(&self.file, error);
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit<'_> for FileExtractor<'_> {
|
||||
fn visit_file(&mut self, i: &File) {
|
||||
if self.extractor.include_item(&i.attrs) {
|
||||
visit::visit_file(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_item_enum(&mut self, i: &ItemEnum) {
|
||||
let mut variants = HashMap::new();
|
||||
let mut has_missing_variants = false;
|
||||
let mut ignored_variants = HashSet::new();
|
||||
let mut main_display_attribute_type = None;
|
||||
|
||||
for variant in &i.variants {
|
||||
let Some(error_attr) = variant.attrs.iter().find(|x| {
|
||||
x.path().is_ident("error") || x.path().is_ident("display")
|
||||
}) else {
|
||||
has_missing_variants = true;
|
||||
continue;
|
||||
};
|
||||
let display_attribute_type = if error_attr.path().is_ident("error")
|
||||
{
|
||||
DisplayAttributeType::ThiserrorError
|
||||
} else {
|
||||
DisplayAttributeType::DeriveMoreDisplay
|
||||
};
|
||||
if display_attribute_type
|
||||
!= main_display_attribute_type
|
||||
.get_or_insert_with(|| {
|
||||
(error_attr.path().span(), display_attribute_type)
|
||||
})
|
||||
.1
|
||||
{
|
||||
let (old_span, old_type) = main_display_attribute_type.unwrap();
|
||||
let mut error = syn::Error::new(
|
||||
error_attr.path().span(),
|
||||
format!(
|
||||
"inconsistent usage of {old_type} and {display_attribute_type}"
|
||||
),
|
||||
);
|
||||
error.combine(syn::Error::new(
|
||||
old_span,
|
||||
format!("{old_type} initially used here"),
|
||||
));
|
||||
self.add_error(error);
|
||||
}
|
||||
let error_message = error_attr
|
||||
.parse_args_with(|input: ParseStream| {
|
||||
if display_attribute_type
|
||||
== DisplayAttributeType::ThiserrorError
|
||||
&& input.peek(kw::transparent)
|
||||
{
|
||||
input.parse::<kw::transparent>()?;
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(input.parse::<LitStr>()?))
|
||||
}
|
||||
})
|
||||
.transpose();
|
||||
match error_message {
|
||||
Some(Ok(message)) => {
|
||||
variants.insert(
|
||||
variant.ident.clone(),
|
||||
EnumMessage::Present {
|
||||
message,
|
||||
display_attribute_type,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(Err(_)) => {
|
||||
self.add_error(syn::Error::new(
|
||||
error_attr.meta.span(),
|
||||
match display_attribute_type {
|
||||
DisplayAttributeType::ThiserrorError => {
|
||||
"only #[error(transparent)] and #[error(\"lone format string\")] syntaxes are supported"
|
||||
}
|
||||
DisplayAttributeType::DeriveMoreDisplay => {
|
||||
"only #[display(\"lone format string\")] syntax is supported"
|
||||
}
|
||||
},
|
||||
));
|
||||
ignored_variants.insert(variant.ident.clone());
|
||||
}
|
||||
None => {
|
||||
ignored_variants.insert(variant.ident.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if !variants.is_empty() {
|
||||
if has_missing_variants {
|
||||
for variant in &i.variants {
|
||||
if ignored_variants.contains(&variant.ident) {
|
||||
continue;
|
||||
}
|
||||
variants.entry(variant.ident.clone()).or_insert_with(
|
||||
|| match main_display_attribute_type.unwrap().1 {
|
||||
DisplayAttributeType::ThiserrorError => {
|
||||
EnumMessage::Absent {
|
||||
variant_span: variant.span(),
|
||||
}
|
||||
}
|
||||
DisplayAttributeType::DeriveMoreDisplay => {
|
||||
EnumMessage::Present {
|
||||
message: LitStr::new(
|
||||
&variant.ident.to_string(),
|
||||
variant.ident.span(),
|
||||
),
|
||||
display_attribute_type:
|
||||
DisplayAttributeType::DeriveMoreDisplay,
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
self.enum_messages.insert(i.ident.clone(), variants);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_item_mod(&mut self, i: &ItemMod) {
|
||||
if self.extractor.include_item(&i.attrs) {
|
||||
visit::visit_item_mod(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_macro(&mut self, i: &Macro) {
|
||||
if !i.path.is_ident("i18n_enum") {
|
||||
return;
|
||||
}
|
||||
let body = match i.parse_body() {
|
||||
Ok(body) => body,
|
||||
Err(err) => {
|
||||
self.add_error(err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
match body {
|
||||
I18nEnumMacroInvocation::Transparent => {}
|
||||
I18nEnumMacroInvocation::Enum {
|
||||
for_enum,
|
||||
root_key,
|
||||
variants,
|
||||
} => {
|
||||
let Some(messages) = self.enum_messages.get(&for_enum) else {
|
||||
return;
|
||||
};
|
||||
let root_key = root_key.value();
|
||||
for variant in variants {
|
||||
if variant.transparent.is_some() {
|
||||
continue;
|
||||
}
|
||||
let Some(message_literal) =
|
||||
messages.get(&variant.variant_name)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let (message, errors) = message_literal
|
||||
.as_option()
|
||||
.map_or_else(
|
||||
|| ("".into(), vec![format!("no default message specified for variant {}", variant.variant_name)]),
|
||||
|(message, display_attribute_type)| {
|
||||
variant.transform_format_string(
|
||||
message.value(),
|
||||
display_attribute_type,
|
||||
)
|
||||
},
|
||||
);
|
||||
let duplicate_key = match self
|
||||
.extractor
|
||||
.output
|
||||
.entry(format!("{}.{}", root_key, variant.key.value()))
|
||||
{
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(TranslationEntry::new(
|
||||
message,
|
||||
variant.key.span(),
|
||||
));
|
||||
None
|
||||
}
|
||||
Entry::Occupied(entry) => {
|
||||
(*entry.get().message != message).then(|| {
|
||||
(entry.key().clone(), entry.get().key_span)
|
||||
})
|
||||
}
|
||||
};
|
||||
if let Some((duplicate_key, original_span)) = duplicate_key
|
||||
{
|
||||
let mut error = syn::Error::new(
|
||||
variant.key.span(),
|
||||
format!(
|
||||
"duplicate variant key {duplicate_key} with differing messages"
|
||||
),
|
||||
);
|
||||
error.combine(syn::Error::new(
|
||||
original_span,
|
||||
"originally used here",
|
||||
));
|
||||
self.extractor.add_error(&self.file, error);
|
||||
}
|
||||
for error in errors {
|
||||
self.extractor.add_error(
|
||||
&self.file,
|
||||
syn::Error::new(
|
||||
match message_literal {
|
||||
EnumMessage::Absent { variant_span } => {
|
||||
*variant_span
|
||||
}
|
||||
EnumMessage::Present {
|
||||
message, ..
|
||||
} => message.span(),
|
||||
},
|
||||
error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum I18nEnumMacroInvocation {
|
||||
Transparent,
|
||||
Enum {
|
||||
for_enum: Ident,
|
||||
root_key: LitStr,
|
||||
variants: Punctuated<I18nEnumVariant, Token![,]>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Parse for I18nEnumMacroInvocation {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if input.peek(kw::transparent) {
|
||||
input.parse::<kw::transparent>()?;
|
||||
input.parse::<Ident>()?;
|
||||
let field_content;
|
||||
bracketed!(field_content in input);
|
||||
field_content.parse::<Ident>()?;
|
||||
field_content.parse::<Token![:]>()?;
|
||||
field_content.parse::<Type>()?;
|
||||
return Ok(I18nEnumMacroInvocation::Transparent);
|
||||
}
|
||||
|
||||
let for_enum = input.parse()?;
|
||||
input.parse::<Token![,]>()?;
|
||||
|
||||
input.parse::<kw::root_key>()?;
|
||||
input.parse::<Token![:]>()?;
|
||||
let root_key = input.parse()?;
|
||||
input.parse::<Token![,]>()?;
|
||||
|
||||
let variants =
|
||||
input.parse_terminated(I18nEnumVariant::parse, Token![,])?;
|
||||
|
||||
Ok(I18nEnumMacroInvocation::Enum {
|
||||
for_enum,
|
||||
root_key,
|
||||
variants,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct I18nEnumVariant {
|
||||
variant_name: Ident,
|
||||
transparent: Option<kw::transparent>,
|
||||
fields_type: FieldsType,
|
||||
fields: Punctuated<Ident, Token![,]>,
|
||||
key: LitStr,
|
||||
}
|
||||
|
||||
impl Parse for I18nEnumVariant {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let variant_name = input.parse()?;
|
||||
|
||||
let mut transparent = None;
|
||||
let mut fields = Punctuated::new();
|
||||
let fields_type;
|
||||
if input.peek(Token![!]) || input.peek(Token![=>]) {
|
||||
// Immediate => also flows into this case for a better error message
|
||||
fields_type = FieldsType::Unit;
|
||||
input.parse::<Token![!]>()?;
|
||||
} else {
|
||||
let content;
|
||||
if input.peek(token::Brace) {
|
||||
fields_type = FieldsType::Named;
|
||||
braced!(content in input);
|
||||
} else {
|
||||
fields_type = FieldsType::Tuple;
|
||||
parenthesized!(content in input);
|
||||
}
|
||||
if content.peek(Token![..]) {
|
||||
content.parse::<Token![..]>()?;
|
||||
} else if content.peek(kw::transparent) {
|
||||
transparent = Some(content.parse()?);
|
||||
content.parse::<Ident>()?;
|
||||
} else if content.peek(Ident) {
|
||||
loop {
|
||||
fields.push_value(content.parse()?);
|
||||
if !content.peek(Token![,]) || !content.peek2(Ident) {
|
||||
break;
|
||||
}
|
||||
fields.push_punct(content.parse()?);
|
||||
}
|
||||
}
|
||||
if fields_type == FieldsType::Named && content.peek(Token![,]) {
|
||||
content.parse::<Token![,]>()?;
|
||||
content.parse::<Token![..]>()?;
|
||||
}
|
||||
};
|
||||
|
||||
input.parse::<Token![=>]>()?;
|
||||
|
||||
let key = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
variant_name,
|
||||
transparent,
|
||||
fields_type,
|
||||
fields,
|
||||
key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl I18nEnumVariant {
|
||||
fn transform_format_string(
|
||||
&self,
|
||||
format_string: String,
|
||||
display_attribute_type: DisplayAttributeType,
|
||||
) -> (String, Vec<String>) {
|
||||
let mut errors = vec![];
|
||||
if !format_string.contains(['\'', '{', '}']) {
|
||||
return (format_string, errors);
|
||||
}
|
||||
|
||||
let known_names = if self.fields_type == FieldsType::Named {
|
||||
self.fields.iter().map(Ident::to_string).collect()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
let mut prev_push_index = 0;
|
||||
let mut format_start = None;
|
||||
let mut extra_format_layers = 0usize;
|
||||
let mut iter = format_string.bytes().enumerate().peekable();
|
||||
while let Some((index, char)) = iter.next() {
|
||||
if char == b'\'' {
|
||||
result.push_str(&format_string[prev_push_index..index + 1]);
|
||||
result.push('\'');
|
||||
prev_push_index = index + 1;
|
||||
}
|
||||
if char == b'{' {
|
||||
if format_start.is_some() {
|
||||
extra_format_layers += 1;
|
||||
continue;
|
||||
}
|
||||
result.push_str(&format_string[prev_push_index..index]);
|
||||
if matches!(iter.peek(), Some((_, b'{'))) {
|
||||
iter.next();
|
||||
result.push_str("'{'");
|
||||
prev_push_index = index + 2;
|
||||
} else {
|
||||
result.push('{');
|
||||
prev_push_index = index + 1;
|
||||
format_start = Some(index + 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if char == b'}' {
|
||||
if extra_format_layers > 0 {
|
||||
extra_format_layers -= 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(prev_start) = format_start {
|
||||
let format_variable = &format_string[prev_start..index];
|
||||
let format_variable = match format_variable.split_once(':')
|
||||
{
|
||||
Some((real_variable, _)) => {
|
||||
errors.push("format specifiers not allowed".into());
|
||||
real_variable
|
||||
}
|
||||
None => format_variable,
|
||||
};
|
||||
let format_variable = match self.fields_type {
|
||||
FieldsType::Unit => {
|
||||
errors.push(
|
||||
"formatting not supported for unit variants"
|
||||
.into(),
|
||||
);
|
||||
format_variable
|
||||
}
|
||||
FieldsType::Tuple => {
|
||||
let format_variable = match display_attribute_type {
|
||||
DisplayAttributeType::ThiserrorError => {
|
||||
format_variable
|
||||
}
|
||||
DisplayAttributeType::DeriveMoreDisplay => {
|
||||
match format_variable.strip_prefix('_') {
|
||||
Some(stripped) => stripped,
|
||||
None => {
|
||||
errors.push(format!("index format variable in #[display] must be prefixed with '_': {{_{format_variable}}}"));
|
||||
format_variable
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match format_variable
|
||||
.parse()
|
||||
.map(|x| self.fields.get(x))
|
||||
{
|
||||
Ok(Some(field)) => &field.to_string(),
|
||||
Ok(None) => {
|
||||
errors.push(format!("index format variable {{{format_variable}}} out of bounds"));
|
||||
format_variable
|
||||
}
|
||||
Err(_) => {
|
||||
errors.push(format!("invalid index format variable {{{format_variable}}} (must be usize)"));
|
||||
format_variable
|
||||
}
|
||||
}
|
||||
}
|
||||
FieldsType::Named => {
|
||||
if !known_names.contains(format_variable) {
|
||||
errors.push(format!("unknown format variable {{{format_variable}}}"))
|
||||
}
|
||||
format_variable
|
||||
}
|
||||
};
|
||||
result.push_str(format_variable);
|
||||
format_start = None;
|
||||
prev_push_index = index;
|
||||
continue;
|
||||
} else if matches!(iter.peek(), Some((_, b'}'))) {
|
||||
iter.next();
|
||||
result.push_str(&format_string[prev_push_index..index]);
|
||||
result.push_str("'}'");
|
||||
prev_push_index = index + 2;
|
||||
continue;
|
||||
} else {
|
||||
errors.push("unmatched } in format string".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push_str(&format_string[prev_push_index..]);
|
||||
|
||||
(result, errors)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum FieldsType {
|
||||
Unit,
|
||||
Tuple,
|
||||
Named,
|
||||
}
|
||||
|
||||
mod kw {
|
||||
use syn::custom_keyword;
|
||||
|
||||
custom_keyword!(transparent);
|
||||
custom_keyword!(root_key);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
mod error;
|
||||
mod extractor;
|
||||
|
||||
use crate::extractor::Extractor;
|
||||
use clap::Parser;
|
||||
use error::Result;
|
||||
use miette::GraphicalReportHandler;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
struct Args {
|
||||
/// The path to output the extracted translation data to
|
||||
#[clap(short, long)]
|
||||
out_file: PathBuf,
|
||||
|
||||
/// Include translations from tests. For simplicity, `cfg` above a module declarations are
|
||||
/// skipped.
|
||||
#[clap(short = 't', long)]
|
||||
include_tests: bool,
|
||||
|
||||
/// The paths to the directories containing Cargo.toml that should be extracted to the specified file
|
||||
packages: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match run(Args::parse()) {
|
||||
Ok(exit_code) => exit(exit_code),
|
||||
Err(err) => {
|
||||
eprintln!("Failed to run extractor:\n{err}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run(args: Args) -> Result<i32> {
|
||||
let mut extractor = Extractor::new(args.include_tests);
|
||||
for package in args.packages {
|
||||
extractor.process_package(&package)?;
|
||||
}
|
||||
if !extractor.errors().is_empty() {
|
||||
for error in extractor.errors() {
|
||||
let mut error_message = String::new();
|
||||
GraphicalReportHandler::new()
|
||||
.render_report(&mut error_message, error)
|
||||
.unwrap();
|
||||
eprintln!("{}", error.render());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = args.out_file.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let writer = File::create(args.out_file)?;
|
||||
serde_json::to_writer_pretty(writer, &extractor.output())?;
|
||||
|
||||
Ok(extractor.errors().len().try_into().unwrap_or(i32::MAX))
|
||||
}
|
||||
@@ -26,7 +26,7 @@ reqwest = { workspace = true, features = [
|
||||
"stream",
|
||||
] }
|
||||
rust-s3 = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde-xml-rs = { workspace = true }
|
||||
sha1_smol = { workspace = true }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO loader_field_enum_values (enum_id, value, created, metadata)\n VALUES ($1, $2, COALESCE($3, timezone('utc', now())), $4)\n ON CONFLICT (enum_id, value) DO UPDATE\n SET metadata = jsonb_set(\n COALESCE(loader_field_enum_values.metadata, $4),\n '{type}', \n COALESCE($4->'type', loader_field_enum_values.metadata->'type')\n ),\n created = COALESCE($3, loader_field_enum_values.created)\n RETURNING id\n ",
|
||||
"query": "\n INSERT INTO loader_field_enum_values (enum_id, value, created, metadata)\n VALUES ($1, $2, COALESCE($3, timezone('utc', now())), $4)\n ON CONFLICT (enum_id, value) DO UPDATE\n SET metadata = jsonb_set(\n COALESCE(loader_field_enum_values.metadata, $4),\n '{type}',\n COALESCE($4->'type', loader_field_enum_values.metadata->'type')\n ),\n created = COALESCE($3, loader_field_enum_values.created)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -21,5 +21,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "79c73369365ed7a09f4f48a87605d22db4a49ab5fd9943b54865448d0e9a8d67"
|
||||
"hash": "ad48051c9ac8f233ebfeeebec834ff96df8525a6ed872be7a64b8f349b36b276"
|
||||
}
|
||||
@@ -31,14 +31,15 @@ bitflags = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
censor = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap = { workspace = true }
|
||||
clickhouse = { workspace = true, features = ["time", "uuid"] }
|
||||
color-eyre = { workspace = true }
|
||||
color-thief = { workspace = true }
|
||||
console-subscriber = { workspace = true }
|
||||
const_format = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
deadpool-redis.workspace = true
|
||||
deadpool-redis = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
either = { workspace = true }
|
||||
eyre = { workspace = true }
|
||||
@@ -95,9 +96,10 @@ rustls.workspace = true
|
||||
rusty-money = { workspace = true }
|
||||
sentry = { workspace = true }
|
||||
sentry-actix = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
serde-xml-rs = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
spdx = { workspace = true, features = ["text"] }
|
||||
@@ -126,7 +128,6 @@ uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
|
||||
validator = { workspace = true, features = ["derive"] }
|
||||
webp = { workspace = true }
|
||||
woothee = { workspace = true }
|
||||
yaserde = { workspace = true, features = ["derive"] }
|
||||
zip = { workspace = true }
|
||||
zxcvbn = { workspace = true }
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"//": "runners we must remove useless development tools from the base image, which frees up ~20 GiB.",
|
||||
"//": "The command commented out below can be used in CI to debug what is taking up space:",
|
||||
"//": "sudo du -xh --max-depth=4 / | sort -rh | curl -X POST --data-urlencode content@/dev/fd/0 https://api.mclo.gs/1/log",
|
||||
"intl:extract": "cargo run --package ariadne-extract -- --out-file ../frontend/src/locales/en-US/labrinth.json .",
|
||||
"test": "if-ci sudo rm -rf /usr/local/lib/android /usr/local/.ghcup /opt/hostedtoolcache/CodeQL /usr/share/swift && cargo nextest run --all-targets --no-fail-fast"
|
||||
},
|
||||
"prettier": "@modrinth/tooling-config/labrinth.prettier.config.cjs"
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::database::models::{DBCollection, DBOrganization, DBTeamMember};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{DBProject, DBVersion, models};
|
||||
use crate::models::users::User;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use sqlx::PgPool;
|
||||
@@ -91,7 +91,7 @@ pub async fn filter_visible_project_ids(
|
||||
user_option: &Option<User>,
|
||||
pool: &PgPool,
|
||||
hide_unlisted: bool,
|
||||
) -> Result<Vec<crate::database::models::DBProjectId>, ApiError> {
|
||||
) -> Result<Vec<models::DBProjectId>, ApiError> {
|
||||
let mut return_projects = Vec::new();
|
||||
let mut check_projects = Vec::new();
|
||||
|
||||
@@ -127,7 +127,7 @@ pub async fn filter_enlisted_projects_ids(
|
||||
projects: Vec<&DBProject>,
|
||||
user_option: &Option<User>,
|
||||
pool: &PgPool,
|
||||
) -> Result<Vec<crate::database::models::DBProjectId>, ApiError> {
|
||||
) -> Result<Vec<models::DBProjectId>, ApiError> {
|
||||
let mut return_projects = vec![];
|
||||
|
||||
if let Some(user) = user_option {
|
||||
@@ -216,9 +216,8 @@ impl ValidateAuthorized for models::DBOAuthClient {
|
||||
return if user.role.is_mod() || user.id == self.created_by.into() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You don't have sufficient permissions to interact with this OAuth application"
|
||||
.to_string(),
|
||||
Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::InsufficientOAuthPermissions,
|
||||
))
|
||||
};
|
||||
}
|
||||
@@ -232,7 +231,7 @@ pub async fn filter_visible_version_ids(
|
||||
user_option: &Option<User>,
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<crate::database::models::DBVersionId>, ApiError> {
|
||||
) -> Result<Vec<models::DBVersionId>, ApiError> {
|
||||
let mut return_versions = Vec::new();
|
||||
|
||||
// First, filter out versions belonging to projects we can't see
|
||||
@@ -281,7 +280,7 @@ pub async fn filter_enlisted_version_ids(
|
||||
user_option: &Option<User>,
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<crate::database::models::DBVersionId>, ApiError> {
|
||||
) -> Result<Vec<models::DBVersionId>, ApiError> {
|
||||
let mut return_versions = Vec::new();
|
||||
|
||||
// Get project ids of versions
|
||||
|
||||
@@ -11,9 +11,10 @@ use serde::{Deserialize, Serialize};
|
||||
pub use validate::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
|
||||
use crate::file_hosting::FileHostingError;
|
||||
use crate::models::error::ApiError;
|
||||
use actix_web::HttpResponse;
|
||||
use crate::models::error::AsApiError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use ariadne::i18n_enum;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -50,7 +51,26 @@ pub enum AuthenticationError {
|
||||
Url,
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for AuthenticationError {
|
||||
i18n_enum!(
|
||||
AuthenticationError,
|
||||
root_key: "labrinth.error.unauthorized",
|
||||
Env(..) => "environment_error",
|
||||
Sqlx(cause) => "database_error.unknown",
|
||||
Database(cause) => "database_error",
|
||||
SerDe(cause) => "invalid_input",
|
||||
Reqwest(..) => "network_error",
|
||||
FileHosting(..) => "file_hosting",
|
||||
Decoding(cause) => "decoding_error",
|
||||
Mail(transparent cause) => "mail_error",
|
||||
InvalidCredentials! => "invalid_credentials",
|
||||
InvalidAuthMethod! => "invalid_auth_method",
|
||||
InvalidClientId! => "invalid_client_id",
|
||||
DuplicateUser! => "duplicate_user",
|
||||
SocketError! => "socket",
|
||||
Url! => "url_error",
|
||||
);
|
||||
|
||||
impl ResponseError for AuthenticationError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AuthenticationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -77,31 +97,7 @@ impl actix_web::ResponseError for AuthenticationError {
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(ApiError {
|
||||
error: self.error_name(),
|
||||
description: self.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticationError {
|
||||
pub fn error_name(&self) -> &'static str {
|
||||
match self {
|
||||
AuthenticationError::Env(..) => "environment_error",
|
||||
AuthenticationError::Sqlx(..) => "database_error",
|
||||
AuthenticationError::Database(..) => "database_error",
|
||||
AuthenticationError::SerDe(..) => "invalid_input",
|
||||
AuthenticationError::Reqwest(..) => "network_error",
|
||||
AuthenticationError::InvalidCredentials => "invalid_credentials",
|
||||
AuthenticationError::Decoding(..) => "decoding_error",
|
||||
AuthenticationError::Mail(..) => "mail_error",
|
||||
AuthenticationError::InvalidAuthMethod => "invalid_auth_method",
|
||||
AuthenticationError::InvalidClientId => "invalid_client_id",
|
||||
AuthenticationError::Url => "url_error",
|
||||
AuthenticationError::FileHosting(..) => "file_hosting",
|
||||
AuthenticationError::DuplicateUser => "duplicate_user",
|
||||
AuthenticationError::SocketError => "socket",
|
||||
}
|
||||
HttpResponse::build(self.status_code()).json(self.as_api_error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use super::ValidatedRedirectUri;
|
||||
use crate::auth::AuthenticationError;
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::error::AsApiError;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::http::{StatusCode, header::LOCATION};
|
||||
use ariadne::i18n::I18nEnum;
|
||||
use ariadne::i18n_enum;
|
||||
use ariadne::ids::DecodingError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("{}", .error_type)]
|
||||
#[error("{error_type}")]
|
||||
pub struct OAuthError {
|
||||
#[source]
|
||||
pub error_type: Box<OAuthErrorType>,
|
||||
@@ -15,6 +17,8 @@ pub struct OAuthError {
|
||||
pub valid_redirect_uri: Option<ValidatedRedirectUri>,
|
||||
}
|
||||
|
||||
i18n_enum!(transparent OAuthError[error_type: OAuthErrorType]);
|
||||
|
||||
impl<T> From<T> for OAuthError
|
||||
where
|
||||
T: Into<OAuthErrorType>,
|
||||
@@ -90,7 +94,13 @@ impl actix_web::ResponseError for OAuthError {
|
||||
redirect_uri = format!(
|
||||
"{}?error={}&error_description={}",
|
||||
redirect_uri,
|
||||
self.error_type.error_name(),
|
||||
self.error_type
|
||||
.translation_id()
|
||||
.split_once('.')
|
||||
.map_or_else(
|
||||
|| self.error_type.translation_id(),
|
||||
|(base, _)| base
|
||||
),
|
||||
self.error_type,
|
||||
);
|
||||
|
||||
@@ -102,10 +112,7 @@ impl actix_web::ResponseError for OAuthError {
|
||||
.append_header((LOCATION, redirect_uri.clone()))
|
||||
.body(redirect_uri)
|
||||
} else {
|
||||
HttpResponse::build(self.status_code()).json(ApiError {
|
||||
error: &self.error_type.error_name(),
|
||||
description: self.error_type.to_string(),
|
||||
})
|
||||
HttpResponse::build(self.status_code()).json(self.as_api_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +121,7 @@ impl actix_web::ResponseError for OAuthError {
|
||||
pub enum OAuthErrorType {
|
||||
#[error(transparent)]
|
||||
AuthenticationError(#[from] AuthenticationError),
|
||||
#[error("Client {} has no redirect URIs specified", .client_id.0)]
|
||||
#[error("Client {client_id} has no redirect URIs specified")]
|
||||
ClientMissingRedirectURI {
|
||||
client_id: crate::database::models::DBOAuthClientId,
|
||||
},
|
||||
@@ -154,6 +161,28 @@ pub enum OAuthErrorType {
|
||||
AccessDenied,
|
||||
}
|
||||
|
||||
// The names before the first period match
|
||||
// IETF RFC 6749 4.1.2.1 (https://datatracker.ietf.org/doc/html/rfc6749#autoid-38)
|
||||
// and 5.2 (https://datatracker.ietf.org/doc/html/rfc6749#section-5.2)
|
||||
i18n_enum!(
|
||||
OAuthErrorType,
|
||||
root_key: "labrinth.error.oauth",
|
||||
AuthenticationError(transparent cause) => "server_error",
|
||||
ClientMissingRedirectURI { client_id } => "invalid_uri.none_specified",
|
||||
RedirectUriNotConfigured(..) => "invalid_uri.none_configured",
|
||||
FailedScopeParse(cause) => "invalid_scope.parse",
|
||||
ScopesTooBroad! => "invalid_scope.too_broad",
|
||||
InvalidAcceptFlowId! => "server_error.invalid_flow_id",
|
||||
InvalidClientId(..) => "invalid_client.invalid_id",
|
||||
MalformedId(cause) => "invalid_request.malformed_id",
|
||||
ClientAuthenticationFailed! => "invalid_client.auth_failed",
|
||||
InvalidAuthCode! => "invalid_grant.invalid_auth_code",
|
||||
UnauthorizedClient! => "unauthorized_client",
|
||||
RedirectUriChanged(..) => "invalid_request.redirect_uri_mismatch",
|
||||
OnlySupportsAuthorizationCodeGrant(grant_type) => "invalid_grant.invalid_grant_type",
|
||||
AccessDenied! => "access_denied",
|
||||
);
|
||||
|
||||
impl From<crate::database::models::DatabaseError> for OAuthErrorType {
|
||||
fn from(value: crate::database::models::DatabaseError) -> Self {
|
||||
OAuthErrorType::AuthenticationError(value.into())
|
||||
@@ -165,29 +194,3 @@ impl From<sqlx::Error> for OAuthErrorType {
|
||||
OAuthErrorType::AuthenticationError(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl OAuthErrorType {
|
||||
pub fn error_name(&self) -> String {
|
||||
// IETF RFC 6749 4.1.2.1 (https://datatracker.ietf.org/doc/html/rfc6749#autoid-38)
|
||||
// And 5.2 (https://datatracker.ietf.org/doc/html/rfc6749#section-5.2)
|
||||
match self {
|
||||
Self::RedirectUriNotConfigured(_)
|
||||
| Self::ClientMissingRedirectURI { client_id: _ } => "invalid_uri",
|
||||
Self::AuthenticationError(_) | Self::InvalidAcceptFlowId => {
|
||||
"server_error"
|
||||
}
|
||||
Self::RedirectUriChanged(_) | Self::MalformedId(_) => {
|
||||
"invalid_request"
|
||||
}
|
||||
Self::FailedScopeParse(_) | Self::ScopesTooBroad => "invalid_scope",
|
||||
Self::InvalidClientId(_) | Self::ClientAuthenticationFailed => {
|
||||
"invalid_client"
|
||||
}
|
||||
Self::InvalidAuthCode
|
||||
| Self::OnlySupportsAuthorizationCodeGrant(_) => "invalid_grant",
|
||||
Self::UnauthorizedClient => "unauthorized_client",
|
||||
Self::AccessDenied => "access_denied",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ impl ErrorPage {
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for ErrorPage {
|
||||
impl ResponseError for ErrorPage {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
self.code
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{models::ids::ProjectId, routes::ApiError};
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::routes::error::ApiError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -61,7 +62,7 @@ pub async fn fetch_views(
|
||||
let query = client
|
||||
.query(
|
||||
"
|
||||
SELECT
|
||||
SELECT
|
||||
toUnixTimestamp(toStartOfInterval(recorded, toIntervalMinute(?))) AS time,
|
||||
project_id AS id,
|
||||
count(1) AS total
|
||||
@@ -91,7 +92,7 @@ pub async fn fetch_downloads(
|
||||
let query = client
|
||||
.query(
|
||||
"
|
||||
SELECT
|
||||
SELECT
|
||||
toUnixTimestamp(toStartOfInterval(recorded, toIntervalMinute(?))) AS time,
|
||||
project_id as id,
|
||||
count(1) AS total
|
||||
|
||||
@@ -15,6 +15,7 @@ use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlx_macros::Type;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
const ID_RETRY_COUNT: usize = 20;
|
||||
|
||||
@@ -110,6 +111,12 @@ macro_rules! impl_db_id_interface {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for $db_id_struct {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&$id_struct(self.0 as u64), f)
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
generate_ids!(
|
||||
$generator_function,
|
||||
|
||||
@@ -12,7 +12,7 @@ use serde_json::json;
|
||||
use crate::database::redis::RedisPool;
|
||||
|
||||
use super::{
|
||||
DatabaseError, LoaderFieldEnumValueId,
|
||||
DatabaseError, LoaderFieldEnumValueId, SchemaError,
|
||||
loader_fields::{
|
||||
LoaderFieldEnum, LoaderFieldEnumValue, VersionField, VersionFieldValue,
|
||||
},
|
||||
@@ -49,11 +49,7 @@ impl MinecraftGameVersion {
|
||||
let game_version_enum =
|
||||
LoaderFieldEnum::get(Self::FIELD_NAME, &mut *exec, redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
DatabaseError::SchemaError(
|
||||
"Could not find game version enum.".to_string(),
|
||||
)
|
||||
})?;
|
||||
.ok_or(SchemaError::MissingGameVersionEnum)?;
|
||||
let game_version_enum_values =
|
||||
LoaderFieldEnumValue::list(game_version_enum.id, &mut *exec, redis)
|
||||
.await?;
|
||||
@@ -81,16 +77,16 @@ impl MinecraftGameVersion {
|
||||
// Tries to create a MinecraftGameVersion from a VersionField
|
||||
// Clones on success
|
||||
pub fn try_from_version_field(
|
||||
version_field: &VersionField,
|
||||
version_field: VersionField,
|
||||
) -> Result<Vec<Self>, DatabaseError> {
|
||||
if version_field.field_name != Self::FIELD_NAME {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Field name {} is not {}",
|
||||
return Err(SchemaError::FieldNameMismatch(
|
||||
version_field.field_name,
|
||||
Self::FIELD_NAME
|
||||
)));
|
||||
Self::FIELD_NAME,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let game_versions = match version_field.clone() {
|
||||
let game_versions = match version_field {
|
||||
VersionField {
|
||||
value: VersionFieldValue::ArrayEnum(_, values),
|
||||
..
|
||||
@@ -102,9 +98,10 @@ impl MinecraftGameVersion {
|
||||
vec![Self::from_enum_value(value)]
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Game version requires field value to be an enum: {version_field:?}"
|
||||
)));
|
||||
return Err(SchemaError::GameVersionFieldNotEnum(Box::new(
|
||||
version_field,
|
||||
))
|
||||
.into());
|
||||
}
|
||||
};
|
||||
Ok(game_versions)
|
||||
@@ -185,9 +182,7 @@ impl<'a> MinecraftGameVersionBuilder<'a> {
|
||||
let game_versions_enum =
|
||||
LoaderFieldEnum::get("game_versions", exec, redis)
|
||||
.await?
|
||||
.ok_or(DatabaseError::SchemaError(
|
||||
"Missing loaders field: 'game_versions'".to_string(),
|
||||
))?;
|
||||
.ok_or(SchemaError::MissingGameVersionEnum)?;
|
||||
|
||||
// Get enum id for game versions
|
||||
let metadata = json!({
|
||||
@@ -205,7 +200,7 @@ impl<'a> MinecraftGameVersionBuilder<'a> {
|
||||
ON CONFLICT (enum_id, value) DO UPDATE
|
||||
SET metadata = jsonb_set(
|
||||
COALESCE(loader_field_enum_values.metadata, $4),
|
||||
'{type}',
|
||||
'{type}',
|
||||
COALESCE($4->'type', loader_field_enum_values.metadata->'type')
|
||||
),
|
||||
created = COALESCE($3, loader_field_enum_values.created)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hasher;
|
||||
|
||||
use super::DatabaseError;
|
||||
use super::ids::*;
|
||||
use super::{DatabaseError, SchemaError};
|
||||
use crate::database::redis::RedisPool;
|
||||
use ariadne::i18n_enum;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use dashmap::DashMap;
|
||||
use derive_more::Display;
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -203,7 +205,7 @@ pub struct LoaderField {
|
||||
pub max_val: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
|
||||
pub enum LoaderFieldType {
|
||||
Integer,
|
||||
Text,
|
||||
@@ -214,6 +216,7 @@ pub enum LoaderFieldType {
|
||||
ArrayEnum(LoaderFieldEnumId),
|
||||
ArrayBoolean,
|
||||
}
|
||||
|
||||
impl LoaderFieldType {
|
||||
pub fn build(
|
||||
field_type_name: &str,
|
||||
@@ -291,13 +294,17 @@ impl std::hash::Hash for LoaderFieldEnumValue {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(
|
||||
Clone, Serialize, Deserialize, Debug, Display, PartialEq, Eq, Hash,
|
||||
)]
|
||||
#[display("{field_name}: {}", value.field_type().to_str())]
|
||||
pub struct VersionField {
|
||||
pub version_id: DBVersionId,
|
||||
pub field_id: LoaderFieldId,
|
||||
pub field_name: String,
|
||||
pub value: VersionFieldValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum VersionFieldValue {
|
||||
Integer(i32),
|
||||
@@ -310,6 +317,25 @@ pub enum VersionFieldValue {
|
||||
ArrayBoolean(Vec<bool>),
|
||||
}
|
||||
|
||||
impl VersionFieldValue {
|
||||
pub fn field_type(&self) -> LoaderFieldType {
|
||||
match self {
|
||||
VersionFieldValue::Integer(_) => LoaderFieldType::Integer,
|
||||
VersionFieldValue::Text(_) => LoaderFieldType::Text,
|
||||
VersionFieldValue::Enum(enum_id, _) => {
|
||||
LoaderFieldType::Enum(*enum_id)
|
||||
}
|
||||
VersionFieldValue::Boolean(_) => LoaderFieldType::Boolean,
|
||||
VersionFieldValue::ArrayInteger(_) => LoaderFieldType::ArrayInteger,
|
||||
VersionFieldValue::ArrayText(_) => LoaderFieldType::ArrayText,
|
||||
VersionFieldValue::ArrayEnum(enum_id, _) => {
|
||||
LoaderFieldType::ArrayEnum(*enum_id)
|
||||
}
|
||||
VersionFieldValue::ArrayBoolean(_) => LoaderFieldType::ArrayBoolean,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct QueryVersionField {
|
||||
pub version_id: DBVersionId,
|
||||
@@ -784,7 +810,7 @@ impl VersionField {
|
||||
loader_field: LoaderField,
|
||||
value: serde_json::Value,
|
||||
enum_variants: Vec<LoaderFieldEnumValue>,
|
||||
) -> Result<VersionField, String> {
|
||||
) -> Result<VersionField, VersionFieldParseError> {
|
||||
let value =
|
||||
VersionFieldValue::parse(&loader_field, value, enum_variants)?;
|
||||
|
||||
@@ -804,20 +830,20 @@ impl VersionField {
|
||||
if let Some(min) = loader_field.min_val
|
||||
&& count < min
|
||||
{
|
||||
return Err(format!(
|
||||
"Provided value '{v}' for {field_name} is less than the minimum of {min}",
|
||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||
field_name = loader_field.field,
|
||||
return Err(VersionFieldParseError::ValueLessThanMinimum(
|
||||
serde_json::to_value(value).unwrap_or_default(),
|
||||
loader_field.field,
|
||||
min,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(max) = loader_field.max_val
|
||||
&& count > max
|
||||
{
|
||||
return Err(format!(
|
||||
"Provided value '{v}' for {field_name} is greater than the maximum of {max}",
|
||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||
field_name = loader_field.field,
|
||||
return Err(VersionFieldParseError::ValueGreaterThanMaximum(
|
||||
serde_json::to_value(value).unwrap_or_default(),
|
||||
loader_field.field,
|
||||
max,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -941,15 +967,16 @@ impl VersionFieldValue {
|
||||
loader_field: &LoaderField,
|
||||
value: serde_json::Value,
|
||||
enum_array: Vec<LoaderFieldEnumValue>,
|
||||
) -> Result<VersionFieldValue, String> {
|
||||
) -> Result<VersionFieldValue, VersionFieldParseError> {
|
||||
let field_name = &loader_field.field;
|
||||
let field_type = &loader_field.field_type;
|
||||
|
||||
let error_value = value.clone();
|
||||
let incorrect_type_error = |field_type: &str| {
|
||||
format!(
|
||||
"Provided value '{v}' for {field_name} could not be parsed to {field_type} ",
|
||||
v = serde_json::to_string(&error_value).unwrap_or_default()
|
||||
let incorrect_type_error = |field_type| {
|
||||
VersionFieldParseError::ParseFailure(
|
||||
error_value,
|
||||
field_name.to_string(),
|
||||
field_type,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -995,8 +1022,9 @@ impl VersionFieldValue {
|
||||
{
|
||||
ev
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Provided value '{enum_value}' is not a valid variant for {field_name}"
|
||||
return Err(VersionFieldParseError::InvalidEnumVariant(
|
||||
serde_json::Value::String(enum_value.to_string()),
|
||||
field_name.to_string(),
|
||||
));
|
||||
}
|
||||
}),
|
||||
@@ -1013,9 +1041,12 @@ impl VersionFieldValue {
|
||||
{
|
||||
enum_values.push(ev.clone());
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Provided value '{av}' is not a valid variant for {field_name}"
|
||||
));
|
||||
return Err(
|
||||
VersionFieldParseError::InvalidEnumVariant(
|
||||
serde_json::Value::String(av),
|
||||
field_name.to_string(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
enum_values
|
||||
@@ -1038,16 +1069,13 @@ impl VersionFieldValue {
|
||||
| LoaderFieldType::Enum(_) => {
|
||||
let mut fields = Self::build_many(field_type, qvfs, qlfev)?;
|
||||
if fields.len() > 1 {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Multiple fields for field {}",
|
||||
field_type.to_str()
|
||||
)));
|
||||
return Err(SchemaError::MultipleFields(
|
||||
field_type.to_str(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
fields.pop().ok_or_else(|| {
|
||||
DatabaseError::SchemaError(format!(
|
||||
"No version fields for field {}",
|
||||
field_type.to_str()
|
||||
))
|
||||
SchemaError::NoVersionFields(field_type.to_str()).into()
|
||||
})
|
||||
}
|
||||
LoaderFieldType::ArrayInteger
|
||||
@@ -1056,10 +1084,7 @@ impl VersionFieldValue {
|
||||
| LoaderFieldType::ArrayEnum(_) => {
|
||||
let fields = Self::build_many(field_type, qvfs, qlfev)?;
|
||||
Ok(fields.into_iter().next().ok_or_else(|| {
|
||||
DatabaseError::SchemaError(format!(
|
||||
"No version fields for field {}",
|
||||
field_type.to_str()
|
||||
))
|
||||
SchemaError::NoVersionFields(field_type.to_str())
|
||||
})?)
|
||||
}
|
||||
}
|
||||
@@ -1076,11 +1101,6 @@ impl VersionFieldValue {
|
||||
qlfev: &[QueryLoaderFieldEnumValue],
|
||||
) -> Result<Vec<(DBVersionId, VersionFieldValue)>, DatabaseError> {
|
||||
let field_name = field_type.to_str();
|
||||
let did_not_exist_error = |field_name: &str, desired_field: &str| {
|
||||
DatabaseError::SchemaError(format!(
|
||||
"Field name {desired_field} for field {field_name} in does not exist"
|
||||
))
|
||||
};
|
||||
|
||||
// Check errors- version_id must all be the same
|
||||
// If the field type is a non-array, then the reason for multiple version ids is that there are multiple versions being aggregated, and those version ids are contained within.
|
||||
@@ -1094,81 +1114,87 @@ impl VersionFieldValue {
|
||||
.unwrap_or(DBVersionId(0));
|
||||
|
||||
if qvfs.iter().map(|qvf| qvf.field_id).unique().count() > 1 {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Multiple field ids for field {field_name}"
|
||||
)));
|
||||
return Err(SchemaError::MultipleIdsForField(field_name).into());
|
||||
}
|
||||
|
||||
let mut value = match field_type {
|
||||
// Singleton fields
|
||||
// If there are multiple, we assume multiple versions are being concatenated
|
||||
LoaderFieldType::Integer => {
|
||||
qvfs.into_iter()
|
||||
let mut value =
|
||||
match field_type {
|
||||
// Singleton fields
|
||||
// If there are multiple, we assume multiple versions are being concatenated
|
||||
LoaderFieldType::Integer => qvfs
|
||||
.into_iter()
|
||||
.map(|qvf| {
|
||||
Ok((
|
||||
qvf.version_id,
|
||||
VersionFieldValue::Integer(qvf.int_value.ok_or(
|
||||
did_not_exist_error(field_name, "int_value"),
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"int_value",
|
||||
field_name,
|
||||
),
|
||||
)?),
|
||||
))
|
||||
})
|
||||
.collect::<Result<
|
||||
Vec<(DBVersionId, VersionFieldValue)>,
|
||||
DatabaseError,
|
||||
>>()?
|
||||
}
|
||||
LoaderFieldType::Text => {
|
||||
qvfs.into_iter()
|
||||
>>()?,
|
||||
LoaderFieldType::Text => qvfs
|
||||
.into_iter()
|
||||
.map(|qvf| {
|
||||
Ok((
|
||||
qvf.version_id,
|
||||
VersionFieldValue::Text(qvf.string_value.ok_or(
|
||||
did_not_exist_error(field_name, "string_value"),
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"string_value",
|
||||
field_name,
|
||||
),
|
||||
)?),
|
||||
))
|
||||
})
|
||||
.collect::<Result<
|
||||
Vec<(DBVersionId, VersionFieldValue)>,
|
||||
DatabaseError,
|
||||
>>()?
|
||||
}
|
||||
LoaderFieldType::Boolean => {
|
||||
qvfs.into_iter()
|
||||
>>()?,
|
||||
LoaderFieldType::Boolean => qvfs
|
||||
.into_iter()
|
||||
.map(|qvf| {
|
||||
Ok((
|
||||
qvf.version_id,
|
||||
VersionFieldValue::Boolean(
|
||||
qvf.int_value.ok_or(did_not_exist_error(
|
||||
field_name,
|
||||
"int_value",
|
||||
))? != 0,
|
||||
qvf.int_value.ok_or(
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"int_value",
|
||||
field_name,
|
||||
),
|
||||
)? != 0,
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect::<Result<
|
||||
Vec<(DBVersionId, VersionFieldValue)>,
|
||||
DatabaseError,
|
||||
>>()?
|
||||
}
|
||||
LoaderFieldType::Enum(id) => {
|
||||
qvfs.into_iter()
|
||||
>>()?,
|
||||
LoaderFieldType::Enum(id) => qvfs
|
||||
.into_iter()
|
||||
.map(|qvf| {
|
||||
Ok((
|
||||
qvf.version_id,
|
||||
VersionFieldValue::Enum(*id, {
|
||||
let enum_id = qvf.enum_value.ok_or(
|
||||
did_not_exist_error(
|
||||
field_name,
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"enum_value",
|
||||
field_name,
|
||||
),
|
||||
)?;
|
||||
let lfev = qlfev
|
||||
.iter()
|
||||
.find(|x| x.id == enum_id)
|
||||
.ok_or(did_not_exist_error(
|
||||
field_name,
|
||||
"enum_value",
|
||||
))?;
|
||||
.ok_or(
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"enum_value",
|
||||
field_name,
|
||||
),
|
||||
)?;
|
||||
LoaderFieldEnumValue {
|
||||
id: lfev.id,
|
||||
enum_id: lfev.enum_id,
|
||||
@@ -1186,84 +1212,94 @@ impl VersionFieldValue {
|
||||
.collect::<Result<
|
||||
Vec<(DBVersionId, VersionFieldValue)>,
|
||||
DatabaseError,
|
||||
>>()?
|
||||
}
|
||||
>>()?,
|
||||
|
||||
// Array fields
|
||||
// We concatenate into one array
|
||||
LoaderFieldType::ArrayInteger => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayInteger(
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
qvf.int_value.ok_or(did_not_exist_error(
|
||||
field_name,
|
||||
"int_value",
|
||||
))
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
LoaderFieldType::ArrayText => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayText(
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
qvf.string_value.ok_or(did_not_exist_error(
|
||||
field_name,
|
||||
"string_value",
|
||||
))
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
LoaderFieldType::ArrayBoolean => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayBoolean(
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
Ok::<bool, DatabaseError>(
|
||||
qvf.int_value.ok_or(did_not_exist_error(
|
||||
field_name,
|
||||
"int_value",
|
||||
))? != 0,
|
||||
)
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
LoaderFieldType::ArrayEnum(id) => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayEnum(
|
||||
*id,
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
let enum_id = qvf.enum_value.ok_or(
|
||||
did_not_exist_error(field_name, "enum_value"),
|
||||
)?;
|
||||
let lfev = qlfev
|
||||
.iter()
|
||||
.find(|x| x.id == enum_id)
|
||||
.ok_or(did_not_exist_error(
|
||||
field_name,
|
||||
"enum_value",
|
||||
))?;
|
||||
Ok::<_, DatabaseError>(LoaderFieldEnumValue {
|
||||
id: lfev.id,
|
||||
enum_id: lfev.enum_id,
|
||||
value: lfev.value.clone(),
|
||||
ordering: lfev.ordering,
|
||||
created: lfev.created,
|
||||
metadata: lfev
|
||||
.metadata
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
// Array fields
|
||||
// We concatenate into one array
|
||||
LoaderFieldType::ArrayInteger => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayInteger(
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
qvf.int_value.ok_or(
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"int_value",
|
||||
field_name,
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
};
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
LoaderFieldType::ArrayText => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayText(
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
qvf.string_value.ok_or(
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"string_value",
|
||||
field_name,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
LoaderFieldType::ArrayBoolean => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayBoolean(
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
Ok::<bool, DatabaseError>(
|
||||
qvf.int_value.ok_or(
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"int_value",
|
||||
field_name,
|
||||
),
|
||||
)? != 0,
|
||||
)
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
LoaderFieldType::ArrayEnum(id) => vec![(
|
||||
version_id,
|
||||
VersionFieldValue::ArrayEnum(
|
||||
*id,
|
||||
qvfs.into_iter()
|
||||
.map(|qvf| {
|
||||
let enum_id = qvf.enum_value.ok_or(
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"enum_value",
|
||||
field_name,
|
||||
),
|
||||
)?;
|
||||
let lfev = qlfev
|
||||
.iter()
|
||||
.find(|x| x.id == enum_id)
|
||||
.ok_or(
|
||||
SchemaError::FieldNameDoesNotExist(
|
||||
"enum_value",
|
||||
field_name,
|
||||
),
|
||||
)?;
|
||||
Ok::<_, DatabaseError>(LoaderFieldEnumValue {
|
||||
id: lfev.id,
|
||||
enum_id: lfev.enum_id,
|
||||
value: lfev.value.clone(),
|
||||
ordering: lfev.ordering,
|
||||
created: lfev.created,
|
||||
metadata: lfev
|
||||
.metadata
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
)],
|
||||
};
|
||||
|
||||
// Sort arrayenums by ordering, then by created
|
||||
for (_, v) in &mut value {
|
||||
@@ -1351,3 +1387,24 @@ impl VersionFieldValue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum VersionFieldParseError {
|
||||
#[error("Provided value {0} for {1} is less than the minimum of {2}")]
|
||||
ValueLessThanMinimum(serde_json::Value, String, i32),
|
||||
#[error("Provided value {0} for {1} is greater than the maximum of {2}")]
|
||||
ValueGreaterThanMaximum(serde_json::Value, String, i32),
|
||||
#[error("Provided value {0} for {1} could not be parsed to {2}")]
|
||||
ParseFailure(serde_json::Value, String, &'static str),
|
||||
#[error("Provided value {0} is not a valid variant for {1}")]
|
||||
InvalidEnumVariant(serde_json::Value, String),
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
VersionFieldParseError,
|
||||
root_key: "labrinth.error.version_field_parse",
|
||||
ValueLessThanMinimum(value, field_name, minimum) => "value_less_than_minimum",
|
||||
ValueGreaterThanMaximum(value, field_name, maximum) => "value_greater_than_maximum",
|
||||
ParseFailure(value, field_name, field_type) => "parse_failure",
|
||||
InvalidEnumVariant(value, field_name) => "invalid_enum_variant",
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ariadne::i18n_enum;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod affiliate_code_item;
|
||||
@@ -63,7 +64,49 @@ pub enum DatabaseError {
|
||||
#[error("Error while serializing with the cache: {0}")]
|
||||
SerdeCacheError(#[from] serde_json::Error),
|
||||
#[error("Schema error: {0}")]
|
||||
SchemaError(String),
|
||||
SchemaError(#[from] SchemaError),
|
||||
#[error("Timeout when waiting for cache subscriber")]
|
||||
CacheTimeout,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
DatabaseError,
|
||||
root_key: "labrinth.error.database",
|
||||
Database(cause) => "sqlx",
|
||||
RandomId! => "random_id",
|
||||
CacheError(cause) => "cache",
|
||||
RedisPool(cause) => "redis_pool",
|
||||
SerdeCacheError(cause) => "cache_serialization",
|
||||
SchemaError(cause) => "schema",
|
||||
CacheTimeout! => "cache_timeout",
|
||||
);
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SchemaError {
|
||||
#[error("Could not find game version enum.")]
|
||||
MissingGameVersionEnum,
|
||||
#[error("Field name {0} is not {1}")]
|
||||
FieldNameMismatch(String, &'static str),
|
||||
#[error("Game version requires field value to be an enum: {0}")]
|
||||
GameVersionFieldNotEnum(Box<loader_fields::VersionField>),
|
||||
#[error("Multiple fields for field {0}")]
|
||||
MultipleFields(&'static str),
|
||||
#[error("No version fields for field {0}")]
|
||||
NoVersionFields(&'static str),
|
||||
#[error("Multiple field ids for field {0}")]
|
||||
MultipleIdsForField(&'static str),
|
||||
#[error("Field name {0} for field {1} in does not exist")]
|
||||
FieldNameDoesNotExist(&'static str, &'static str),
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
SchemaError,
|
||||
root_key: "labrinth.error.database.schema",
|
||||
MissingGameVersionEnum! => "missing_game_version_enum",
|
||||
FieldNameMismatch(expected, actual) => "field_name_mismatch",
|
||||
GameVersionFieldNotEnum(field) => "game_version_field_not_enum",
|
||||
MultipleFields(field) => "multiple_fields",
|
||||
NoVersionFields(field) => "no_version_fields",
|
||||
MultipleIdsForField(field) => "multiple_ids_for_field",
|
||||
FieldNameDoesNotExist(field_name, field) => "field_name_does_not_exist",
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::{NotificationChannel, NotificationType};
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TEMPLATES_NAMESPACE: &str = "notifications_templates";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::database::models::ids::{DBProductId, DBProductPriceId};
|
||||
use crate::models::billing::ProductMetadata;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
|
||||
pub struct DBProductsTaxIdentifier {
|
||||
pub id: i32,
|
||||
|
||||
@@ -4,20 +4,51 @@ use thiserror::Error;
|
||||
mod mock;
|
||||
mod s3_host;
|
||||
|
||||
use ariadne::i18n_enum;
|
||||
use bytes::Bytes;
|
||||
use derive_more::Display;
|
||||
pub use mock::MockHost;
|
||||
pub use s3_host::{S3BucketConfig, S3Host};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FileHostingError {
|
||||
#[error("S3 error when {0}: {1}")]
|
||||
S3Error(&'static str, s3::error::S3Error),
|
||||
#[error("{0}: {1}")]
|
||||
S3Error(S3ErrorAction, s3::error::S3Error),
|
||||
#[error("File system error in file hosting: {0}")]
|
||||
FileSystemError(#[from] std::io::Error),
|
||||
#[error("Invalid Filename")]
|
||||
InvalidFilename,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
FileHostingError,
|
||||
root_key: "labrinth.error.file_hosting_error",
|
||||
S3Error(action, cause) => "s3",
|
||||
FileSystemError(cause) => "file_system",
|
||||
InvalidFilename! => "invalid_filename",
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display)]
|
||||
pub enum S3ErrorAction {
|
||||
#[display("S3 error when creating bucket instance")]
|
||||
CreatingBucketInstance,
|
||||
#[display("S3 error when uploading file")]
|
||||
UploadingFile,
|
||||
#[display("S3 error when generating presigned URL")]
|
||||
GeneratingPresignedUrl,
|
||||
#[display("S3 error when deleting file")]
|
||||
DeletingFile,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
S3ErrorAction,
|
||||
root_key: "labrinth.error.file_hosting_error.s3",
|
||||
CreatingBucketInstance! => "creating_bucket_instance",
|
||||
UploadingFile! => "uploading_file",
|
||||
GeneratingPresignedUrl! => "generating_presigned_url",
|
||||
DeletingFile! => "deleting_file",
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UploadFileData {
|
||||
pub file_name: String,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::file_hosting::{
|
||||
DeleteFileData, FileHost, FileHostPublicity, FileHostingError,
|
||||
UploadFileData,
|
||||
S3ErrorAction, UploadFileData,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
@@ -51,7 +51,10 @@ impl S3Host {
|
||||
},
|
||||
)
|
||||
.map_err(|e| {
|
||||
FileHostingError::S3Error("creating Bucket instance", e)
|
||||
FileHostingError::S3Error(
|
||||
S3ErrorAction::CreatingBucketInstance,
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
|
||||
bucket.name = config.name;
|
||||
@@ -97,7 +100,9 @@ impl FileHost for S3Host {
|
||||
content_type,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| FileHostingError::S3Error("uploading file", e))?;
|
||||
.map_err(|e| {
|
||||
FileHostingError::S3Error(S3ErrorAction::UploadingFile, e)
|
||||
})?;
|
||||
|
||||
Ok(UploadFileData {
|
||||
file_name: file_name.to_string(),
|
||||
@@ -121,7 +126,10 @@ impl FileHost for S3Host {
|
||||
.presign_get(format!("/{file_name}"), expiry_secs, None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
FileHostingError::S3Error("generating presigned URL", e)
|
||||
FileHostingError::S3Error(
|
||||
S3ErrorAction::GeneratingPresignedUrl,
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
Ok(url)
|
||||
}
|
||||
@@ -134,7 +142,9 @@ impl FileHost for S3Host {
|
||||
self.get_bucket(file_publicity)
|
||||
.delete_object(format!("/{file_name}"))
|
||||
.await
|
||||
.map_err(|e| FileHostingError::S3Error("deleting file", e))?;
|
||||
.map_err(|e| {
|
||||
FileHostingError::S3Error(S3ErrorAction::DeletingFile, e)
|
||||
})?;
|
||||
|
||||
Ok(DeleteFileData {
|
||||
file_name: file_name.to_string(),
|
||||
|
||||
@@ -12,6 +12,7 @@ use tracing::{info, warn};
|
||||
|
||||
extern crate clickhouse as clickhouse_crate;
|
||||
use clickhouse_crate::Client;
|
||||
use routes::error;
|
||||
use util::cors::default_cors;
|
||||
|
||||
use crate::background_task::update_versions;
|
||||
@@ -308,16 +309,16 @@ pub fn app_config(
|
||||
labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.app_data(web::FormConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
error::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::PathConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
error::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::QueryConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
error::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::JsonConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
error::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::Data::new(labrinth_config.redis_pool.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.pool.clone()))
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
use ariadne::i18n::{I18nEnum, TranslationData};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
/// An error returned by the API
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ApiError<'a> {
|
||||
pub error: &'a str,
|
||||
pub struct ApiError {
|
||||
pub error: &'static str,
|
||||
pub description: String,
|
||||
pub translatable_error: TranslationData,
|
||||
}
|
||||
|
||||
pub trait AsApiError {
|
||||
fn as_api_error(&self) -> ApiError;
|
||||
}
|
||||
|
||||
impl<T> AsApiError for T
|
||||
where
|
||||
T: I18nEnum + Display,
|
||||
{
|
||||
fn as_api_error(&self) -> ApiError {
|
||||
let translation_id = self.translation_id();
|
||||
ApiError {
|
||||
error: translation_id
|
||||
.split_once('.')
|
||||
.map_or(translation_id, |(base, _)| base),
|
||||
description: self.to_string(),
|
||||
translatable_error: self.translation_data(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ use crate::models::ids::{
|
||||
VersionId,
|
||||
};
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use ariadne::i18n::{I18nEnum, TranslationData};
|
||||
use ariadne::i18n_enum;
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -22,79 +25,114 @@ pub struct Notification {
|
||||
pub body: NotificationBody,
|
||||
|
||||
pub name: String,
|
||||
pub translatable_name: TranslationData,
|
||||
pub text: String,
|
||||
pub translatable_text: TranslationData,
|
||||
pub link: String,
|
||||
pub actions: Vec<NotificationAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq)]
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Display,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NotificationType {
|
||||
// If adding a notification type, add a variant in `NotificationBody` of the same name!
|
||||
#[display("A project you follow has been updated!")]
|
||||
ProjectUpdate,
|
||||
#[display("You have been invited to join a team!")]
|
||||
TeamInvite,
|
||||
#[display("You have been invited to join an organization!")]
|
||||
OrganizationInvite,
|
||||
#[display("Project status has changed")]
|
||||
StatusChange,
|
||||
#[display("A moderator has sent you a message!")]
|
||||
ModeratorMessage,
|
||||
// The notifications from here to down below are listed with messages for completeness' sake,
|
||||
// though they should never be sent via site notifications. This should be disabled via database
|
||||
// options. Messages should be reviewed and worded better if we want to distribute these notifications
|
||||
// via the site.
|
||||
#[display("LEGACY MARKDOWN NOTIFICATION")]
|
||||
LegacyMarkdown,
|
||||
#[display("Password reset requested")]
|
||||
ResetPassword,
|
||||
#[display("Verify your email")]
|
||||
VerifyEmail,
|
||||
#[display("Auth provider added")]
|
||||
AuthProviderAdded,
|
||||
#[display("Auth provider removed")]
|
||||
AuthProviderRemoved,
|
||||
#[display("Two-factor authentication enabled")]
|
||||
TwoFactorEnabled,
|
||||
#[display("Two-factor authentication removed")]
|
||||
TwoFactorRemoved,
|
||||
#[display("Password changed")]
|
||||
PasswordChanged,
|
||||
#[display("Password removed")]
|
||||
PasswordRemoved,
|
||||
#[display("Email changed")]
|
||||
EmailChanged,
|
||||
#[display("Payment failed")]
|
||||
PaymentFailed,
|
||||
#[display("Tax notification")]
|
||||
TaxNotification,
|
||||
#[display("New personal access token created")]
|
||||
PatCreated,
|
||||
#[display("New message in moderation thread")]
|
||||
ModerationMessageReceived,
|
||||
#[display("Report status updated")]
|
||||
ReportStatusUpdated,
|
||||
#[display("Report submitted")]
|
||||
ReportSubmitted,
|
||||
#[display("Project approved")]
|
||||
ProjectStatusApproved,
|
||||
#[display("Project status updated")]
|
||||
ProjectStatusNeutral,
|
||||
#[display("Project ownership transferred")]
|
||||
ProjectTransferred,
|
||||
#[display("Payout available")]
|
||||
PayoutAvailable,
|
||||
#[display("Notification")]
|
||||
Custom,
|
||||
#[display("")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
NotificationType,
|
||||
root_key: "labrinth.notification.type",
|
||||
ProjectUpdate! => "project_update",
|
||||
TeamInvite! => "team_invite",
|
||||
OrganizationInvite! => "organization_invite",
|
||||
StatusChange! => "status_change",
|
||||
ModeratorMessage! => "moderator_message",
|
||||
LegacyMarkdown! => "legacy_markdown",
|
||||
ResetPassword! => "reset_password",
|
||||
VerifyEmail! => "verify_email",
|
||||
AuthProviderAdded! => "auth_provider_added",
|
||||
AuthProviderRemoved! => "auth_provider_removed",
|
||||
TwoFactorEnabled! => "two_factor_enabled",
|
||||
TwoFactorRemoved! => "two_factor_removed",
|
||||
PasswordChanged! => "password_changed",
|
||||
PasswordRemoved! => "password_removed",
|
||||
EmailChanged! => "email_changed",
|
||||
PaymentFailed! => "payment_failed",
|
||||
TaxNotification! => "tax_notification",
|
||||
PatCreated! => "pat_created",
|
||||
ModerationMessageReceived! => "moderation_message_received",
|
||||
ReportStatusUpdated! => "report_status_updated",
|
||||
ReportSubmitted! => "report_submitted",
|
||||
ProjectStatusApproved! => "project_status_approved",
|
||||
ProjectStatusNeutral! => "project_status_neutral",
|
||||
ProjectTransferred! => "project_transferred",
|
||||
PayoutAvailable! => "payout_available",
|
||||
Custom! => "custom",
|
||||
Unknown! => "unknown",
|
||||
);
|
||||
|
||||
impl NotificationType {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
NotificationType::ProjectUpdate => "project_update",
|
||||
NotificationType::TeamInvite => "team_invite",
|
||||
NotificationType::OrganizationInvite => "organization_invite",
|
||||
NotificationType::StatusChange => "status_change",
|
||||
NotificationType::ModeratorMessage => "moderator_message",
|
||||
NotificationType::LegacyMarkdown => "legacy_markdown",
|
||||
NotificationType::ResetPassword => "reset_password",
|
||||
NotificationType::VerifyEmail => "verify_email",
|
||||
NotificationType::AuthProviderAdded => "auth_provider_added",
|
||||
NotificationType::AuthProviderRemoved => "auth_provider_removed",
|
||||
NotificationType::TwoFactorEnabled => "two_factor_enabled",
|
||||
NotificationType::TwoFactorRemoved => "two_factor_removed",
|
||||
NotificationType::PasswordChanged => "password_changed",
|
||||
NotificationType::PasswordRemoved => "password_removed",
|
||||
NotificationType::EmailChanged => "email_changed",
|
||||
NotificationType::PaymentFailed => "payment_failed",
|
||||
NotificationType::TaxNotification => "tax_notification",
|
||||
NotificationType::PatCreated => "pat_created",
|
||||
NotificationType::PayoutAvailable => "payout_available",
|
||||
NotificationType::ModerationMessageReceived => {
|
||||
"moderation_message_received"
|
||||
}
|
||||
NotificationType::ReportStatusUpdated => "report_status_updated",
|
||||
NotificationType::ReportSubmitted => "report_submitted",
|
||||
NotificationType::ProjectStatusApproved => {
|
||||
"project_status_approved"
|
||||
}
|
||||
NotificationType::Custom => "custom",
|
||||
NotificationType::ProjectStatusNeutral => "project_status_neutral",
|
||||
NotificationType::ProjectTransferred => "project_transferred",
|
||||
NotificationType::Unknown => "unknown",
|
||||
}
|
||||
self.translation_id()
|
||||
}
|
||||
|
||||
pub fn from_str_or_default(s: &str) -> Self {
|
||||
@@ -134,31 +172,40 @@ impl NotificationType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, Display)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum NotificationBody {
|
||||
#[display(
|
||||
"The project {project_id} has released a new version: {version_id}"
|
||||
)]
|
||||
ProjectUpdate {
|
||||
project_id: ProjectId,
|
||||
version_id: VersionId,
|
||||
},
|
||||
#[display("An invite has been sent for you to be {role} of a team")]
|
||||
TeamInvite {
|
||||
project_id: ProjectId,
|
||||
team_id: TeamId,
|
||||
invited_by: UserId,
|
||||
role: String,
|
||||
},
|
||||
#[display(
|
||||
"An invite has been sent for you to be {role} of an organization"
|
||||
)]
|
||||
OrganizationInvite {
|
||||
organization_id: OrganizationId,
|
||||
invited_by: UserId,
|
||||
team_id: TeamId,
|
||||
role: String,
|
||||
},
|
||||
#[display("Status has changed from {old_status} to {new_status}")]
|
||||
StatusChange {
|
||||
project_id: ProjectId,
|
||||
old_status: ProjectStatus,
|
||||
new_status: ProjectStatus,
|
||||
},
|
||||
/// This is for website notifications only. Email notifications have `ModerationMessageReceived`.
|
||||
#[display("Click on the link to read more.")]
|
||||
ModeratorMessage {
|
||||
thread_id: ThreadId,
|
||||
message_id: ThreadMessageId,
|
||||
@@ -166,33 +213,31 @@ pub enum NotificationBody {
|
||||
project_id: Option<ProjectId>,
|
||||
report_id: Option<ReportId>,
|
||||
},
|
||||
PatCreated {
|
||||
token_name: String,
|
||||
},
|
||||
#[display("Your personal access token '{token_name}' was created.")]
|
||||
PatCreated { token_name: String },
|
||||
/// This differs from ModeratorMessage as this notification is only for project threads and
|
||||
/// email notifications, not for site notifications.
|
||||
ModerationMessageReceived {
|
||||
project_id: ProjectId,
|
||||
},
|
||||
ReportStatusUpdated {
|
||||
report_id: ReportId,
|
||||
},
|
||||
ReportSubmitted {
|
||||
report_id: ReportId,
|
||||
},
|
||||
ProjectStatusApproved {
|
||||
project_id: ProjectId,
|
||||
},
|
||||
#[display("You have a new message in a moderation thread.")]
|
||||
ModerationMessageReceived { project_id: ProjectId },
|
||||
#[display("A report you are involved in has been updated.")]
|
||||
ReportStatusUpdated { report_id: ReportId },
|
||||
#[display("Your report was submitted successfully.")]
|
||||
ReportSubmitted { report_id: ReportId },
|
||||
#[display("Your project has been approved.")]
|
||||
ProjectStatusApproved { project_id: ProjectId },
|
||||
#[display("Your project status has been updated.")]
|
||||
ProjectStatusNeutral {
|
||||
project_id: ProjectId,
|
||||
old_status: ProjectStatus,
|
||||
new_status: ProjectStatus,
|
||||
},
|
||||
#[display("A project's ownership has been transferred.")]
|
||||
ProjectTransferred {
|
||||
project_id: ProjectId,
|
||||
new_owner_user_id: Option<UserId>,
|
||||
new_owner_organization_id: Option<OrganizationId>,
|
||||
},
|
||||
#[display("{text}")]
|
||||
LegacyMarkdown {
|
||||
notification_type: Option<String>,
|
||||
name: String,
|
||||
@@ -200,30 +245,34 @@ pub enum NotificationBody {
|
||||
link: String,
|
||||
actions: Vec<NotificationAction>,
|
||||
},
|
||||
ResetPassword {
|
||||
flow: String,
|
||||
},
|
||||
VerifyEmail {
|
||||
flow: String,
|
||||
},
|
||||
AuthProviderAdded {
|
||||
provider: String,
|
||||
},
|
||||
AuthProviderRemoved {
|
||||
provider: String,
|
||||
},
|
||||
#[display(
|
||||
"You've requested to reset your password. Please check your email for a reset link."
|
||||
)]
|
||||
ResetPassword { flow: String },
|
||||
// See comment above VerifyEmail in NotificationType
|
||||
#[display(
|
||||
"You've requested to verify your email. Please check your email for a verification link."
|
||||
)]
|
||||
VerifyEmail { flow: String },
|
||||
#[display("You've added a new authentication provider to your account.")]
|
||||
AuthProviderAdded { provider: String },
|
||||
#[display("You've removed a authentication provider from your account.")]
|
||||
AuthProviderRemoved { provider: String },
|
||||
#[display("You've enabled two-factor authentication on your account.")]
|
||||
TwoFactorEnabled,
|
||||
#[display("You've removed two-factor authentication from your account.")]
|
||||
TwoFactorRemoved,
|
||||
#[display("You've changed your account password.")]
|
||||
PasswordChanged,
|
||||
#[display("You've removed your account password.")]
|
||||
PasswordRemoved,
|
||||
EmailChanged {
|
||||
new_email: String,
|
||||
to_email: String,
|
||||
},
|
||||
PaymentFailed {
|
||||
amount: String,
|
||||
service: String,
|
||||
},
|
||||
#[display("Your account email was changed.")]
|
||||
EmailChanged { new_email: String, to_email: String },
|
||||
#[display(
|
||||
"A payment on your account failed. Please update your billing information."
|
||||
)]
|
||||
PaymentFailed { amount: String, service: String },
|
||||
#[display("You've received a tax notification.")]
|
||||
TaxNotification {
|
||||
subscription_id: UserSubscriptionId,
|
||||
new_amount: i64,
|
||||
@@ -235,18 +284,53 @@ pub enum NotificationBody {
|
||||
due: DateTime<Utc>,
|
||||
service: String,
|
||||
},
|
||||
#[display("A payout is available!")]
|
||||
PayoutAvailable {
|
||||
date_available: DateTime<Utc>,
|
||||
amount: u64,
|
||||
},
|
||||
#[display("{title}")]
|
||||
Custom {
|
||||
key: String,
|
||||
title: String,
|
||||
body_md: String,
|
||||
},
|
||||
#[display("")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
NotificationBody,
|
||||
root_key: "labrinth.notification.body",
|
||||
ProjectUpdate { project_id, version_id } => "project_update",
|
||||
TeamInvite { role, .. } => "team_invite",
|
||||
OrganizationInvite { role, .. } => "organization_invite",
|
||||
StatusChange { old_status, new_status, .. } => "status_change",
|
||||
ModeratorMessage { .. } => "moderator_message",
|
||||
PatCreated { token_name } => "pat_created",
|
||||
ModerationMessageReceived { .. } => "moderation_message_received",
|
||||
ReportStatusUpdated { .. } => "report_status_updated",
|
||||
ReportSubmitted { .. } => "report_submitted",
|
||||
ProjectStatusApproved { .. } => "project_status_approved",
|
||||
ProjectStatusNeutral { .. } => "project_status_neutral",
|
||||
ProjectTransferred { .. } => "project_transferred",
|
||||
LegacyMarkdown { transparent text } => "legacy_markdown",
|
||||
ResetPassword { .. } => "reset_password",
|
||||
VerifyEmail { .. } => "verify_email",
|
||||
AuthProviderAdded { .. } => "auth_provider_added",
|
||||
AuthProviderRemoved { .. } => "auth_provider_removed",
|
||||
TwoFactorEnabled! => "two_factor_enabled",
|
||||
TwoFactorRemoved! => "two_factor_removed",
|
||||
PasswordChanged! => "password_changed",
|
||||
PasswordRemoved! => "password_removed",
|
||||
EmailChanged { .. } => "email_changed",
|
||||
PaymentFailed { .. } => "payment_failed",
|
||||
TaxNotification { .. } => "tax_notification",
|
||||
PayoutAvailable { .. } => "payout_available",
|
||||
Custom { transparent title } => "custom",
|
||||
Unknown! => "unknown",
|
||||
);
|
||||
|
||||
impl NotificationBody {
|
||||
pub fn notification_type(&self) -> NotificationType {
|
||||
match &self {
|
||||
@@ -329,29 +413,20 @@ impl NotificationBody {
|
||||
|
||||
impl From<DBNotification> for Notification {
|
||||
fn from(notif: DBNotification) -> Self {
|
||||
let (name, text, link, actions) = {
|
||||
let (link, actions) = {
|
||||
match ¬if.body {
|
||||
NotificationBody::ProjectUpdate {
|
||||
project_id,
|
||||
version_id,
|
||||
} => (
|
||||
"A project you follow has been updated!".to_string(),
|
||||
format!(
|
||||
"The project {project_id} has released a new version: {version_id}"
|
||||
),
|
||||
format!("/project/{project_id}/version/{version_id}"),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::TeamInvite {
|
||||
project_id,
|
||||
role,
|
||||
team_id,
|
||||
..
|
||||
} => (
|
||||
"You have been invited to join a team!".to_string(),
|
||||
format!(
|
||||
"An invite has been sent for you to be {role} of a team"
|
||||
),
|
||||
format!("/project/{project_id}"),
|
||||
vec![
|
||||
NotificationAction {
|
||||
@@ -375,15 +450,9 @@ impl From<DBNotification> for Notification {
|
||||
),
|
||||
NotificationBody::OrganizationInvite {
|
||||
organization_id,
|
||||
role,
|
||||
team_id,
|
||||
..
|
||||
} => (
|
||||
"You have been invited to join an organization!"
|
||||
.to_string(),
|
||||
format!(
|
||||
"An invite has been sent for you to be {role} of an organization"
|
||||
),
|
||||
format!("/organization/{organization_id}"),
|
||||
vec![
|
||||
NotificationAction {
|
||||
@@ -405,27 +474,14 @@ impl From<DBNotification> for Notification {
|
||||
},
|
||||
],
|
||||
),
|
||||
NotificationBody::StatusChange {
|
||||
old_status,
|
||||
new_status,
|
||||
project_id,
|
||||
} => (
|
||||
"Project status has changed".to_string(),
|
||||
format!(
|
||||
"Status has changed from {} to {}",
|
||||
old_status.as_friendly_str(),
|
||||
new_status.as_friendly_str()
|
||||
),
|
||||
format!("/project/{project_id}"),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::StatusChange { project_id, .. } => {
|
||||
(format!("/project/{project_id}"), vec![])
|
||||
}
|
||||
NotificationBody::ModeratorMessage {
|
||||
project_id,
|
||||
report_id,
|
||||
..
|
||||
} => (
|
||||
"A moderator has sent you a message!".to_string(),
|
||||
"Click on the link to read more.".to_string(),
|
||||
if let Some(project_id) = project_id {
|
||||
format!("/project/{project_id}")
|
||||
} else if let Some(report_id) = report_id {
|
||||
@@ -435,148 +491,26 @@ impl From<DBNotification> for Notification {
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
// The notifications from here to down below are listed with messages for completeness' sake,
|
||||
// though they should never be sent via site notifications. This should be disabled via database
|
||||
// options. Messages should be reviewed and worded better if we want to distribute these notifications
|
||||
// via the site.
|
||||
NotificationBody::PatCreated { token_name } => (
|
||||
"New personal access token created".to_string(),
|
||||
format!("Your personal access token '{token_name}' was created."),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ReportStatusUpdated { .. } => (
|
||||
"Report status updated".to_string(),
|
||||
"A report you are involved in has been updated.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ReportSubmitted { .. } => (
|
||||
"Report submitted".to_string(),
|
||||
"Your report was submitted successfully.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ProjectStatusApproved { .. } => (
|
||||
"Project approved".to_string(),
|
||||
"Your project has been approved.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ProjectStatusNeutral { .. } => (
|
||||
"Project status updated".to_string(),
|
||||
"Your project status has been updated.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ProjectTransferred { .. } => (
|
||||
"Project ownership transferred".to_string(),
|
||||
"A project's ownership has been transferred.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
// Don't expose the `flow` field
|
||||
NotificationBody::ResetPassword { .. } => (
|
||||
"Password reset requested".to_string(),
|
||||
"You've requested to reset your password. Please check your email for a reset link.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::LegacyMarkdown {
|
||||
name,
|
||||
text,
|
||||
link,
|
||||
actions,
|
||||
..
|
||||
} => (
|
||||
name.clone(),
|
||||
text.clone(),
|
||||
link.clone(),
|
||||
actions.clone().into_iter().collect(),
|
||||
),
|
||||
NotificationBody::PaymentFailed { .. } => (
|
||||
"Payment failed".to_string(),
|
||||
"A payment on your account failed. Please update your billing information.".to_string(),
|
||||
"/settings/billing".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::VerifyEmail { .. } => (
|
||||
"Verify your email".to_string(),
|
||||
"You've requested to verify your email. Please check your email for a verification link.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::AuthProviderAdded { .. } => (
|
||||
"Auth provider added".to_string(),
|
||||
"You've added a new authentication provider to your account.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::AuthProviderRemoved { .. } => (
|
||||
"Auth provider removed".to_string(),
|
||||
"You've removed a authentication provider from your account.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::TwoFactorEnabled => (
|
||||
"Two-factor authentication enabled".to_string(),
|
||||
"You've enabled two-factor authentication on your account.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::TwoFactorRemoved => (
|
||||
"Two-factor authentication removed".to_string(),
|
||||
"You've removed two-factor authentication from your account.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::PasswordChanged => (
|
||||
"Password changed".to_string(),
|
||||
"You've changed your account password.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::PasswordRemoved => (
|
||||
"Password removed".to_string(),
|
||||
"You've removed your account password.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::EmailChanged { .. } => (
|
||||
"Email changed".to_string(),
|
||||
"Your account email was changed.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::TaxNotification { .. } => (
|
||||
"Tax notification".to_string(),
|
||||
"You've received a tax notification.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::PayoutAvailable { .. } => (
|
||||
"Payout available".to_string(),
|
||||
"A payout is available!".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ModerationMessageReceived { .. } => (
|
||||
"New message in moderation thread".to_string(),
|
||||
"You have a new message in a moderation thread.".to_string(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::Custom { title, .. } => (
|
||||
"Notification".to_string(),
|
||||
title.clone(),
|
||||
"#".to_string(),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::Unknown => {
|
||||
("".to_string(), "".to_string(), "#".to_string(), vec![])
|
||||
NotificationBody::LegacyMarkdown { link, actions, .. } => {
|
||||
(link.clone(), actions.clone().into_iter().collect())
|
||||
}
|
||||
NotificationBody::PaymentFailed { .. } => {
|
||||
("/settings/billing".to_string(), vec![])
|
||||
}
|
||||
_ => ("#".to_string(), vec![]),
|
||||
}
|
||||
};
|
||||
let (name, translatable_name) = match ¬if.body {
|
||||
NotificationBody::LegacyMarkdown { name, .. } => {
|
||||
(name.clone(), TranslationData::Literal(name.clone()))
|
||||
}
|
||||
_ => (
|
||||
notif.body.notification_type().to_string(),
|
||||
notif.body.notification_type().translation_data(),
|
||||
),
|
||||
};
|
||||
let text = notif.body.to_string();
|
||||
let translatable_text = notif.body.translation_data();
|
||||
|
||||
Self {
|
||||
id: notif.id.into(),
|
||||
@@ -586,7 +520,9 @@ impl From<DBNotification> for Notification {
|
||||
created: notif.created,
|
||||
|
||||
name,
|
||||
translatable_name,
|
||||
text,
|
||||
translatable_text,
|
||||
link,
|
||||
actions,
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ use crate::database::models::version_item::VersionQueryResult;
|
||||
use crate::models::ids::{
|
||||
OrganizationId, ProjectId, TeamId, ThreadId, VersionId,
|
||||
};
|
||||
use ariadne::i18n::I18nEnum;
|
||||
use ariadne::i18n_enum;
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::Display;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
@@ -417,22 +420,26 @@ impl From<LinkUrl> for Link {
|
||||
}
|
||||
|
||||
/// A status decides the visibility of a project in search, URLs, and the whole site itself.
|
||||
/// Approved - Project is displayed on search, and accessible by URL
|
||||
/// Rejected - Project is not displayed on search, and not accessible by URL (Temporary state, project can reapply)
|
||||
/// Draft - Project is not displayed on search, and not accessible by URL
|
||||
/// Unlisted - Project is not displayed on search, but accessible by URL
|
||||
/// Withheld - Same as unlisted, but set by a moderator. Cannot be switched to another type without moderator approval
|
||||
/// Processing - Project is not displayed on search, and not accessible by URL (Temporary state, project under review)
|
||||
/// Scheduled - Project is scheduled to be released in the future
|
||||
/// Private - Project is approved, but is not viewable to the public
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
/// - Approved - Project is displayed on search, and accessible by URL
|
||||
/// - Rejected - Project is not displayed on search, and not accessible by URL (Temporary state, project can reapply)
|
||||
/// - Draft - Project is not displayed on search, and not accessible by URL
|
||||
/// - Unlisted - Project is not displayed on search, but accessible by URL
|
||||
/// - Withheld - Same as unlisted, but set by a moderator. Cannot be switched to another type without moderator approval
|
||||
/// - Processing - Project is not displayed on search, and not accessible by URL (Temporary state, project under review)
|
||||
/// - Scheduled - Project is scheduled to be released in the future
|
||||
/// - Private - Project is approved, but is not viewable to the public
|
||||
#[derive(
|
||||
Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug, Display,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProjectStatus {
|
||||
#[display("Listed")]
|
||||
Approved,
|
||||
Archived,
|
||||
Rejected,
|
||||
Draft,
|
||||
Unlisted,
|
||||
#[display("Under review")]
|
||||
Processing,
|
||||
Withheld,
|
||||
Scheduled,
|
||||
@@ -440,11 +447,20 @@ pub enum ProjectStatus {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProjectStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
i18n_enum!(
|
||||
ProjectStatus,
|
||||
root_key: "labrinth.project_status",
|
||||
Approved! => "approved",
|
||||
Archived! => "archived",
|
||||
Rejected! => "rejected",
|
||||
Draft! => "draft",
|
||||
Unlisted! => "unlisted",
|
||||
Processing! => "processing",
|
||||
Withheld! => "withheld",
|
||||
Scheduled! => "scheduled",
|
||||
Private! => "private",
|
||||
Unknown! => "unknown",
|
||||
);
|
||||
|
||||
impl ProjectStatus {
|
||||
pub fn from_string(string: &str) -> ProjectStatus {
|
||||
@@ -460,33 +476,9 @@ impl ProjectStatus {
|
||||
_ => ProjectStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ProjectStatus::Approved => "approved",
|
||||
ProjectStatus::Rejected => "rejected",
|
||||
ProjectStatus::Draft => "draft",
|
||||
ProjectStatus::Unlisted => "unlisted",
|
||||
ProjectStatus::Processing => "processing",
|
||||
ProjectStatus::Unknown => "unknown",
|
||||
ProjectStatus::Archived => "archived",
|
||||
ProjectStatus::Withheld => "withheld",
|
||||
ProjectStatus::Scheduled => "scheduled",
|
||||
ProjectStatus::Private => "private",
|
||||
}
|
||||
}
|
||||
pub fn as_friendly_str(&self) -> &'static str {
|
||||
match self {
|
||||
ProjectStatus::Approved => "Listed",
|
||||
ProjectStatus::Rejected => "Rejected",
|
||||
ProjectStatus::Draft => "Draft",
|
||||
ProjectStatus::Unlisted => "Unlisted",
|
||||
ProjectStatus::Processing => "Under review",
|
||||
ProjectStatus::Unknown => "Unknown",
|
||||
ProjectStatus::Archived => "Archived",
|
||||
ProjectStatus::Withheld => "Withheld",
|
||||
ProjectStatus::Scheduled => "Scheduled",
|
||||
ProjectStatus::Private => "Private",
|
||||
}
|
||||
self.translation_id()
|
||||
}
|
||||
|
||||
pub fn iterator() -> impl Iterator<Item = ProjectStatus> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::analytics::{Download, PageView, Playtime};
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use redis::cmd;
|
||||
use sqlx::PgPool;
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::models::billing::{
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::users::Badges;
|
||||
use crate::models::users::User;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::internal::billing::payments::*;
|
||||
use crate::util::anrok;
|
||||
use crate::util::archon::ArchonClient;
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::models::notifications::{NotificationBody, NotificationType};
|
||||
use crate::models::v3::notifications::{
|
||||
NotificationChannel, NotificationDeliveryStatus,
|
||||
};
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use ariadne::i18n_enum;
|
||||
use chrono::Utc;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use lettre::message::Mailbox;
|
||||
@@ -115,6 +116,17 @@ pub enum MailError {
|
||||
HttpTemplate(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
MailError,
|
||||
root_key: "labrinth.error.mail",
|
||||
Env(..) => "environment",
|
||||
Mail(cause) => "email",
|
||||
Address(cause) => "address",
|
||||
Smtp(cause) => "smtp",
|
||||
Uninitialized! => "uninitialized",
|
||||
HttpTemplate(cause) => "http_template",
|
||||
);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EmailQueue {
|
||||
pg: PgPool,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::database::models::{
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::NotificationBody;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::error::Context;
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use futures::TryFutureExt;
|
||||
|
||||
@@ -3,12 +3,13 @@ use crate::database;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::error::AsApiError;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pack::{PackFile, PackFileHash, PackFormat};
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use dashmap::DashSet;
|
||||
use hex::ToHex;
|
||||
use itertools::Itertools;
|
||||
@@ -674,9 +675,8 @@ impl AutomatedModerationQueue {
|
||||
format!(
|
||||
"*<{}/user/AutoMod|AutoMod>* changed project status from *{}* to *Rejected*",
|
||||
dotenvy::var("SITE_URL")?,
|
||||
&project.inner.status.as_friendly_str(),
|
||||
)
|
||||
.to_string(),
|
||||
&project.inner.status,
|
||||
),
|
||||
),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::models::payouts::{
|
||||
PayoutMethodType,
|
||||
};
|
||||
use crate::models::projects::MonetizationStatus;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::webhook::{
|
||||
PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
|
||||
};
|
||||
@@ -882,8 +882,8 @@ pub async fn process_payout(
|
||||
MonetizationStatus::Monetized.as_str(),
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| !x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.fetch(&mut *transaction)
|
||||
.try_fold(DashMap::new(), |acc: DashMap<i64, HashMap<i64, Decimal>>, r| {
|
||||
@@ -905,7 +905,7 @@ pub async fn process_payout(
|
||||
MonetizationStatus::Monetized.as_str(),
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| !x.is_hidden())
|
||||
.map(|x| x.to_string())
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch(&mut *transaction)
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::queue::analytics::AnalyticsQueue;
|
||||
use crate::queue::maxmind::MaxMindIndexer;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, ApiInvalidInput};
|
||||
use crate::util::date::get_current_tenths_of_ms;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
@@ -66,12 +66,11 @@ pub async fn page_view_ingest(
|
||||
.ok();
|
||||
let conn_info = req.connection_info().peer_addr().map(|x| x.to_string());
|
||||
|
||||
let url = Url::parse(&url_input.url).map_err(|_| {
|
||||
ApiError::InvalidInput("invalid page view URL specified!".to_string())
|
||||
})?;
|
||||
let domain = url.host_str().ok_or_else(|| {
|
||||
ApiError::InvalidInput("invalid page view URL specified!".to_string())
|
||||
})?;
|
||||
let url = Url::parse(&url_input.url)
|
||||
.map_err(|_| ApiError::InvalidInput(ApiInvalidInput::PageViewUrl))?;
|
||||
let domain = url
|
||||
.host_str()
|
||||
.ok_or_else(|| ApiError::InvalidInput(ApiInvalidInput::PageViewUrl))?;
|
||||
let url_origin = url.origin().ascii_serialization();
|
||||
|
||||
let is_valid_url_origin =
|
||||
@@ -81,9 +80,7 @@ pub async fn page_view_ingest(
|
||||
.any(|origin| origin == "*" || url_origin == *origin);
|
||||
|
||||
if !is_valid_url_origin {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"invalid page view URL specified!".to_string(),
|
||||
));
|
||||
return Err(ApiError::InvalidInput(ApiInvalidInput::PageViewUrl));
|
||||
}
|
||||
|
||||
let headers = req
|
||||
@@ -190,9 +187,7 @@ pub async fn playtime_ingest(
|
||||
let playtimes = playtime_input.0;
|
||||
|
||||
if playtimes.len() > 2000 {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Too much playtime entered for version!".to_string(),
|
||||
));
|
||||
return Err(ApiError::InvalidInput(ApiInvalidInput::TooMuchPlaytime));
|
||||
}
|
||||
|
||||
let versions = crate::database::models::DBVersion::get_many(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::cors::default_cors;
|
||||
use crate::util::guards::admin_key_guard;
|
||||
use actix_web::{HttpResponse, get};
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
use crate::database::models::loader_fields::VersionFieldParseError;
|
||||
use crate::file_hosting::FileHostingError;
|
||||
use crate::models::error::AsApiError;
|
||||
use crate::models::ids::ProjectId;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use ariadne::i18n_enum;
|
||||
use derive_more::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error(transparent)]
|
||||
Internal(eyre::Report),
|
||||
#[error(transparent)]
|
||||
Request(eyre::Report),
|
||||
// TODO: Use an I18nEnum instead of a String
|
||||
#[error("Invalid Input: {0}")]
|
||||
InvalidInput(ApiInvalidInput),
|
||||
#[error("Environment Error")]
|
||||
Env(#[from] dotenvy::Error),
|
||||
#[error("Error while uploading file: {0}")]
|
||||
FileHosting(#[from] FileHostingError),
|
||||
#[error("Database Error: {0}")]
|
||||
Database(#[from] crate::database::models::DatabaseError),
|
||||
#[error("Database Error: {0}")]
|
||||
SqlxDatabase(#[from] sqlx::Error),
|
||||
#[error("Database Error: {0}")]
|
||||
RedisDatabase(#[from] redis::RedisError),
|
||||
#[error("Clickhouse Error: {0}")]
|
||||
Clickhouse(#[from] clickhouse::error::Error),
|
||||
#[error("Internal server error: {0}")]
|
||||
Xml(serde_xml_rs::Error),
|
||||
#[error("Deserialization error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("Authentication Error: {0}")]
|
||||
Authentication(#[from] crate::auth::AuthenticationError),
|
||||
#[error("Authentication Error: {0}")]
|
||||
SpecificAuthentication(#[from] SpecificAuthenticationError),
|
||||
#[error("Invalid Input: {0}")]
|
||||
InvalidLoaderField(#[from] VersionFieldParseError),
|
||||
// TODO: Perhaps remove this in favor of InvalidInput?
|
||||
#[error("Error while validating input: {0}")]
|
||||
Validation(String),
|
||||
#[error("Search Error: {0}")]
|
||||
Search(#[from] meilisearch_sdk::errors::Error),
|
||||
#[error("Indexing Error: {0}")]
|
||||
Indexing(#[from] crate::search::indexing::IndexingError),
|
||||
// TODO: Use an I18nEnum instead of a String
|
||||
#[error("Payments Error: {0}")]
|
||||
Payments(String),
|
||||
// TODO: Use an I18nEnum instead of a String
|
||||
#[error("Discord Error: {0}")]
|
||||
Discord(String),
|
||||
#[error("Slack Webhook Error: Error while sending projects webhook")]
|
||||
Slack,
|
||||
#[error("Captcha Error. Try resubmitting the form.")]
|
||||
Turnstile,
|
||||
#[error("Error while decoding Base62: {0}")]
|
||||
Decoding(#[from] ariadne::ids::DecodingError),
|
||||
#[error("Image Parsing Error: {0}")]
|
||||
ImageParse(#[from] image::ImageError),
|
||||
#[error("Password Hashing Error: {0}")]
|
||||
PasswordHashing(#[from] argon2::password_hash::Error),
|
||||
#[error("{0}")]
|
||||
Mail(#[from] crate::queue::email::MailError),
|
||||
#[error("Error while rerouting request: {0}")]
|
||||
Reroute(#[from] reqwest::Error),
|
||||
#[error("Unable to read Zip Archive: {0}")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
#[error("IO Error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Resource not found")]
|
||||
NotFound,
|
||||
#[error("The requested route does not exist")]
|
||||
RouteNotFound,
|
||||
// TODO: Use an I18nEnum instead of a String
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
#[error("External tax compliance API Error")]
|
||||
TaxComplianceApi,
|
||||
#[error(transparent)]
|
||||
TaxProcessor(#[from] crate::util::anrok::AnrokError),
|
||||
#[error(
|
||||
"You are being rate-limited. Please wait {0} milliseconds. 0/{1} remaining."
|
||||
)]
|
||||
RateLimitError(u128, u32),
|
||||
#[error("Error while interacting with payment processor: {0}")]
|
||||
Stripe(#[from] stripe::StripeError),
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
ApiError,
|
||||
root_key: "labrinth.error",
|
||||
Internal(transparent cause) => "internal_error",
|
||||
Request(transparent cause) => "internal_error",
|
||||
InvalidInput(cause) => "invalid_input",
|
||||
Env(..) => "environment_error",
|
||||
FileHosting(cause) => "file_hosting_error",
|
||||
Database(cause) => "database_error",
|
||||
SqlxDatabase(cause) => "database_error",
|
||||
RedisDatabase(cause) => "database_error",
|
||||
Clickhouse(cause) => "clickhouse_error",
|
||||
Xml(cause) => "xml_error",
|
||||
Json(cause) => "json_error",
|
||||
Authentication(cause) => "unauthorized",
|
||||
SpecificAuthentication(cause) => "unauthorized",
|
||||
InvalidLoaderField(cause) => "invalid_input",
|
||||
Validation(cause) => "invalid_input.validation",
|
||||
Search(cause) => "search_error",
|
||||
Indexing(cause) => "indexing_error",
|
||||
Payments(cause) => "payments_error",
|
||||
Discord(cause) => "discord_error",
|
||||
Slack! => "slack_error",
|
||||
Turnstile! => "turnstile_error",
|
||||
Decoding(cause) => "decoding_error",
|
||||
ImageParse(cause) => "invalid_image",
|
||||
PasswordHashing(cause) => "password_hashing_error",
|
||||
Mail(transparent cause) => "mail_error",
|
||||
Reroute(cause) => "reroute_error",
|
||||
Zip(cause) => "zip_error",
|
||||
Io(cause) => "io_error",
|
||||
NotFound! => "not_found",
|
||||
RouteNotFound! => "not_found.route",
|
||||
Conflict(cause) => "conflict",
|
||||
TaxComplianceApi! => "tax_compliance_api_error",
|
||||
TaxProcessor(transparent cause) => "tax_processor_error",
|
||||
RateLimitError(wait_ms, total_allowed_requests) => "ratelimit_error",
|
||||
Stripe(cause) => "stripe_error",
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display)]
|
||||
pub enum ApiInvalidInput {
|
||||
#[display("Specified version does not exist!")]
|
||||
UnknownVersion,
|
||||
#[display("Invalid download URL specified!")]
|
||||
DownloadUrl,
|
||||
#[display("Project {_0} does not exist")]
|
||||
UnknownProject(ProjectId),
|
||||
#[display("Invalid page view URL specified!")]
|
||||
PageViewUrl,
|
||||
#[display("Too much playtime entered for version!")]
|
||||
TooMuchPlaytime,
|
||||
#[display("Invalid resolution_minutes")]
|
||||
ResolutionMinutes,
|
||||
#[display("You cannot see the subscriptions of other users!")]
|
||||
OtherUsersSubscription,
|
||||
#[display("This charge cannot be refunded!")]
|
||||
NonrefundableCharge,
|
||||
#[display("You cannot refund more than the amount of the charge!")]
|
||||
RefundTooLarge,
|
||||
#[display("Charge does not have attached payment id!")]
|
||||
NoAttachedPaymentId,
|
||||
#[display("This charge was not processed via a payment platform.")]
|
||||
NoPaymentPlatform,
|
||||
#[display("Could not link new product price to product.")]
|
||||
CouldntLinkNewProductPrice,
|
||||
#[display("Could not link current product price to product.")]
|
||||
CouldntLinkCurrentProductPrice,
|
||||
#[display("Could not find a valid price for the user's duration")]
|
||||
NoValidPriceForUserDuration,
|
||||
#[display("Could not convert proration to i32")]
|
||||
ProrationToI32,
|
||||
#[display("Could not find a valid price for the specified duration")]
|
||||
NoValidPriceForSpecifiedDuration,
|
||||
#[display("Invalid currency code")]
|
||||
CurrencyCode,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
ApiInvalidInput,
|
||||
root_key: "labrinth.error.invalid_input",
|
||||
UnknownVersion! => "unknown_version",
|
||||
DownloadUrl! => "download_url",
|
||||
UnknownProject(project_id) => "unknown_project",
|
||||
PageViewUrl! => "page_view_url",
|
||||
TooMuchPlaytime! => "too_much_playtime",
|
||||
ResolutionMinutes! => "resolution_minutes",
|
||||
OtherUsersSubscription! => "other_users_subscription",
|
||||
NonrefundableCharge! => "nonrefundable_charge",
|
||||
RefundTooLarge! => "refund_too_large",
|
||||
NoAttachedPaymentId! => "no_attached_payment_id",
|
||||
NoPaymentPlatform! => "no_payment_platform",
|
||||
CouldntLinkNewProductPrice! => "couldnt_link_new_product_price",
|
||||
CouldntLinkCurrentProductPrice! => "couldnt_link_current_product_price",
|
||||
NoValidPriceForUserDuration! => "no_valid_price_for_user_duration",
|
||||
ProrationToI32! => "proration_to_i32",
|
||||
NoValidPriceForSpecifiedDuration! => "no_valid_price_for_specified_duration",
|
||||
CurrencyCode! => "currency_code",
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum SpecificAuthenticationError {
|
||||
#[error("You do not have permission to view affiliate codes!")]
|
||||
ViewAffiliateCodes,
|
||||
#[error("You do not have permission to create an affiliate code!")]
|
||||
CreateAffiliateCode,
|
||||
#[error("Affiliate user not found!")]
|
||||
UnknownAffiliateUser,
|
||||
#[error("You do not have permission to update affiliate codes!")]
|
||||
UpdateAffiliateCodes,
|
||||
#[error("You do not have permission to refund a subscription!")]
|
||||
Refund,
|
||||
#[error("Invalid master key")]
|
||||
InvalidMasterKey,
|
||||
#[error(
|
||||
"You don't have sufficient permissions to interact with this OAuth application"
|
||||
)]
|
||||
InsufficientOAuthPermissions,
|
||||
#[error("You don't have permission to set this status!")]
|
||||
SetStatus,
|
||||
#[error(
|
||||
"The password change flow code is invalid or has expired. Did you copy it promptly and correctly?"
|
||||
)]
|
||||
InvalidFlowCode,
|
||||
#[error("You must specify the old password to change your password!")]
|
||||
OldPasswordNotSpecified,
|
||||
#[error("You are not authorized to upload images for this project")]
|
||||
UploadProjectImages,
|
||||
#[error("You are not authorized to upload images for this version")]
|
||||
UploadVersionImages,
|
||||
#[error("You are not authorized to upload images for this thread message")]
|
||||
UploadThreadMessageImages,
|
||||
#[error("You are not authorized to upload images for this report")]
|
||||
UploadReportImages,
|
||||
#[error("You are not authorized to read this notification!")]
|
||||
NotificationRead,
|
||||
#[error("You are not authorized to delete this notification!")]
|
||||
NotificationDelete,
|
||||
#[error(
|
||||
"You do not have permission to see the OAuth clients of this user!"
|
||||
)]
|
||||
SeeOAuthClients,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the slug of this organization!"
|
||||
)]
|
||||
EditOrgSlug,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the description of this organization!"
|
||||
)]
|
||||
EditOrgDescription,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the name of this organization!"
|
||||
)]
|
||||
EditOrgName,
|
||||
#[error("You do not have permission to edit this organization!")]
|
||||
EditOrg,
|
||||
#[error("You don't have permission to delete this organization!")]
|
||||
DeleteOrg,
|
||||
#[error(
|
||||
"You need to be an owner of a project to add it to an organization!"
|
||||
)]
|
||||
NotProjectOwnerForAddToOrg,
|
||||
#[error("You do not have permission to add projects to this organization!")]
|
||||
AddToOrg,
|
||||
#[error("You don't have permission to edit this organization's icon.")]
|
||||
EditOrgIcon,
|
||||
#[error("You do not have permission to edit this project!")]
|
||||
EditProject,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the name of this project!"
|
||||
)]
|
||||
EditProjectName,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the summary of this project!"
|
||||
)]
|
||||
EditProjectSummary,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the status of this project!"
|
||||
)]
|
||||
EditProjectStatus,
|
||||
#[error("You don't have permission to set this status!")]
|
||||
RestrictedProjectStatus,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the requested status of this project!"
|
||||
)]
|
||||
EditProjectRequestedStatus,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the license URL of this project!"
|
||||
)]
|
||||
EditProjectLicenseUrl,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the slug of this project!"
|
||||
)]
|
||||
EditProjectSlug,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the license of this project!"
|
||||
)]
|
||||
EditProjectLicense,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the links of this project!"
|
||||
)]
|
||||
EditProjectLinks,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the moderation message of this project!"
|
||||
)]
|
||||
EditProjectModerationMessage,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the moderation message body of this project!"
|
||||
)]
|
||||
EditProjectModerationMessageBody,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the description (body) of this project!"
|
||||
)]
|
||||
EditProjectDescription,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the monetization status of this project!"
|
||||
)]
|
||||
EditProjectMonetizationStatus,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the side types migration review status of this project!"
|
||||
)]
|
||||
EditProjectSideTypesMigrationReviewStatus,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the additional categories of this project!"
|
||||
)]
|
||||
EditProjectAdditionalCategories,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the categories of this project!"
|
||||
)]
|
||||
EditProjectCategories,
|
||||
#[error("You do not have the permissions to bulk edit project {0}!")]
|
||||
BulkEditProject(String),
|
||||
#[error("You are not a member of project {0}!")]
|
||||
NotMemberOfProject(String),
|
||||
#[error("You don't have permission to edit this project's icon.")]
|
||||
EditProjectIcon,
|
||||
#[error("You have reached the maximum of gallery images to upload.")]
|
||||
MaximumGalleryImages,
|
||||
#[error("You don't have permission to edit this project's gallery.")]
|
||||
EditProjectGallery,
|
||||
#[error("You don't have permission to delete this project!")]
|
||||
DeleteProject,
|
||||
#[error("Unable to obtain user IP address!")]
|
||||
UnknownUserIp,
|
||||
#[error(
|
||||
"You do not have permission to upload a version for this shared instance."
|
||||
)]
|
||||
SharedInstanceUploadVersion,
|
||||
#[error("You do not have permission to edit this shared instance.")]
|
||||
EditSharedInstance,
|
||||
#[error("You do not have permission to delete this shared instance.")]
|
||||
DeleteSharedInstance,
|
||||
#[error(
|
||||
"You do not have permission to delete this shared instance version."
|
||||
)]
|
||||
DeleteSharedInstanceVersion,
|
||||
#[error("You don't have permission to invite users to this team")]
|
||||
InviteUsersToTeam,
|
||||
#[error("You don't have permission to invite users to this organization")]
|
||||
InviteUsersToOrg,
|
||||
#[error(
|
||||
"You do not have permission to give this user default project permissions."
|
||||
)]
|
||||
GiveUserDefaultProjectPermissions,
|
||||
#[error("You don't have permission to edit members of this team")]
|
||||
EditTeamMembers,
|
||||
#[error(
|
||||
"You cannot override the project permissions of the organization owner!"
|
||||
)]
|
||||
OverrideOrgOwnerDefaultProjectPermissions,
|
||||
#[error("You don't have permission to edit the ownership of this team")]
|
||||
EditTeamOwnership,
|
||||
#[error("The owner can't be removed from a team")]
|
||||
RemoveOwnerFromTeam,
|
||||
#[error("You do not have permission to remove a member from this team")]
|
||||
RemoveTeamMember,
|
||||
#[error("You do not have permission to cancel a team invite")]
|
||||
CancelTeamInvite,
|
||||
#[error(
|
||||
"You do not have permission to remove a member from this organization"
|
||||
)]
|
||||
RemoveOrgMember,
|
||||
#[error("You do not have permission to cancel an organization invite")]
|
||||
CancelOrgInvite,
|
||||
#[error("You cannot delete this message!")]
|
||||
DeleteMessage,
|
||||
#[error("You do not have permission to get a user from their email!")]
|
||||
GetUserFromEmail,
|
||||
#[error("You do not have the permissions to edit the role of this user!")]
|
||||
EditUserRole,
|
||||
#[error("You do not have the permissions to edit the badges of this user!")]
|
||||
EditUserBadges,
|
||||
#[error(
|
||||
"You do not have the permissions to edit the venmo handle of this user!"
|
||||
)]
|
||||
EditUserVenmoHandle,
|
||||
#[error("You do not have permission to edit this user!")]
|
||||
EditUser,
|
||||
#[error("You don't have permission to edit this user's icon.")]
|
||||
EditUserIcon,
|
||||
#[error("You do not have permission to delete this user!")]
|
||||
DeleteUser,
|
||||
#[error(
|
||||
"You do not have permission to see the projects this user follows!"
|
||||
)]
|
||||
SeeUserFollows,
|
||||
#[error(
|
||||
"You do not have permission to see the notifications of this user!"
|
||||
)]
|
||||
SeeUserNotifications,
|
||||
#[error("You don't have permission to delete this file!")]
|
||||
DeleteFile,
|
||||
#[error("You do not have the permissions to edit this version!")]
|
||||
EditVersion,
|
||||
#[error("You don't have permission to set the downloads of this mod")]
|
||||
SetModDownloads,
|
||||
#[error("You do not have permission to delete versions in this team")]
|
||||
DeleteVersionsInTeam,
|
||||
#[error("You do not have permission to send custom emails!")]
|
||||
SendCustomEmails,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
SpecificAuthenticationError,
|
||||
root_key: "labrinth.error.unauthorized.specific",
|
||||
ViewAffiliateCodes! => "view_affiliate_codes",
|
||||
CreateAffiliateCode! => "create_affiliate_code",
|
||||
UnknownAffiliateUser! => "unknown_affiliate_user",
|
||||
UpdateAffiliateCodes! => "update_affiliate_codes",
|
||||
Refund! => "refund",
|
||||
InvalidMasterKey! => "invalid_master_key",
|
||||
InsufficientOAuthPermissions! => "insufficient_oauth_permissions",
|
||||
SetStatus! => "set_status",
|
||||
InvalidFlowCode! => "invalid_flow_code",
|
||||
OldPasswordNotSpecified! => "old_password_not_specified",
|
||||
UploadProjectImages! => "upload_project_images",
|
||||
UploadVersionImages! => "upload_version_images",
|
||||
UploadThreadMessageImages! => "upload_thread_message_images",
|
||||
UploadReportImages! => "upload_report_images",
|
||||
NotificationRead! => "notification_read",
|
||||
NotificationDelete! => "notification_delete",
|
||||
SeeOAuthClients! => "see_oauth_clients",
|
||||
EditOrgSlug! => "edit_org_slug",
|
||||
EditOrgDescription! => "edit_org_description",
|
||||
EditOrgName! => "edit_org_name",
|
||||
EditOrg! => "edit_org",
|
||||
DeleteOrg! => "delete_org",
|
||||
NotProjectOwnerForAddToOrg! => "not_project_owner_for_add_to_org",
|
||||
AddToOrg! => "add_to_org",
|
||||
EditOrgIcon! => "edit_org_icon",
|
||||
EditProject! => "edit_project",
|
||||
EditProjectName! => "edit_project_name",
|
||||
EditProjectSummary! => "edit_project_summary",
|
||||
EditProjectStatus! => "edit_project_status",
|
||||
RestrictedProjectStatus! => "restricted_project_status",
|
||||
EditProjectRequestedStatus! => "edit_project_requested_status",
|
||||
EditProjectLicenseUrl! => "edit_project_license_url",
|
||||
EditProjectSlug! => "edit_project_slug",
|
||||
EditProjectLicense! => "edit_project_license",
|
||||
EditProjectLinks! => "edit_project_links",
|
||||
EditProjectModerationMessage! => "edit_project_moderation_message",
|
||||
EditProjectModerationMessageBody! => "edit_project_moderation_message_body",
|
||||
EditProjectDescription! => "edit_project_description",
|
||||
EditProjectMonetizationStatus! => "edit_project_monetization_status",
|
||||
EditProjectSideTypesMigrationReviewStatus! => "edit_project_side_types_migration_review_status",
|
||||
EditProjectAdditionalCategories! => "edit_project_additional_categories",
|
||||
EditProjectCategories! => "edit_project_categories",
|
||||
BulkEditProject(project_name) => "bulk_edit_project",
|
||||
NotMemberOfProject(project_name) => "not_member_of_project",
|
||||
EditProjectIcon! => "edit_project_icon",
|
||||
MaximumGalleryImages! => "maximum_gallery_images",
|
||||
EditProjectGallery! => "edit_project_gallery",
|
||||
DeleteProject! => "delete_project",
|
||||
UnknownUserIp! => "unknown_user_ip",
|
||||
SharedInstanceUploadVersion! => "shared_instance_upload_version",
|
||||
EditSharedInstance! => "edit_shared_instance",
|
||||
DeleteSharedInstance! => "delete_shared_instance",
|
||||
DeleteSharedInstanceVersion! => "delete_shared_instance_version",
|
||||
InviteUsersToTeam! => "invite_users_to_team",
|
||||
InviteUsersToOrg! => "invite_users_to_org",
|
||||
GiveUserDefaultProjectPermissions! => "give_user_default_project_permissions",
|
||||
EditTeamMembers! => "edit_team_members",
|
||||
OverrideOrgOwnerDefaultProjectPermissions! => "override_org_owner_default_project_permissions",
|
||||
EditTeamOwnership! => "edit_team_ownership",
|
||||
RemoveOwnerFromTeam! => "remove_owner_from_team",
|
||||
RemoveTeamMember! => "remove_team_member",
|
||||
CancelTeamInvite! => "cancel_team_invite",
|
||||
RemoveOrgMember! => "remove_org_member",
|
||||
CancelOrgInvite! => "cancel_org_invite",
|
||||
DeleteMessage! => "delete_message",
|
||||
GetUserFromEmail! => "get_user_from_email",
|
||||
EditUserRole! => "edit_user_role",
|
||||
EditUserBadges! => "edit_user_badges",
|
||||
EditUserVenmoHandle! => "edit_user_venmo_handle",
|
||||
EditUser! => "edit_user",
|
||||
EditUserIcon! => "edit_user_icon",
|
||||
DeleteUser! => "delete_user",
|
||||
SeeUserFollows! => "see_user_follows",
|
||||
SeeUserNotifications! => "see_user_notifications",
|
||||
DeleteFile! => "delete_file",
|
||||
EditVersion! => "edit_version",
|
||||
SetModDownloads! => "set_mod_downloads",
|
||||
DeleteVersionsInTeam! => "delete_versions_in_team",
|
||||
SendCustomEmails! => "send_custom_emails",
|
||||
);
|
||||
|
||||
impl ResponseError for ApiError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
ApiError::Internal(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Request(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::RedisDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Clickhouse(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Authentication(..) => StatusCode::UNAUTHORIZED,
|
||||
ApiError::SpecificAuthentication(..) => StatusCode::UNAUTHORIZED,
|
||||
ApiError::Xml(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Json(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Search(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Indexing(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::InvalidInput(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::InvalidLoaderField(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Validation(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Payments(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
ApiError::Discord(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
ApiError::Turnstile => StatusCode::BAD_REQUEST,
|
||||
ApiError::Decoding(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::ImageParse(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::PasswordHashing(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Reroute(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::NotFound => StatusCode::NOT_FOUND,
|
||||
ApiError::RouteNotFound => StatusCode::NOT_FOUND,
|
||||
ApiError::Conflict(..) => StatusCode::CONFLICT,
|
||||
ApiError::TaxComplianceApi => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Zip(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Io(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::RateLimitError(..) => StatusCode::TOO_MANY_REQUESTS,
|
||||
ApiError::Stripe(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
ApiError::TaxProcessor(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Slack => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(self.as_api_error())
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use crate::queue::analytics::AnalyticsQueue;
|
||||
use crate::queue::maxmind::MaxMindIndexer;
|
||||
use crate::queue::moderation::AUTOMOD_ID;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, ApiInvalidInput};
|
||||
use crate::search::SearchConfig;
|
||||
use crate::util::date::get_current_tenths_of_ms;
|
||||
use crate::util::guards::admin_key_guard;
|
||||
@@ -104,14 +104,11 @@ pub async fn count_download(
|
||||
{
|
||||
(version.id, version.mod_id)
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Specified version does not exist!".to_string(),
|
||||
));
|
||||
return Err(ApiError::InvalidInput(ApiInvalidInput::UnknownVersion));
|
||||
};
|
||||
|
||||
let url = url::Url::parse(&download_body.url).map_err(|_| {
|
||||
ApiError::InvalidInput("invalid download URL specified!".to_string())
|
||||
})?;
|
||||
let url = url::Url::parse(&download_body.url)
|
||||
.map_err(|_| ApiError::InvalidInput(ApiInvalidInput::DownloadUrl))?;
|
||||
|
||||
let ip = crate::util::ip::convert_to_ip_v6(&download_body.ip)
|
||||
.unwrap_or_else(|_| Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped());
|
||||
@@ -167,7 +164,7 @@ pub async fn force_reindex(
|
||||
#[derive(Deserialize)]
|
||||
pub struct DelphiIngest {
|
||||
pub url: String,
|
||||
pub project_id: crate::models::ids::ProjectId,
|
||||
pub project_id: ProjectId,
|
||||
pub version_id: crate::models::ids::VersionId,
|
||||
pub issues: HashMap<String, HashMap<String, String>>,
|
||||
}
|
||||
@@ -192,10 +189,7 @@ pub async fn delphi_result_ingest(
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Project {} does not exist",
|
||||
body.project_id
|
||||
))
|
||||
ApiError::InvalidInput(ApiInvalidInput::UnknownProject(body.project_id))
|
||||
})?;
|
||||
|
||||
let mut header = format!("Suspicious traces found at {}", body.url);
|
||||
|
||||
@@ -12,13 +12,14 @@ use crate::{
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
};
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
@@ -63,8 +64,8 @@ async fn get_all(
|
||||
.collect::<Vec<_>>();
|
||||
Ok(HttpResponse::Ok().json(codes))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to view affiliate codes!".to_string(),
|
||||
Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::ViewAffiliateCodes,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -80,7 +81,7 @@ async fn create(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<CreateRequest>,
|
||||
body: Json<CreateRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_, creator) = get_user_from_headers(
|
||||
&req,
|
||||
@@ -95,9 +96,8 @@ async fn create(
|
||||
let is_affiliate = creator.badges.contains(Badges::AFFILIATE);
|
||||
|
||||
if !is_admin && !is_affiliate {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to create an affiliate code!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::CreateAffiliateCode,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -116,8 +116,8 @@ async fn create(
|
||||
let Some(_affiliate_user) =
|
||||
DBUser::get_id(affiliate_id, &**pool, &redis).await?
|
||||
else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"Affiliate user not found!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::UnknownAffiliateUser,
|
||||
));
|
||||
};
|
||||
}
|
||||
@@ -237,7 +237,7 @@ async fn patch(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<PatchRequest>,
|
||||
body: Json<PatchRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
@@ -263,8 +263,8 @@ async fn patch(
|
||||
}
|
||||
|
||||
if !is_admin && !user.badges.contains(Badges::AFFILIATE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to update affiliate codes!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::UpdateAffiliateCodes,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@ use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::products_tax_identifier_item::product_info_by_product_price_id;
|
||||
use crate::database::models::{
|
||||
charge_item, generate_charge_id, product_item, user_subscription_item,
|
||||
};
|
||||
use crate::database::models::user_subscription_item::DBUserSubscription;
|
||||
use crate::database::models::{DBUser, generate_charge_id, product_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::billing::{
|
||||
Charge, ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration,
|
||||
@@ -16,7 +15,9 @@ use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::users::Badges;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{
|
||||
ApiError, ApiInvalidInput, SpecificAuthenticationError,
|
||||
};
|
||||
use crate::util::anrok;
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
@@ -110,26 +111,24 @@ pub async fn subscriptions(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let subscriptions =
|
||||
user_subscription_item::DBUserSubscription::get_all_user(
|
||||
if let Some(user_id) = query.user_id {
|
||||
if user.role.is_admin() {
|
||||
user_id.into()
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot see the subscriptions of other users!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let subscriptions = DBUserSubscription::get_all_user(
|
||||
if let Some(user_id) = query.user_id {
|
||||
if user.role.is_admin() {
|
||||
user_id.into()
|
||||
} else {
|
||||
user.id.into()
|
||||
},
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(UserSubscription::from)
|
||||
.collect::<Vec<_>>();
|
||||
return Err(ApiError::InvalidInput(
|
||||
ApiInvalidInput::OtherUsersSubscription,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
user.id.into()
|
||||
},
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(UserSubscription::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(subscriptions))
|
||||
}
|
||||
@@ -174,8 +173,8 @@ pub async fn refund_charge(
|
||||
let (id,) = info.into_inner();
|
||||
|
||||
if !user.role.is_admin() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to refund a subscription!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::Refund,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -203,14 +202,13 @@ pub async fn refund_charge(
|
||||
|
||||
if charge.status != ChargeStatus::Succeeded {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"This charge cannot be refunded!".to_string(),
|
||||
ApiInvalidInput::NonrefundableCharge,
|
||||
));
|
||||
}
|
||||
|
||||
if (refundable - refund_amount) < 0 {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot refund more than the amount of the charge!"
|
||||
.to_string(),
|
||||
ApiInvalidInput::RefundTooLarge,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -306,15 +304,13 @@ pub async fn refund_charge(
|
||||
)
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Charge does not have attached payment id!"
|
||||
.to_string(),
|
||||
ApiInvalidInput::NoAttachedPaymentId,
|
||||
));
|
||||
}
|
||||
}
|
||||
PaymentPlatform::None => {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"This charge was not processed via a payment platform."
|
||||
.to_owned(),
|
||||
ApiInvalidInput::NoPaymentPlatform,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -435,9 +431,7 @@ pub async fn edit_subscription(
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not link new product price to product.".to_owned(),
|
||||
)
|
||||
ApiError::InvalidInput(ApiInvalidInput::CouldntLinkNewProductPrice)
|
||||
})?;
|
||||
let current_product = product_item::DBProduct::get(
|
||||
current_product_price.product_id,
|
||||
@@ -446,7 +440,7 @@ pub async fn edit_subscription(
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not link current product price to product.".to_owned(),
|
||||
ApiInvalidInput::CouldntLinkCurrentProductPrice,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -488,8 +482,8 @@ pub async fn edit_subscription(
|
||||
///
|
||||
/// Returns the proration requirement (see [`Proration`]) and the new product price's amount.
|
||||
fn proration_amount(
|
||||
open_charge: &charge_item::DBCharge,
|
||||
subscription: &user_subscription_item::DBUserSubscription,
|
||||
open_charge: &DBCharge,
|
||||
subscription: &DBUserSubscription,
|
||||
current_price: &product_item::DBProductPrice,
|
||||
new_product_price: &product_item::DBProductPrice,
|
||||
) -> Result<(Proration, i32), ApiError> {
|
||||
@@ -501,8 +495,7 @@ pub async fn edit_subscription(
|
||||
Price::Recurring { intervals } => {
|
||||
*intervals.get(&duration).ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find a valid price for the user's duration"
|
||||
.to_owned(),
|
||||
ApiInvalidInput::NoValidPriceForUserDuration,
|
||||
)
|
||||
})?
|
||||
}
|
||||
@@ -513,8 +506,7 @@ pub async fn edit_subscription(
|
||||
Price::Recurring { intervals } => {
|
||||
*intervals.get(&duration).ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find a valid price for the user's duration"
|
||||
.to_owned(),
|
||||
ApiInvalidInput::NoValidPriceForUserDuration,
|
||||
)
|
||||
})?
|
||||
}
|
||||
@@ -526,9 +518,7 @@ pub async fn edit_subscription(
|
||||
.floor()
|
||||
.to_i32()
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not convert proration to i32".to_owned(),
|
||||
)
|
||||
ApiError::InvalidInput(ApiInvalidInput::ProrationToI32)
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
@@ -547,10 +537,9 @@ pub async fn edit_subscription(
|
||||
|
||||
let dry = query.dry.unwrap_or_default();
|
||||
|
||||
let subscription =
|
||||
user_subscription_item::DBUserSubscription::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound)?;
|
||||
let subscription = DBUserSubscription::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound)?;
|
||||
|
||||
if subscription.user_id != user.id.into() && !user.role.is_admin() {
|
||||
return Err(ApiError::NotFound);
|
||||
@@ -558,16 +547,15 @@ pub async fn edit_subscription(
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let mut open_charge = charge_item::DBCharge::get_open_subscription(
|
||||
subscription.id,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find open charge for this subscription".to_string(),
|
||||
)
|
||||
})?;
|
||||
let mut open_charge =
|
||||
DBCharge::get_open_subscription(subscription.id, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find open charge for this subscription"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let current_price = product_item::DBProductPrice::get(
|
||||
subscription.price_id,
|
||||
@@ -923,23 +911,22 @@ pub async fn charges(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let charges =
|
||||
crate::database::models::charge_item::DBCharge::get_from_user(
|
||||
if let Some(user_id) = query.user_id {
|
||||
if user.role.is_admin() {
|
||||
user_id.into()
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot see the subscriptions of other users!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let charges = DBCharge::get_from_user(
|
||||
if let Some(user_id) = query.user_id {
|
||||
if user.role.is_admin() {
|
||||
user_id.into()
|
||||
} else {
|
||||
user.id.into()
|
||||
},
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot see the subscriptions of other users!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
user.id.into()
|
||||
},
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(
|
||||
charges
|
||||
@@ -1124,11 +1111,7 @@ pub async fn remove_payment_method(
|
||||
.await?;
|
||||
|
||||
let user_subscriptions =
|
||||
user_subscription_item::DBUserSubscription::get_all_user(
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
DBUserSubscription::get_all_user(user.id.into(), &**pool).await?;
|
||||
|
||||
if user_subscriptions
|
||||
.iter()
|
||||
@@ -1222,16 +1205,14 @@ pub async fn active_servers(
|
||||
.get("X-Master-Key")
|
||||
.is_none_or(|it| it.as_bytes() != master_key.as_bytes())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"Invalid master key".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::InvalidMasterKey,
|
||||
));
|
||||
}
|
||||
|
||||
let servers = user_subscription_item::DBUserSubscription::get_all_servers(
|
||||
query.subscription_status,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let servers =
|
||||
DBUserSubscription::get_all_servers(query.subscription_status, &**pool)
|
||||
.await?;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActiveServer {
|
||||
@@ -1297,7 +1278,7 @@ pub struct PaymentRequest {
|
||||
#[serde(flatten)]
|
||||
pub type_: PaymentRequestType,
|
||||
pub charge: ChargeRequestType,
|
||||
pub existing_payment_intent: Option<stripe::PaymentIntentId>,
|
||||
pub existing_payment_intent: Option<PaymentIntentId>,
|
||||
pub metadata: Option<PaymentRequestMetadata>,
|
||||
}
|
||||
|
||||
@@ -1385,12 +1366,11 @@ pub async fn stripe_webhook(
|
||||
&dotenvy::var("STRIPE_WEBHOOK_SECRET")?,
|
||||
) {
|
||||
struct PaymentIntentMetadata {
|
||||
pub user_item: crate::database::models::user_item::DBUser,
|
||||
pub user_item: DBUser,
|
||||
pub product_price_item: product_item::DBProductPrice,
|
||||
pub product_item: product_item::DBProduct,
|
||||
pub charge_item: crate::database::models::charge_item::DBCharge,
|
||||
pub user_subscription_item:
|
||||
Option<user_subscription_item::DBUserSubscription>,
|
||||
pub charge_item: DBCharge,
|
||||
pub user_subscription_item: Option<DBUserSubscription>,
|
||||
pub payment_metadata: Option<PaymentRequestMetadata>,
|
||||
pub new_region: Option<String>,
|
||||
pub next_tax_amount: i64,
|
||||
@@ -1416,11 +1396,7 @@ pub async fn stripe_webhook(
|
||||
break 'metadata;
|
||||
};
|
||||
|
||||
let Some(user) =
|
||||
crate::database::models::user_item::DBUser::get_id(
|
||||
user_id, pool, redis,
|
||||
)
|
||||
.await?
|
||||
let Some(user) = DBUser::get_id(user_id, pool, redis).await?
|
||||
else {
|
||||
break 'metadata;
|
||||
};
|
||||
@@ -1458,10 +1434,7 @@ pub async fn stripe_webhook(
|
||||
|
||||
let (charge, price, product, subscription, new_region) =
|
||||
if let Some(mut charge) =
|
||||
crate::database::models::charge_item::DBCharge::get(
|
||||
charge_id, pool,
|
||||
)
|
||||
.await?
|
||||
DBCharge::get(charge_id, pool).await?
|
||||
{
|
||||
let Some(price) = product_item::DBProductPrice::get(
|
||||
charge.price_id,
|
||||
@@ -1491,11 +1464,8 @@ pub async fn stripe_webhook(
|
||||
|
||||
if let Some(subscription_id) = charge.subscription_id {
|
||||
let maybe_subscription =
|
||||
user_subscription_item::DBUserSubscription::get(
|
||||
subscription_id,
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
DBUserSubscription::get(subscription_id, pool)
|
||||
.await?;
|
||||
|
||||
let Some(mut subscription) = maybe_subscription
|
||||
else {
|
||||
@@ -1584,14 +1554,23 @@ pub async fn stripe_webhook(
|
||||
break 'metadata;
|
||||
};
|
||||
|
||||
let subscription = if let Some(mut subscription) = user_subscription_item::DBUserSubscription::get(subscription_id, pool).await? {
|
||||
subscription.status = SubscriptionStatus::Unprovisioned;
|
||||
subscription.price_id = price_id;
|
||||
subscription.interval = interval;
|
||||
let subscription = if let Some(
|
||||
mut subscription,
|
||||
) =
|
||||
DBUserSubscription::get(
|
||||
subscription_id,
|
||||
pool,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
subscription.status =
|
||||
SubscriptionStatus::Unprovisioned;
|
||||
subscription.price_id = price_id;
|
||||
subscription.interval = interval;
|
||||
|
||||
subscription
|
||||
} else {
|
||||
user_subscription_item::DBUserSubscription {
|
||||
subscription
|
||||
} else {
|
||||
DBUserSubscription {
|
||||
id: subscription_id,
|
||||
user_id,
|
||||
price_id,
|
||||
@@ -1600,7 +1579,7 @@ pub async fn stripe_webhook(
|
||||
status: SubscriptionStatus::Unprovisioned,
|
||||
metadata: None,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if charge_status != ChargeStatus::Failed {
|
||||
subscription
|
||||
@@ -2014,7 +1993,7 @@ pub async fn stripe_webhook(
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
crate::database::models::user_item::DBUser::clear_caches(
|
||||
DBUser::clear_caches(
|
||||
&[(metadata.user_item.id, None)],
|
||||
&redis,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::*;
|
||||
use crate::models::v3::billing::SubscriptionStatus;
|
||||
use crate::models::v3::users::User;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::anrok;
|
||||
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::models::users::Role;
|
||||
use crate::models::v3::notifications::NotificationBody;
|
||||
use crate::models::v3::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::util::guards::external_notification_key_guard;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::web;
|
||||
@@ -83,8 +83,8 @@ pub async fn send_custom_email(
|
||||
.1;
|
||||
|
||||
if user.role != Role::Admin {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to send custom emails!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::SendCustomEmails,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::auth::templates::ErrorPage;
|
||||
use crate::auth::validate::{
|
||||
get_full_user_from_headers, get_user_record_from_bearer_token,
|
||||
};
|
||||
@@ -12,7 +13,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::users::{Badges, Role};
|
||||
use crate::queue::email::EmailQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::routes::internal::session::issue_session;
|
||||
use crate::util::captcha::check_hcaptcha;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
@@ -83,9 +84,7 @@ impl TempUser {
|
||||
redis: &RedisPool,
|
||||
) -> Result<crate::database::models::DBUserId, AuthenticationError> {
|
||||
if let Some(email) = &self.email
|
||||
&& crate::database::models::DBUser::get_by_email(email, client)
|
||||
.await?
|
||||
.is_some()
|
||||
&& DBUser::get_by_email(email, client).await?.is_some()
|
||||
{
|
||||
return Err(AuthenticationError::DuplicateUser);
|
||||
}
|
||||
@@ -107,12 +106,7 @@ impl TempUser {
|
||||
}
|
||||
);
|
||||
|
||||
let new_id = crate::database::models::DBUser::get(
|
||||
&test_username,
|
||||
client,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
let new_id = DBUser::get(&test_username, client, redis).await?;
|
||||
|
||||
if new_id.is_none() {
|
||||
username = Some(test_username);
|
||||
@@ -163,7 +157,7 @@ impl TempUser {
|
||||
};
|
||||
|
||||
if let Some(username) = username {
|
||||
crate::database::models::DBUser {
|
||||
DBUser {
|
||||
id: user_id,
|
||||
github_id: if provider == AuthProvider::GitHub {
|
||||
Some(
|
||||
@@ -1112,29 +1106,29 @@ pub async fn auth_callback(
|
||||
client: Data<PgPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
redis: Data<RedisPool>,
|
||||
) -> Result<HttpResponse, crate::auth::templates::ErrorPage> {
|
||||
let state_string = query
|
||||
.get("state")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?
|
||||
.clone();
|
||||
) -> Result<HttpResponse, ErrorPage> {
|
||||
let res = async || {
|
||||
let state = query
|
||||
.get("state")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?
|
||||
.clone();
|
||||
|
||||
let state = state_string.clone();
|
||||
let res: Result<HttpResponse, AuthenticationError> = async move {
|
||||
let flow = DBFlow::get(&state, &redis).await?;
|
||||
|
||||
// Extract cookie header from request
|
||||
if let Some(DBFlow::OAuth {
|
||||
user_id,
|
||||
provider,
|
||||
url,
|
||||
}) = flow
|
||||
user_id,
|
||||
provider,
|
||||
url,
|
||||
}) = flow
|
||||
{
|
||||
DBFlow::remove(&state, &redis).await?;
|
||||
|
||||
let token = provider.get_token(query).await?;
|
||||
let oauth_user = provider.get_user(&token).await?;
|
||||
|
||||
let user_id_opt = provider.get_user_id(&oauth_user.id, &**client).await?;
|
||||
let user_id_opt =
|
||||
provider.get_user_id(&oauth_user.id, &**client).await?;
|
||||
|
||||
let mut transaction = client.begin().await?;
|
||||
if let Some(id) = user_id {
|
||||
@@ -1146,9 +1140,12 @@ pub async fn auth_callback(
|
||||
.update_user_id(id, Some(&oauth_user.id), &mut transaction)
|
||||
.await?;
|
||||
|
||||
let user = crate::database::models::DBUser::get_id(id, &**client, &redis).await?;
|
||||
let user = crate::database::models::DBUser::get_id(
|
||||
id, &**client, &redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if provider == AuthProvider::PayPal {
|
||||
if provider == AuthProvider::PayPal {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
@@ -1163,22 +1160,32 @@ pub async fn auth_callback(
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
} else if let Some(user) = user {
|
||||
NotificationBuilder { body: NotificationBody::AuthProviderAdded { provider: provider.as_str().to_string() } }
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::AuthProviderAdded {
|
||||
provider: provider.as_str().to_string(),
|
||||
},
|
||||
}
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
crate::database::models::DBUser::clear_caches(&[(id, None)], &redis).await?;
|
||||
crate::database::models::DBUser::clear_caches(
|
||||
&[(id, None)],
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("Location", &*url))
|
||||
.json(serde_json::json!({ "url": url })))
|
||||
} else {
|
||||
let user_id = if let Some(user_id) = user_id_opt {
|
||||
let user = crate::database::models::DBUser::get_id(user_id, &**client, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let user = crate::database::models::DBUser::get_id(
|
||||
user_id, &**client, &redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
if user.totp_secret.is_some() {
|
||||
let flow = DBFlow::Login2FA { user_id: user.id }
|
||||
@@ -1199,10 +1206,20 @@ pub async fn auth_callback(
|
||||
|
||||
user_id
|
||||
} else {
|
||||
oauth_user.create_account(provider, &mut transaction, &client, &file_host, &redis).await?
|
||||
oauth_user
|
||||
.create_account(
|
||||
provider,
|
||||
&mut transaction,
|
||||
&client,
|
||||
&file_host,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let session = issue_session(req, user_id, &mut transaction, &redis).await?;
|
||||
let session =
|
||||
issue_session(&req, user_id, &mut transaction, &redis)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
let redirect_url = format!(
|
||||
@@ -1222,11 +1239,13 @@ pub async fn auth_callback(
|
||||
.json(serde_json::json!({ "url": redirect_url })))
|
||||
}
|
||||
} else {
|
||||
Err::<HttpResponse, AuthenticationError>(AuthenticationError::InvalidCredentials)
|
||||
Err::<HttpResponse, AuthenticationError>(
|
||||
AuthenticationError::InvalidCredentials,
|
||||
)
|
||||
}
|
||||
}.await;
|
||||
};
|
||||
|
||||
Ok(res?)
|
||||
Ok(res().await?)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1429,7 +1448,8 @@ pub async fn create_account_with_password(
|
||||
.insert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
let session = issue_session(req, user_id, &mut transaction, &redis).await?;
|
||||
let session =
|
||||
issue_session(&req, user_id, &mut transaction, &redis).await?;
|
||||
let res = crate::models::sessions::Session::from(session, true, None);
|
||||
|
||||
let mailbox: Mailbox = new_account.email.parse().map_err(|_| {
|
||||
@@ -1523,7 +1543,7 @@ pub async fn login_password(
|
||||
} else {
|
||||
let mut transaction = pool.begin().await?;
|
||||
let session =
|
||||
issue_session(req, user.id, &mut transaction, &redis).await?;
|
||||
issue_session(&req, user.id, &mut transaction, &redis).await?;
|
||||
let res = crate::models::sessions::Session::from(session, true, None);
|
||||
transaction.commit().await?;
|
||||
|
||||
@@ -1583,9 +1603,7 @@ async fn validate_2fa_code(
|
||||
|
||||
Ok(true)
|
||||
} else if allow_backup {
|
||||
let backup_codes =
|
||||
crate::database::models::DBUser::get_backup_codes(user_id, pool)
|
||||
.await?;
|
||||
let backup_codes = DBUser::get_backup_codes(user_id, pool).await?;
|
||||
|
||||
if !backup_codes.contains(&input) {
|
||||
Ok(false)
|
||||
@@ -1603,11 +1621,7 @@ async fn validate_2fa_code(
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
crate::database::models::DBUser::clear_caches(
|
||||
&[(user_id, None)],
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
DBUser::clear_caches(&[(user_id, None)], redis).await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -1653,7 +1667,7 @@ pub async fn login_2fa(
|
||||
DBFlow::remove(&login.flow, &redis).await?;
|
||||
|
||||
let session =
|
||||
issue_session(req, user_id, &mut transaction, &redis).await?;
|
||||
issue_session(&req, user_id, &mut transaction, &redis).await?;
|
||||
let res = crate::models::sessions::Session::from(session, true, None);
|
||||
transaction.commit().await?;
|
||||
|
||||
@@ -2029,8 +2043,8 @@ pub async fn change_password(
|
||||
|
||||
Some(user)
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The password change flow code is invalid or has expired. Did you copy it promptly and correctly?".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::InvalidFlowCode,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -2057,11 +2071,12 @@ pub async fn change_password(
|
||||
}
|
||||
|
||||
if let Some(pass) = user.password.as_ref() {
|
||||
let old_password = change_password.old_password.as_ref().ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You must specify the old password to change your password!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let old_password =
|
||||
change_password.old_password.as_ref().ok_or_else(|| {
|
||||
ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::OldPasswordNotSpecified,
|
||||
)
|
||||
})?;
|
||||
|
||||
let hasher = Argon2::default();
|
||||
hasher.verify_password(
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::auth::get_user_from_headers;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use actix_web::{HttpRequest, HttpResponse, post, web};
|
||||
use sqlx::PgPool;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::database::models::users_redeemals::{
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::queue::billing::try_process_user_redeemal;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::guards::medal_key_guard;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
@@ -10,8 +10,8 @@ pub mod pats;
|
||||
pub mod session;
|
||||
pub mod statuses;
|
||||
|
||||
pub use super::ApiError;
|
||||
use super::v3::oauth_clients;
|
||||
pub use crate::routes::error::ApiError;
|
||||
use crate::util::cors::default_cors;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::ApiError;
|
||||
use crate::database;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::{auth::check_is_moderator_from_headers, models::pats::Scopes};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use ariadne::ids::random_base62;
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::database;
|
||||
use crate::database::models::generate_pat_id;
|
||||
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
|
||||
use crate::database::redis::RedisPool;
|
||||
use actix_web::web::{self, Data};
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::sessions::Session;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::env::parse_var;
|
||||
use actix_web::http::header::AUTHORIZATION;
|
||||
use actix_web::web::{Data, ServiceConfig, scope};
|
||||
@@ -84,12 +84,12 @@ pub async fn get_session_metadata(
|
||||
}
|
||||
|
||||
pub async fn issue_session(
|
||||
req: HttpRequest,
|
||||
req: &HttpRequest,
|
||||
user_id: DBUserId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
redis: &RedisPool,
|
||||
) -> Result<DBSession, AuthenticationError> {
|
||||
let metadata = get_session_metadata(&req).await?;
|
||||
let metadata = get_session_metadata(req).await?;
|
||||
|
||||
let session = ChaCha20Rng::from_entropy()
|
||||
.sample_iter(&Alphanumeric)
|
||||
@@ -244,7 +244,7 @@ pub async fn refresh(
|
||||
|
||||
DBSession::remove(session.id, &mut transaction).await?;
|
||||
let new_session =
|
||||
issue_session(req, session.user_id, &mut transaction, &redis)
|
||||
issue_session(&req, session.user_id, &mut transaction, &redis)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
DBSession::clear_cache(
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::queue::session::AuthQueue;
|
||||
use crate::queue::socket::{
|
||||
ActiveSocket, ActiveSockets, SocketId, TunnelSocketType,
|
||||
};
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::sync::friends::{FRIENDS_CHANNEL_NAME, RedisFriendsMessage};
|
||||
use crate::sync::status::{
|
||||
get_user_status, push_back_user_expiry, replace_user_status,
|
||||
|
||||
@@ -9,12 +9,12 @@ use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::{auth::get_user_from_headers, database};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, route, web};
|
||||
use serde::Serialize;
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashSet;
|
||||
use yaserde::YaSerialize;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(maven_metadata);
|
||||
@@ -23,45 +23,37 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(version_file);
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(rename = "metadata")]
|
||||
#[derive(Default, Debug, Clone, Serialize)]
|
||||
#[serde(rename = "metadata", rename_all = "camelCase")]
|
||||
pub struct Metadata {
|
||||
#[yaserde(rename = "groupId")]
|
||||
group_id: String,
|
||||
#[yaserde(rename = "artifactId")]
|
||||
artifact_id: String,
|
||||
versioning: Versioning,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(rename = "versioning")]
|
||||
#[derive(Default, Debug, Clone, Serialize)]
|
||||
#[serde(rename = "versioning", rename_all = "camelCase")]
|
||||
pub struct Versioning {
|
||||
latest: String,
|
||||
release: String,
|
||||
versions: Versions,
|
||||
#[yaserde(rename = "lastUpdated")]
|
||||
last_updated: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(rename = "versions")]
|
||||
#[derive(Default, Debug, Clone, Serialize)]
|
||||
#[serde(rename = "versions")]
|
||||
pub struct Versions {
|
||||
#[yaserde(rename = "version")]
|
||||
#[serde(rename = "version")]
|
||||
versions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(rename = "project", namespaces = { "" = "http://maven.apache.org/POM/4.0.0" })]
|
||||
#[derive(Default, Debug, Clone, Serialize)]
|
||||
#[serde(rename = "project", rename_all = "camelCase")]
|
||||
pub struct MavenPom {
|
||||
#[yaserde(rename = "xsi:schemaLocation", attribute = true)]
|
||||
#[serde(rename = "@xsi:schemaLocation")]
|
||||
schema_location: String,
|
||||
#[yaserde(rename = "xmlns:xsi", attribute = true)]
|
||||
xsi: String,
|
||||
#[yaserde(rename = "modelVersion")]
|
||||
model_version: String,
|
||||
#[yaserde(rename = "groupId")]
|
||||
group_id: String,
|
||||
#[yaserde(rename = "artifactId")]
|
||||
artifact_id: String,
|
||||
version: String,
|
||||
name: String,
|
||||
@@ -157,7 +149,7 @@ pub async fn maven_metadata(
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/xml")
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?))
|
||||
.body(serde_xml_rs::to_string(&respdata).map_err(ApiError::Xml)?))
|
||||
}
|
||||
|
||||
async fn find_version(
|
||||
@@ -215,7 +207,7 @@ async fn find_version(
|
||||
if !game_versions.is_empty() {
|
||||
let version_game_versions =
|
||||
x.version_fields.clone().into_iter().find_map(|v| {
|
||||
MinecraftGameVersion::try_from_version_field(&v).ok()
|
||||
MinecraftGameVersion::try_from_version_field(v).ok()
|
||||
});
|
||||
if let Some(version_game_versions) = version_game_versions {
|
||||
bool &= version_game_versions
|
||||
@@ -318,7 +310,6 @@ pub async fn version_file(
|
||||
schema_location:
|
||||
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
.to_string(),
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance".to_string(),
|
||||
model_version: "4.0.0".to_string(),
|
||||
group_id: "maven.modrinth".to_string(),
|
||||
artifact_id: project_id,
|
||||
@@ -326,9 +317,13 @@ pub async fn version_file(
|
||||
name: project.inner.name,
|
||||
description: project.inner.description,
|
||||
};
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("text/xml")
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?));
|
||||
return Ok(HttpResponse::Ok().content_type("text/xml").body(
|
||||
serde_xml_rs::SerdeXml::new()
|
||||
.default_namespace("http://maven.apache.org/POM/4.0.0")
|
||||
.namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
.to_string(&respdata)
|
||||
.map_err(ApiError::Xml)?,
|
||||
));
|
||||
} else if let Some(selected_file) =
|
||||
find_file(&project_id, &vnum, &version, &file)
|
||||
{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::file_hosting::FileHostingError;
|
||||
use crate::routes::analytics::{page_view_ingest, playtime_ingest};
|
||||
use crate::util::cors::default_cors;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use actix_cors::Cors;
|
||||
use actix_files::Files;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use futures::FutureExt;
|
||||
|
||||
pub mod error;
|
||||
pub mod internal;
|
||||
pub mod v2;
|
||||
pub mod v3;
|
||||
@@ -84,162 +83,3 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
|
||||
.service(Files::new("/", "assets/")),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error(transparent)]
|
||||
Internal(eyre::Report),
|
||||
#[error(transparent)]
|
||||
Request(eyre::Report),
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
#[error("Environment error")]
|
||||
Env(#[from] dotenvy::Error),
|
||||
#[error("Error while uploading file: {0}")]
|
||||
FileHosting(#[from] FileHostingError),
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] crate::database::models::DatabaseError),
|
||||
#[error("SQLx database error: {0}")]
|
||||
SqlxDatabase(#[from] sqlx::Error),
|
||||
#[error("Redis database error: {0}")]
|
||||
RedisDatabase(#[from] redis::RedisError),
|
||||
#[error("Clickhouse error: {0}")]
|
||||
Clickhouse(#[from] clickhouse::error::Error),
|
||||
#[error("XML error: {0}")]
|
||||
Xml(String),
|
||||
#[error("Deserialization error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("Authentication error: {0}")]
|
||||
Authentication(#[from] crate::auth::AuthenticationError),
|
||||
#[error("Authentication error: {0}")]
|
||||
CustomAuthentication(String),
|
||||
#[error("Error while validating input: {0}")]
|
||||
Validation(String),
|
||||
#[error("Search error: {0}")]
|
||||
Search(#[from] meilisearch_sdk::errors::Error),
|
||||
#[error("Indexing error: {0}")]
|
||||
Indexing(#[from] crate::search::indexing::IndexingError),
|
||||
#[error("Payments error: {0}")]
|
||||
Payments(String),
|
||||
#[error("Discord error: {0}")]
|
||||
Discord(String),
|
||||
#[error("Slack webhook error: {0}")]
|
||||
Slack(String),
|
||||
#[error("Captcha error. Try resubmitting the form.")]
|
||||
Turnstile,
|
||||
#[error("Error while decoding Base62: {0}")]
|
||||
Decoding(#[from] ariadne::ids::DecodingError),
|
||||
#[error("Image parsing error: {0}")]
|
||||
ImageParse(#[from] image::ImageError),
|
||||
#[error("Password hashing error: {0}")]
|
||||
PasswordHashing(#[from] argon2::password_hash::Error),
|
||||
#[error("{0}")]
|
||||
Mail(#[from] crate::queue::email::MailError),
|
||||
#[error("Error while rerouting request: {0:?}")]
|
||||
Reroute(#[from] reqwest::Error),
|
||||
#[error("Unable to read zip archive: {0}")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
#[error("IO Error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Resource not found")]
|
||||
NotFound,
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
#[error("External tax compliance API error")]
|
||||
TaxComplianceApi,
|
||||
#[error(transparent)]
|
||||
TaxProcessor(#[from] crate::util::anrok::AnrokError),
|
||||
#[error(
|
||||
"You are being rate-limited. Please wait {0} milliseconds. 0/{1} remaining."
|
||||
)]
|
||||
RateLimitError(u128, u32),
|
||||
#[error("Error while interacting with payment processor: {0}")]
|
||||
Stripe(#[from] stripe::StripeError),
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> {
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::Internal(..) => "internal_error",
|
||||
Self::Request(..) => "request_error",
|
||||
ApiError::Env(..) => "environment_error",
|
||||
ApiError::Database(..) => "database_error",
|
||||
ApiError::SqlxDatabase(..) => "database_error",
|
||||
ApiError::RedisDatabase(..) => "database_error",
|
||||
ApiError::Authentication(..) => "unauthorized",
|
||||
ApiError::CustomAuthentication(..) => "unauthorized",
|
||||
ApiError::Xml(..) => "xml_error",
|
||||
ApiError::Json(..) => "json_error",
|
||||
ApiError::Search(..) => "search_error",
|
||||
ApiError::Indexing(..) => "indexing_error",
|
||||
ApiError::FileHosting(..) => "file_hosting_error",
|
||||
ApiError::InvalidInput(..) => "invalid_input",
|
||||
ApiError::Validation(..) => "invalid_input",
|
||||
ApiError::Payments(..) => "payments_error",
|
||||
ApiError::Discord(..) => "discord_error",
|
||||
ApiError::Turnstile => "turnstile_error",
|
||||
ApiError::Decoding(..) => "decoding_error",
|
||||
ApiError::ImageParse(..) => "invalid_image",
|
||||
ApiError::PasswordHashing(..) => "password_hashing_error",
|
||||
ApiError::Mail(..) => "mail_error",
|
||||
ApiError::Clickhouse(..) => "clickhouse_error",
|
||||
ApiError::Reroute(..) => "reroute_error",
|
||||
ApiError::NotFound => "not_found",
|
||||
ApiError::Conflict(..) => "conflict",
|
||||
ApiError::TaxComplianceApi => "tax_compliance_api_error",
|
||||
ApiError::Zip(..) => "zip_error",
|
||||
ApiError::Io(..) => "io_error",
|
||||
ApiError::RateLimitError(..) => "ratelimit_error",
|
||||
ApiError::Stripe(..) => "stripe_error",
|
||||
ApiError::TaxProcessor(..) => "tax_processor_error",
|
||||
ApiError::Slack(..) => "slack_error",
|
||||
},
|
||||
description: self.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for ApiError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
ApiError::Internal(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Request(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::InvalidInput(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::RedisDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Clickhouse(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Authentication(..) => StatusCode::UNAUTHORIZED,
|
||||
ApiError::CustomAuthentication(..) => StatusCode::UNAUTHORIZED,
|
||||
ApiError::Xml(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Json(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Search(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Indexing(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Validation(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Payments(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
ApiError::Discord(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
ApiError::Turnstile => StatusCode::BAD_REQUEST,
|
||||
ApiError::Decoding(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::ImageParse(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::PasswordHashing(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Reroute(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::NotFound => StatusCode::NOT_FOUND,
|
||||
ApiError::Conflict(..) => StatusCode::CONFLICT,
|
||||
ApiError::TaxComplianceApi => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Zip(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Io(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::RateLimitError(..) => StatusCode::TOO_MANY_REQUESTS,
|
||||
ApiError::Stripe(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
ApiError::TaxProcessor(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Slack(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(self.as_api_error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
use crate::models::error::ApiError;
|
||||
use actix_web::{HttpResponse, Responder};
|
||||
use crate::routes::error::ApiError;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
|
||||
pub async fn not_found() -> impl Responder {
|
||||
let data = ApiError {
|
||||
error: "not_found",
|
||||
description: "the requested route does not exist".to_string(),
|
||||
};
|
||||
|
||||
HttpResponse::NotFound().json(data)
|
||||
pub async fn not_found() -> HttpResponse {
|
||||
ApiError::RouteNotFound.error_response()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::projects::VersionType;
|
||||
use crate::queue::session::AuthQueue;
|
||||
|
||||
use super::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(forge_updates);
|
||||
|
||||
@@ -12,7 +12,7 @@ mod version_creation;
|
||||
pub mod version_file;
|
||||
mod versions;
|
||||
|
||||
pub use super::ApiError;
|
||||
pub use crate::routes::error::ApiError;
|
||||
use crate::util::cors::default_cors;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::ApiError;
|
||||
use crate::models::projects::Project;
|
||||
use crate::models::v2::projects::LegacyProject;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::internal;
|
||||
use crate::{database::redis::RedisPool, routes::v2_reroute};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::models::ids::NotificationId;
|
||||
use crate::models::notifications::Notification;
|
||||
use crate::models::v2::notifications::LegacyNotification;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::v2_reroute;
|
||||
use crate::routes::v3;
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::models::v2::projects::{
|
||||
DonationLink, LegacyProject, LegacySideType,
|
||||
};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::NewGalleryItem;
|
||||
use crate::routes::v3::project_creation::default_project_type;
|
||||
use crate::routes::v3::project_creation::{CreateError, NewGalleryItem};
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::web::Data;
|
||||
@@ -18,12 +18,12 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
use super::version_creation::InitialVersionData;
|
||||
use crate::routes::v3::create_error::CreateError;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
use super::version_creation::InitialVersionData;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(project_create);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ use crate::models::v2::projects::{
|
||||
use crate::models::v2::search::LegacySearchResults;
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::v3::projects::ProjectIds;
|
||||
use crate::routes::{ApiError, v2_reroute, v3};
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use crate::search::{SearchConfig, SearchError, search_for_project};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::database::redis::RedisPool;
|
||||
use crate::models::reports::Report;
|
||||
use crate::models::v2::reports::LegacyReport;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::{ApiError, v2_reroute, v3};
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::{
|
||||
ApiError, v2_reroute,
|
||||
v2_reroute,
|
||||
v3::{self, statistics::V3Stats},
|
||||
};
|
||||
use actix_web::{HttpResponse, get, web};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ApiError;
|
||||
use crate::database::models::loader_fields::LoaderFieldEnumValue;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v2::projects::LegacySideType;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::v2_reroute::capitalize_first;
|
||||
use crate::routes::v3::tags::{LinkPlatformQueryData, LoaderFieldsEnumQuery};
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::models::teams::{
|
||||
};
|
||||
use crate::models::v2::teams::LegacyTeamMember;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::{ApiError, v2_reroute, v3};
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
||||
use ariadne::ids::UserId;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::models::ids::{ThreadId, ThreadMessageId};
|
||||
use crate::models::threads::{MessageBody, Thread};
|
||||
use crate::models::v2::threads::LegacyThread;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::{ApiError, v2_reroute, v3};
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::models::v2::notifications::LegacyNotification;
|
||||
use crate::models::v2::projects::LegacyProject;
|
||||
use crate::models::v2::user::LegacyUser;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::{ApiError, v2_reroute, v3};
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::models::projects::{
|
||||
use crate::models::v2::projects::LegacyVersion;
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::routes::v3::create_error::CreateError;
|
||||
use crate::routes::v3::version_creation;
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use actix_multipart::Multipart;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::ApiError;
|
||||
use crate::database::ReadOnlyPgPool;
|
||||
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::error::ApiError;
|
||||
use crate::routes::v3::version_file::HashQuery;
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ApiError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models;
|
||||
use crate::models::ids::VersionId;
|
||||
@@ -9,6 +8,7 @@ use crate::models::projects::{
|
||||
};
|
||||
use crate::models::v2::projects::LegacyVersion;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::{v2_reroute, v3};
|
||||
use crate::search::SearchConfig;
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ApiError;
|
||||
use super::v3::project_creation::CreateError;
|
||||
use crate::models::v2::projects::LegacySideType;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::v3::create_error::{CreateError, CreationInvalidInput};
|
||||
use crate::util::actix::{
|
||||
MultipartSegment, MultipartSegmentData, generate_multipart,
|
||||
};
|
||||
@@ -124,12 +124,11 @@ where
|
||||
// Finishes the json segment, with aggregated content dispositions
|
||||
{
|
||||
let json_value = json.ok_or(CreateError::InvalidInput(
|
||||
"No json segment found in multipart.".to_string(),
|
||||
CreationInvalidInput::NoJsonInMultipart,
|
||||
))?;
|
||||
let mut json_segment =
|
||||
json_segment.ok_or(CreateError::InvalidInput(
|
||||
"No json segment found in multipart.".to_string(),
|
||||
))?;
|
||||
let mut json_segment = json_segment.ok_or(
|
||||
CreateError::InvalidInput(CreationInvalidInput::NoJsonInMultipart),
|
||||
)?;
|
||||
|
||||
// Call closure, with the json value and names of the other segments
|
||||
let json_value: U = closure(json_value, content_dispositions).await?;
|
||||
@@ -142,20 +141,13 @@ where
|
||||
|
||||
let (boundary, payload) = generate_multipart(segments);
|
||||
|
||||
match (
|
||||
if let Ok((key, value)) = (
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={boundary}").as_str(),
|
||||
)
|
||||
.try_into_pair()
|
||||
{
|
||||
Ok((key, value)) => {
|
||||
headers.insert(key, value);
|
||||
}
|
||||
Err(err) => {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Error inserting test header: {err:?}."
|
||||
));
|
||||
}
|
||||
headers.insert(key, value);
|
||||
};
|
||||
|
||||
let new_multipart =
|
||||
|
||||
@@ -16,6 +16,7 @@ use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::routes::error::ApiInvalidInput;
|
||||
use crate::{
|
||||
auth::{AuthenticationError, get_user_from_headers},
|
||||
database::{
|
||||
@@ -29,7 +30,7 @@ use crate::{
|
||||
teams::ProjectPermissions,
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
routes::ApiError,
|
||||
routes::error::ApiError,
|
||||
};
|
||||
|
||||
// TODO: this service `analytics` is shadowed by `analytics_get_old`'s
|
||||
@@ -451,9 +452,7 @@ pub async fn get(
|
||||
TimeRangeResolution::Minutes(resolution_minutes) => {
|
||||
let resolution_minutes = i64::try_from(resolution_minutes.get())
|
||||
.map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"Resolution must fit into a `i64`".into(),
|
||||
)
|
||||
ApiError::InvalidInput(ApiInvalidInput::ResolutionMinutes)
|
||||
})?;
|
||||
let resolution = TimeDelta::try_minutes(resolution_minutes)
|
||||
.ok_or_else(|| {
|
||||
|
||||
@@ -10,8 +10,8 @@ use crate::models::ids::{CollectionId, ProjectId};
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::v3::user_limits::UserLimits;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::routes::v3::create_error::{CreateError, CreationInvalidInput};
|
||||
use crate::util::error::Context;
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::util::routes::read_limited_from_payload;
|
||||
@@ -86,7 +86,9 @@ pub async fn collection_create(
|
||||
}
|
||||
|
||||
collection_create_data.validate().map_err(|err| {
|
||||
CreateError::InvalidInput(validation_errors_to_string(err, None))
|
||||
CreateError::InvalidInput(CreationInvalidInput::Validation(
|
||||
validation_errors_to_string(err, None),
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut transaction = client.begin().await?;
|
||||
@@ -121,7 +123,7 @@ pub async fn collection_create(
|
||||
let now = Utc::now();
|
||||
collection_builder_actual.insert(&mut transaction).await?;
|
||||
|
||||
let response = crate::models::collections::Collection {
|
||||
let response = Collection {
|
||||
id: collection_id,
|
||||
user: collection_builder.user_id.into(),
|
||||
name: collection_builder.name.clone(),
|
||||
@@ -145,9 +147,9 @@ pub struct CollectionIds {
|
||||
pub async fn collections_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<CollectionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let ids = serde_json::from_str::<Vec<&str>>(&ids.ids)?;
|
||||
let ids = ids
|
||||
@@ -181,9 +183,9 @@ pub async fn collections_get(
|
||||
pub async fn collection_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
|
||||
@@ -231,10 +233,10 @@ pub struct EditCollection {
|
||||
pub async fn collection_edit(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
pool: Data<PgPool>,
|
||||
new_collection: web::Json<EditCollection>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
@@ -297,8 +299,8 @@ pub async fn collection_edit(
|
||||
|| collection_item.status.is_approved()
|
||||
&& status.can_be_requested())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to set this status!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::SetStatus,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -387,11 +389,11 @@ pub async fn collection_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
@@ -471,10 +473,10 @@ pub async fn collection_icon_edit(
|
||||
pub async fn delete_collection_icon(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
@@ -530,9 +532,9 @@ pub async fn delete_collection_icon(
|
||||
pub async fn collection_delete(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
use crate::auth::AuthenticationError;
|
||||
use crate::database::models;
|
||||
use crate::database::models::loader_fields::VersionFieldParseError;
|
||||
use crate::file_hosting::FileHostingError;
|
||||
use crate::models::error::AsApiError;
|
||||
use crate::models::ids::ImageId;
|
||||
use crate::models::projects::VersionStatus;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::search::indexing::IndexingError;
|
||||
use crate::validate::ValidationWarning;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::http::StatusCode;
|
||||
use ariadne::i18n_enum;
|
||||
use derive_more::{Display, From};
|
||||
use image::ImageError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CreateError {
|
||||
#[error("Environment Error")]
|
||||
EnvError(#[from] dotenvy::Error),
|
||||
#[error("An unknown database error occurred")]
|
||||
SqlxDatabaseError(#[from] sqlx::Error),
|
||||
#[error("Database Error: {0}")]
|
||||
DatabaseError(#[from] models::DatabaseError),
|
||||
#[error("Indexing Error: {0}")]
|
||||
IndexingError(#[from] IndexingError),
|
||||
#[error("Error while parsing multipart payload: {0}")]
|
||||
MultipartError(#[from] actix_multipart::MultipartError),
|
||||
#[error("Error while parsing JSON: {0}")]
|
||||
SerDeError(#[from] serde_json::Error),
|
||||
#[error("Error while validating input: {0}")]
|
||||
ValidationError(String),
|
||||
#[error("Error while uploading file: {0}")]
|
||||
FileHostingError(#[from] FileHostingError),
|
||||
#[error("Error while validating uploaded file: {0}")]
|
||||
FileValidationError(#[from] crate::validate::ValidationError),
|
||||
#[error("{0}")]
|
||||
MissingValueError(MissingValuePart),
|
||||
#[error("Invalid format for image: {0}")]
|
||||
InvalidIconFormat(ApiError),
|
||||
#[error("Error with multipart data: {0}")]
|
||||
InvalidInput(CreationInvalidInput),
|
||||
#[error("Error with multipart data: {0}")]
|
||||
InvalidLoaderField(#[from] VersionFieldParseError),
|
||||
#[error("Invalid loader: {0}")]
|
||||
InvalidLoader(String),
|
||||
#[error("Invalid category: {0}")]
|
||||
InvalidCategory(String),
|
||||
#[error("Invalid file type for version file: {0}")]
|
||||
InvalidFileType(String),
|
||||
#[error("Slug is already taken!")]
|
||||
SlugCollision,
|
||||
#[error("Authentication Error: {0}")]
|
||||
Unauthorized(#[from] AuthenticationError),
|
||||
#[error("Authentication Error: {0}")]
|
||||
CreationAuthenticationError(CreationAuthenticationError),
|
||||
#[error("Image Parsing Error: {0}")]
|
||||
ImageError(#[from] ImageError),
|
||||
#[error("Project limit reached")]
|
||||
LimitReached,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
CreateError,
|
||||
root_key: "labrinth.error.creation",
|
||||
EnvError(..) => "environment_error",
|
||||
SqlxDatabaseError(..) => "database_error.unknown",
|
||||
DatabaseError(cause) => "database_error",
|
||||
IndexingError(cause) => "indexing_error",
|
||||
MultipartError(cause) => "invalid_input.multipart",
|
||||
SerDeError(cause) => "invalid_input.parsing",
|
||||
ValidationError(cause) => "invalid_input.validation",
|
||||
FileHostingError(cause) => "file_hosting_error",
|
||||
FileValidationError(cause) => "invalid_input.file",
|
||||
MissingValueError(transparent cause) => "invalid_input.missing_value",
|
||||
InvalidIconFormat(cause) => "invalid_input.icon",
|
||||
InvalidInput(cause) => "invalid_input",
|
||||
InvalidLoaderField(cause) => "invalid_input",
|
||||
InvalidLoader(loader) => "invalid_input.loader",
|
||||
InvalidCategory(category) => "invalid_input.category",
|
||||
InvalidFileType(extension) => "invalid_input.file_type",
|
||||
SlugCollision! => "invalid_input.slug_collision",
|
||||
Unauthorized(cause_or_reason) => "unauthorized",
|
||||
CreationAuthenticationError(cause_or_reason) => "unauthorized",
|
||||
ImageError(cause) => "invalid_image",
|
||||
LimitReached! => "limit_reached",
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display)]
|
||||
pub enum MissingValuePart {
|
||||
#[display("No `data` field in multipart upload")]
|
||||
DataField,
|
||||
#[display("Missing content name")]
|
||||
ContentName,
|
||||
#[display("Missing content file name")]
|
||||
ContentFileName,
|
||||
#[display("Missing content file extension")]
|
||||
ContentFileExtension,
|
||||
#[display("Missing project id")]
|
||||
ProjectId,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
MissingValuePart,
|
||||
root_key: "labrinth.error.creation.missing_value",
|
||||
DataField! => "data_field",
|
||||
ContentName! => "content_name",
|
||||
ContentFileName! => "content_file_name",
|
||||
ContentFileExtension! => "content_file_extension",
|
||||
ProjectId! => "project_id",
|
||||
);
|
||||
|
||||
#[derive(Debug, Display, From)]
|
||||
pub enum CreationInvalidInput {
|
||||
#[display("Failed to get created team.")]
|
||||
FailedGettingNewTeam,
|
||||
#[display("`data` field must come before file fields")]
|
||||
DataFieldOutOfOrder,
|
||||
#[display("Duplicate multipart field name")]
|
||||
DuplicateMultipartField,
|
||||
#[display("Projects can only have one icon")]
|
||||
MultipleIcons,
|
||||
#[display("Only one gallery image can be featured.")]
|
||||
MultipleFeaturedGallery,
|
||||
#[display("Gallery image exceeds the maximum of {_0}.")]
|
||||
GalleryImageTooLarge(&'static str),
|
||||
#[display("File `{_0}` (field {_1}) isn't specified in the versions data")]
|
||||
FileNotSpecified(String, String),
|
||||
#[display("Some files were specified in initial_versions but not uploaded")]
|
||||
InitialVersionsFilesMissing,
|
||||
#[display("Invalid organization ID specified!")]
|
||||
InvalidOrganizationId,
|
||||
#[display("Project submitted for review with no initial versions")]
|
||||
NoInitialVersions,
|
||||
#[from]
|
||||
#[display("Invalid SPDX license identifier: {_0}")]
|
||||
InvalidLicenseId(spdx::ParseError),
|
||||
#[display("Link platform {_0} does not exist.")]
|
||||
NonexistentLinkPlatform(String),
|
||||
#[display("Image {_0} is not unused or in the '{_1}' context")]
|
||||
ImproperContextImage(ImageId, &'static str),
|
||||
#[display("Image {_0} does not exist")]
|
||||
NonexistentImage(ImageId),
|
||||
#[display("Found project id in initial version for new project")]
|
||||
ProjectIdInInitialVersion,
|
||||
#[display("Icons must be smaller than {_0}")]
|
||||
IconTooLarge(&'static str),
|
||||
#[display("Status '{_0}' cannot be requested")]
|
||||
CannotRequestStatus(VersionStatus),
|
||||
#[display("An invalid project id was supplied")]
|
||||
InvalidProjectId,
|
||||
#[display("An invalid version id was supplied")]
|
||||
InvalidVersionId,
|
||||
#[display("`data` field is required")]
|
||||
MissingDataField,
|
||||
#[display("Versions must have at least one file uploaded to them")]
|
||||
MissingAnyFiles,
|
||||
#[display("At least one file must be specified")]
|
||||
NoFilesSpecified,
|
||||
#[display("Duplicate files are not allowed to be uploaded to Modrinth!")]
|
||||
DuplicateFiles,
|
||||
#[display("File names must not contain slashes!")]
|
||||
FileNameHasSlashes,
|
||||
#[display(
|
||||
"Project file exceeds the maximum of {_0}. Contact a moderator or admin to request permission to upload larger files."
|
||||
)]
|
||||
ProjectFileTooLarge(&'static str),
|
||||
#[display("Loader field '{_0}' does not exist for any loaders supplied")]
|
||||
NonexistentLoaderField(String),
|
||||
#[display("Missing mandatory loader fields: {_0}")]
|
||||
MissingLoaderFields(String),
|
||||
#[display("No json segment found in multipart.")]
|
||||
NoJsonInMultipart,
|
||||
FileValidation(ValidationWarning),
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
CreationInvalidInput,
|
||||
root_key: "labrinth.error.creation.invalid_input",
|
||||
FailedGettingNewTeam! => "failed_getting_new_team",
|
||||
DataFieldOutOfOrder! => "data_field_out_of_order",
|
||||
DuplicateMultipartField! => "duplicate_multipart_field",
|
||||
MultipleIcons! => "multiple_icons",
|
||||
MultipleFeaturedGallery! => "multiple_featured_gallery",
|
||||
GalleryImageTooLarge(limit) => "gallery_image_too_large",
|
||||
FileNotSpecified(file_name, in_field) => "file_not_specified",
|
||||
InitialVersionsFilesMissing! => "initial_versions_files_missing",
|
||||
InvalidOrganizationId! => "invalid_organization_id",
|
||||
NoInitialVersions! => "no_initial_versions",
|
||||
InvalidLicenseId(cause) => "invalid_license_id",
|
||||
NonexistentLinkPlatform(platform) => "nonexistent_link_platform",
|
||||
ImproperContextImage(image_id, proper_context) => "improper_context_image",
|
||||
NonexistentImage(image_id) => "nonexistent_image",
|
||||
ProjectIdInInitialVersion! => "project_id_in_initial_version",
|
||||
IconTooLarge(limit) => "icon_too_large",
|
||||
CannotRequestStatus(status) => "cannot_request_status",
|
||||
InvalidProjectId! => "invalid_project_id",
|
||||
InvalidVersionId! => "invalid_version_id",
|
||||
MissingDataField! => "missing_data_field",
|
||||
MissingAnyFiles! => "missing_any_files",
|
||||
NoFilesSpecified! => "no_files_specified",
|
||||
DuplicateFiles! => "duplicate_files",
|
||||
FileNameHasSlashes! => "file_name_has_slashes",
|
||||
ProjectFileTooLarge(limit) => "project_file_too_large",
|
||||
NonexistentLoaderField(field) => "nonexistent_loader_field",
|
||||
MissingLoaderFields(fields) => "missing_loader_fields",
|
||||
NoJsonInMultipart! => "no_json_in_multipart",
|
||||
FileValidation(transparent cause) => "file_validation",
|
||||
Validation(transparent reason) => "validation",
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display)]
|
||||
pub enum CreationAuthenticationError {
|
||||
#[display(
|
||||
"You do not have the permissions to create projects in this organization!"
|
||||
)]
|
||||
CreateProjectsInOrganization,
|
||||
#[display("You don't have permission to upload this version!")]
|
||||
UploadVersion,
|
||||
#[display("You don't have permission to upload files to this version!")]
|
||||
UploadFilesToVersion,
|
||||
}
|
||||
|
||||
i18n_enum!(
|
||||
CreationAuthenticationError,
|
||||
root_key: "labrinth.error.creation.unauthorized",
|
||||
CreateProjectsInOrganization! => "create_projects_in_organization",
|
||||
UploadVersion! => "upload_version",
|
||||
UploadFilesToVersion! => "upload_files_to_version",
|
||||
);
|
||||
|
||||
impl actix_web::ResponseError for CreateError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::SqlxDatabaseError(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::IndexingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::FileHostingError(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
CreateError::SerDeError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidIconFormat(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidInput(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidLoaderField(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidLoader(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
|
||||
CreateError::CreationAuthenticationError(..) => {
|
||||
StatusCode::UNAUTHORIZED
|
||||
}
|
||||
CreateError::SlugCollision => StatusCode::BAD_REQUEST,
|
||||
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::ImageError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::LimitReached => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(self.as_api_error())
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::users::UserFriend;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::queue::socket::ActiveSockets;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::routes::internal::statuses::{
|
||||
broadcast_friends_message, send_message_to_user,
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::ids::{ReportId, ThreadMessageId, VersionId};
|
||||
use crate::models::images::{Image, ImageContext};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::routes::read_limited_from_payload;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
@@ -76,8 +76,8 @@ pub async fn images_add(
|
||||
{
|
||||
*project_id = Some(project.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this project".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::UploadProjectImages,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -103,8 +103,8 @@ pub async fn images_add(
|
||||
{
|
||||
*version_id = Some(version.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this version".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::UploadVersionImages,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -136,9 +136,8 @@ pub async fn images_add(
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*thread_message_id = Some(thread_message.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this thread message"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::UploadThreadMessageImages,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -162,8 +161,8 @@ pub async fn images_add(
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*report_id = Some(report.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this report".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::UploadReportImages,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
database::redis::RedisPool,
|
||||
models::{pats::Scopes, v3::user_limits::UserLimits},
|
||||
queue::session::AuthQueue,
|
||||
routes::ApiError,
|
||||
routes::error::ApiError,
|
||||
};
|
||||
use actix_web::{HttpRequest, web};
|
||||
use sqlx::PgPool;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub use super::ApiError;
|
||||
pub use crate::routes::error::ApiError;
|
||||
use crate::util::cors::default_cors;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde_json::json;
|
||||
@@ -6,6 +6,7 @@ use serde_json::json;
|
||||
pub mod analytics_get;
|
||||
pub mod analytics_get_old;
|
||||
pub mod collections;
|
||||
pub mod create_error;
|
||||
pub mod friends;
|
||||
pub mod images;
|
||||
pub mod limits;
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::models::ids::NotificationId;
|
||||
use crate::models::notifications::Notification;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
@@ -55,11 +55,7 @@ pub async fn notifications_get(
|
||||
.collect();
|
||||
|
||||
let notifications_data: Vec<DBNotification> =
|
||||
database::models::notification_item::DBNotification::get_many(
|
||||
¬ification_ids,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
DBNotification::get_many(¬ification_ids, &**pool).await?;
|
||||
|
||||
let notifications: Vec<Notification> = notifications_data
|
||||
.into_iter()
|
||||
@@ -148,8 +144,8 @@ pub async fn notification_read(
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to read this notification!".to_string(),
|
||||
Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::NotificationRead,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
@@ -198,9 +194,8 @@ pub async fn notification_delete(
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to delete this notification!"
|
||||
.to_string(),
|
||||
Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::NotificationDelete,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::{collections::HashSet, fmt::Display, sync::Arc};
|
||||
|
||||
use super::ApiError;
|
||||
use crate::file_hosting::FileHostPublicity;
|
||||
use crate::models::ids::OAuthClientId;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::routes::v3::create_error::CreateError;
|
||||
use crate::util::img::{delete_old_images, upload_image_optimized};
|
||||
use crate::{
|
||||
auth::{checks::ValidateAuthorized, get_user_from_headers},
|
||||
@@ -21,7 +22,6 @@ use crate::{
|
||||
pats::Scopes,
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
routes::v3::project_creation::CreateError,
|
||||
util::validate::validation_errors_to_string,
|
||||
};
|
||||
use crate::{
|
||||
@@ -80,8 +80,8 @@ pub async fn get_user_clients(
|
||||
if target_user.id != current_user.id.into()
|
||||
&& !current_user.role.is_admin()
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to see the OAuth clients of this user!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::SeeOAuthClients,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::ApiError;
|
||||
use crate::auth::checks::is_visible_organization;
|
||||
use crate::auth::{filter_visible_projects, get_user_from_headers};
|
||||
use crate::database::models::team_item::DBTeamMember;
|
||||
@@ -15,7 +14,8 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
use crate::models::v3::user_limits::UserLimits;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::routes::v3::create_error::{CreateError, CreationInvalidInput};
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::util::routes::read_limited_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
@@ -89,7 +89,7 @@ pub async fn organization_projects_get(
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
let projects_data = crate::database::models::DBProject::get_many_ids(
|
||||
let projects_data = database::models::DBProject::get_many_ids(
|
||||
&project_ids,
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -173,7 +173,7 @@ pub async fn organization_create(
|
||||
let team = team_item::TeamBuilder {
|
||||
members: vec![team_item::TeamMemberBuilder {
|
||||
user_id: current_user.id.into(),
|
||||
role: crate::models::teams::DEFAULT_ROLE.to_owned(),
|
||||
role: models::teams::DEFAULT_ROLE.to_owned(),
|
||||
is_owner: true,
|
||||
permissions: ProjectPermissions::all(),
|
||||
organization_permissions: Some(OrganizationPermissions::all()),
|
||||
@@ -205,14 +205,14 @@ pub async fn organization_create(
|
||||
.into_iter()
|
||||
.next();
|
||||
let members_data = if let Some(member_data) = member_data {
|
||||
vec![crate::models::teams::TeamMember::from_model(
|
||||
vec![models::teams::TeamMember::from_model(
|
||||
member_data,
|
||||
current_user.clone(),
|
||||
false,
|
||||
)]
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Failed to get created team.".to_owned(), // should never happen
|
||||
CreationInvalidInput::FailedGettingNewTeam, // should never happen
|
||||
));
|
||||
};
|
||||
|
||||
@@ -250,7 +250,7 @@ pub async fn organization_get(
|
||||
DBTeamMember::get_from_team_full(data.team_id, &**pool, &redis)
|
||||
.await?;
|
||||
|
||||
let users = crate::database::models::DBUser::get_many_ids(
|
||||
let users = database::models::DBUser::get_many_ids(
|
||||
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -269,13 +269,13 @@ pub async fn organization_get(
|
||||
.filter(|x| {
|
||||
logged_in
|
||||
|| x.accepted
|
||||
|| user_id.is_some_and(
|
||||
|y: crate::database::models::DBUserId| y == x.user_id,
|
||||
)
|
||||
|| user_id.is_some_and(|y: database::models::DBUserId| {
|
||||
y == x.user_id
|
||||
})
|
||||
})
|
||||
.filter_map(|data| {
|
||||
users.iter().find(|x| x.id == data.user_id).map(|user| {
|
||||
crate::models::teams::TeamMember::from(
|
||||
models::teams::TeamMember::from(
|
||||
data,
|
||||
user.clone(),
|
||||
!logged_in,
|
||||
@@ -314,7 +314,7 @@ pub async fn organizations_get(
|
||||
let teams_data =
|
||||
DBTeamMember::get_from_team_full_many(&team_ids, &**pool, &redis)
|
||||
.await?;
|
||||
let users = crate::database::models::DBUser::get_many_ids(
|
||||
let users = database::models::DBUser::get_many_ids(
|
||||
&teams_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -361,13 +361,13 @@ pub async fn organizations_get(
|
||||
.filter(|x| {
|
||||
logged_in
|
||||
|| x.accepted
|
||||
|| user_id.is_some_and(
|
||||
|y: crate::database::models::DBUserId| y == x.user_id,
|
||||
)
|
||||
|| user_id.is_some_and(|y: database::models::DBUserId| {
|
||||
y == x.user_id
|
||||
})
|
||||
})
|
||||
.filter_map(|data| {
|
||||
users.iter().find(|x| x.id == data.user_id).map(|user| {
|
||||
crate::models::teams::TeamMember::from(
|
||||
models::teams::TeamMember::from(
|
||||
data,
|
||||
user.clone(),
|
||||
!logged_in,
|
||||
@@ -420,12 +420,11 @@ pub async fn organizations_edit(
|
||||
})?;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let result =
|
||||
database::models::DBOrganization::get(&string, &**pool, &redis).await?;
|
||||
let result = DBOrganization::get(&string, &**pool, &redis).await?;
|
||||
if let Some(organization_item) = result {
|
||||
let id = organization_item.id;
|
||||
|
||||
let team_member = database::models::DBTeamMember::get_from_user_id(
|
||||
let team_member = DBTeamMember::get_from_user_id(
|
||||
organization_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -441,9 +440,8 @@ pub async fn organizations_edit(
|
||||
let mut transaction = pool.begin().await?;
|
||||
if let Some(description) = &new_organization.description {
|
||||
if !perms.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the description of this organization!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditOrgDescription,
|
||||
));
|
||||
}
|
||||
sqlx::query!(
|
||||
@@ -461,9 +459,8 @@ pub async fn organizations_edit(
|
||||
|
||||
if let Some(name) = &new_organization.name {
|
||||
if !perms.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the name of this organization!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditOrgName,
|
||||
));
|
||||
}
|
||||
sqlx::query!(
|
||||
@@ -481,9 +478,8 @@ pub async fn organizations_edit(
|
||||
|
||||
if let Some(slug) = &new_organization.slug {
|
||||
if !perms.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the slug of this organization!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditOrgSlug,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -542,7 +538,7 @@ pub async fn organizations_edit(
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::DBOrganization::clear_cache(
|
||||
DBOrganization::clear_cache(
|
||||
organization_item.id,
|
||||
Some(organization_item.slug),
|
||||
&redis,
|
||||
@@ -551,9 +547,8 @@ pub async fn organizations_edit(
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to edit this organization!"
|
||||
.to_string(),
|
||||
Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditOrg,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
@@ -579,30 +574,28 @@ pub async fn organization_delete(
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let organization =
|
||||
database::models::DBOrganization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let organization = DBOrganization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !user.role.is_admin() {
|
||||
let team_member =
|
||||
database::models::DBTeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
let team_member = DBTeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
})?;
|
||||
|
||||
let permissions = OrganizationPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
@@ -611,9 +604,8 @@ pub async fn organization_delete(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(OrganizationPermissions::DELETE_ORGANIZATION) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to delete this organization!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteOrg,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -650,10 +642,9 @@ pub async fn organization_delete(
|
||||
.await?;
|
||||
|
||||
for organization_project_team in &organization_project_teams {
|
||||
let new_id = crate::database::models::ids::generate_team_member_id(
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
let new_id =
|
||||
database::models::ids::generate_team_member_id(&mut transaction)
|
||||
.await?;
|
||||
let member = DBTeamMember {
|
||||
id: new_id,
|
||||
team_id: *organization_project_team,
|
||||
@@ -669,16 +660,13 @@ pub async fn organization_delete(
|
||||
member.insert(&mut transaction).await?;
|
||||
}
|
||||
// Safely remove the organization
|
||||
let result = database::models::DBOrganization::remove(
|
||||
organization.id,
|
||||
&mut transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
let result =
|
||||
DBOrganization::remove(organization.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
database::models::DBOrganization::clear_cache(
|
||||
DBOrganization::clear_cache(
|
||||
organization.id,
|
||||
Some(organization.slug),
|
||||
&redis,
|
||||
@@ -686,7 +674,7 @@ pub async fn organization_delete(
|
||||
.await?;
|
||||
|
||||
for team_id in &organization_project_teams {
|
||||
database::models::DBTeamMember::clear_cache(*team_id, &redis).await?;
|
||||
DBTeamMember::clear_cache(*team_id, &redis).await?;
|
||||
}
|
||||
|
||||
if !organization_project_teams.is_empty() {
|
||||
@@ -724,14 +712,13 @@ pub async fn organization_projects_add(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let organization =
|
||||
database::models::DBOrganization::get(&info, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let organization = DBOrganization::get(&info, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let project_item = database::models::DBProject::get(
|
||||
&project_info.project_id,
|
||||
@@ -751,37 +738,35 @@ pub async fn organization_projects_add(
|
||||
));
|
||||
}
|
||||
|
||||
let project_team_member =
|
||||
database::models::DBTeamMember::get_from_user_id_project(
|
||||
project_item.inner.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
let project_team_member = DBTeamMember::get_from_user_id_project(
|
||||
project_item.inner.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"You are not a member of this project!".to_string(),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"You are not a member of this project!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let organization_team_member =
|
||||
database::models::DBTeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
})?;
|
||||
let organization_team_member = DBTeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"You are not a member of this organization!".to_string(),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"You are not a member of this organization!".to_string(),
|
||||
)
|
||||
})?;
|
||||
})?;
|
||||
|
||||
// Require ownership of a project to add it to an organization
|
||||
if !current_user.role.is_admin() && !project_team_member.is_owner {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You need to be an owner of a project to add it to an organization!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::NotProjectOwnerForAddToOrg,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -840,11 +825,7 @@ pub async fn organization_projects_add(
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
database::models::DBTeamMember::clear_cache(
|
||||
project_item.inner.team_id,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
DBTeamMember::clear_cache(project_item.inner.team_id, &redis).await?;
|
||||
database::models::DBProject::clear_cache(
|
||||
project_item.inner.id,
|
||||
project_item.inner.slug,
|
||||
@@ -853,9 +834,8 @@ pub async fn organization_projects_add(
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to add projects to this organization!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::AddToOrg,
|
||||
));
|
||||
}
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
@@ -887,17 +867,13 @@ pub async fn organization_projects_remove(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let organization = database::models::DBOrganization::get(
|
||||
&organization_id,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let organization = DBOrganization::get(&organization_id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let project_item =
|
||||
database::models::DBProject::get(&project_id, &**pool, &redis)
|
||||
@@ -919,19 +895,18 @@ pub async fn organization_projects_remove(
|
||||
));
|
||||
}
|
||||
|
||||
let organization_team_member =
|
||||
database::models::DBTeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
let organization_team_member = DBTeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"You are not a member of this organization!".to_string(),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"You are not a member of this organization!".to_string(),
|
||||
)
|
||||
})?;
|
||||
})?;
|
||||
|
||||
let permissions = OrganizationPermissions::get_permissions_by_role(
|
||||
¤t_user.role,
|
||||
@@ -940,7 +915,7 @@ pub async fn organization_projects_remove(
|
||||
.unwrap_or_default();
|
||||
if permissions.contains(OrganizationPermissions::REMOVE_PROJECT) {
|
||||
// Now that permissions are confirmed, we confirm the veracity of the new user as an org member
|
||||
database::models::DBTeamMember::get_from_user_id_organization(
|
||||
DBTeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
data.new_owner.into(),
|
||||
false,
|
||||
@@ -956,14 +931,13 @@ pub async fn organization_projects_remove(
|
||||
|
||||
// Then, we get the team member of the project and that user (if it exists)
|
||||
// We use the team member get directly
|
||||
let new_owner =
|
||||
database::models::DBTeamMember::get_from_user_id_project(
|
||||
project_item.inner.id,
|
||||
data.new_owner.into(),
|
||||
true,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let new_owner = DBTeamMember::get_from_user_id_project(
|
||||
project_item.inner.id,
|
||||
data.new_owner.into(),
|
||||
true,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
@@ -971,11 +945,10 @@ pub async fn organization_projects_remove(
|
||||
let new_owner = match new_owner {
|
||||
Some(new_owner) => new_owner,
|
||||
None => {
|
||||
let new_id =
|
||||
crate::database::models::ids::generate_team_member_id(
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
let new_id = database::models::ids::generate_team_member_id(
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
let member = DBTeamMember {
|
||||
id: new_id,
|
||||
team_id: project_item.inner.team_id,
|
||||
@@ -1028,11 +1001,7 @@ pub async fn organization_projects_remove(
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
database::models::DBTeamMember::clear_cache(
|
||||
project_item.inner.team_id,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
DBTeamMember::clear_cache(project_item.inner.team_id, &redis).await?;
|
||||
database::models::DBProject::clear_cache(
|
||||
project_item.inner.id,
|
||||
project_item.inner.slug,
|
||||
@@ -1041,9 +1010,8 @@ pub async fn organization_projects_remove(
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to add projects to this organization!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::AddToOrg,
|
||||
));
|
||||
}
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
@@ -1076,17 +1044,16 @@ pub async fn organization_icon_edit(
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let organization_item =
|
||||
database::models::DBOrganization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let organization_item = DBOrganization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::DBTeamMember::get_from_user_id(
|
||||
let team_member = DBTeamMember::get_from_user_id(
|
||||
organization_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -1101,9 +1068,8 @@ pub async fn organization_icon_edit(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this organization's icon."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditOrgIcon,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1152,7 +1118,7 @@ pub async fn organization_icon_edit(
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::DBOrganization::clear_cache(
|
||||
DBOrganization::clear_cache(
|
||||
organization_item.id,
|
||||
Some(organization_item.slug),
|
||||
&redis,
|
||||
@@ -1181,17 +1147,16 @@ pub async fn delete_organization_icon(
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let organization_item =
|
||||
database::models::DBOrganization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let organization_item = DBOrganization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified organization does not exist!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::DBTeamMember::get_from_user_id(
|
||||
let team_member = DBTeamMember::get_from_user_id(
|
||||
organization_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -1206,9 +1171,8 @@ pub async fn delete_organization_icon(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this organization's icon."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditOrgIcon,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1236,7 +1200,7 @@ pub async fn delete_organization_icon(
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
database::models::DBOrganization::clear_cache(
|
||||
DBOrganization::clear_cache(
|
||||
organization_item.id,
|
||||
Some(organization_item.slug),
|
||||
&redis,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::payouts::{PayoutMethodType, PayoutStatus};
|
||||
use crate::queue::payouts::PayoutsQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::avalara1099;
|
||||
use crate::util::error::Context;
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::version_creation::{InitialVersionData, try_create_version_fields};
|
||||
use crate::auth::{AuthenticationError, get_user_from_headers};
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::loader_fields::{
|
||||
Loader, LoaderField, LoaderFieldEnumValue,
|
||||
};
|
||||
@@ -7,7 +7,6 @@ use crate::database::models::thread_item::ThreadBuilder;
|
||||
use crate::database::models::{self, DBUser, image_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity, FileHostingError};
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::ids::{ImageId, OrganizationId, ProjectId, VersionId};
|
||||
use crate::models::images::{Image, ImageContext};
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -19,141 +18,32 @@ use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
use crate::models::threads::ThreadType;
|
||||
use crate::models::v3::user_limits::UserLimits;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::search::indexing::IndexingError;
|
||||
use crate::routes::v3::create_error::{
|
||||
CreateError, CreationAuthenticationError, CreationInvalidInput,
|
||||
MissingValuePart,
|
||||
};
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::routes::read_from_field;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_multipart::{Field, Multipart};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{self, Data};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use ariadne::ids::UserId;
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use chrono::Utc;
|
||||
use futures::stream::StreamExt;
|
||||
use image::ImageError;
|
||||
use itertools::Itertools;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use validator::Validate;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("project", web::post().to(project_create));
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CreateError {
|
||||
#[error("Environment Error")]
|
||||
EnvError(#[from] dotenvy::Error),
|
||||
#[error("An unknown database error occurred")]
|
||||
SqlxDatabaseError(#[from] sqlx::Error),
|
||||
#[error("Database Error: {0}")]
|
||||
DatabaseError(#[from] models::DatabaseError),
|
||||
#[error("Indexing Error: {0}")]
|
||||
IndexingError(#[from] IndexingError),
|
||||
#[error("Error while parsing multipart payload: {0}")]
|
||||
MultipartError(#[from] actix_multipart::MultipartError),
|
||||
#[error("Error while parsing JSON: {0}")]
|
||||
SerDeError(#[from] serde_json::Error),
|
||||
#[error("Error while validating input: {0}")]
|
||||
ValidationError(String),
|
||||
#[error("Error while uploading file: {0}")]
|
||||
FileHostingError(#[from] FileHostingError),
|
||||
#[error("Error while validating uploaded file: {0}")]
|
||||
FileValidationError(#[from] crate::validate::ValidationError),
|
||||
#[error("{}", .0)]
|
||||
MissingValueError(String),
|
||||
#[error("Invalid format for image: {0}")]
|
||||
InvalidIconFormat(String),
|
||||
#[error("Error with multipart data: {0}")]
|
||||
InvalidInput(String),
|
||||
#[error("Invalid game version: {0}")]
|
||||
InvalidGameVersion(String),
|
||||
#[error("Invalid loader: {0}")]
|
||||
InvalidLoader(String),
|
||||
#[error("Invalid category: {0}")]
|
||||
InvalidCategory(String),
|
||||
#[error("Invalid file type for version file: {0}")]
|
||||
InvalidFileType(String),
|
||||
#[error("Slug is already taken!")]
|
||||
SlugCollision,
|
||||
#[error("Authentication Error: {0}")]
|
||||
Unauthorized(#[from] AuthenticationError),
|
||||
#[error("Authentication Error: {0}")]
|
||||
CustomAuthenticationError(String),
|
||||
#[error("Image Parsing Error: {0}")]
|
||||
ImageError(#[from] ImageError),
|
||||
#[error("Project limit reached")]
|
||||
LimitReached,
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for CreateError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::SqlxDatabaseError(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::IndexingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateError::FileHostingError(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
CreateError::SerDeError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidIconFormat(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidInput(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidGameVersion(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidLoader(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
|
||||
CreateError::CustomAuthenticationError(..) => {
|
||||
StatusCode::UNAUTHORIZED
|
||||
}
|
||||
CreateError::SlugCollision => StatusCode::BAD_REQUEST,
|
||||
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::ImageError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::LimitReached => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(ApiError {
|
||||
error: match self {
|
||||
CreateError::EnvError(..) => "environment_error",
|
||||
CreateError::SqlxDatabaseError(..) => "database_error",
|
||||
CreateError::DatabaseError(..) => "database_error",
|
||||
CreateError::IndexingError(..) => "indexing_error",
|
||||
CreateError::FileHostingError(..) => "file_hosting_error",
|
||||
CreateError::SerDeError(..) => "invalid_input",
|
||||
CreateError::MultipartError(..) => "invalid_input",
|
||||
CreateError::MissingValueError(..) => "invalid_input",
|
||||
CreateError::InvalidIconFormat(..) => "invalid_input",
|
||||
CreateError::InvalidInput(..) => "invalid_input",
|
||||
CreateError::InvalidGameVersion(..) => "invalid_input",
|
||||
CreateError::InvalidLoader(..) => "invalid_input",
|
||||
CreateError::InvalidCategory(..) => "invalid_input",
|
||||
CreateError::InvalidFileType(..) => "invalid_input",
|
||||
CreateError::Unauthorized(..) => "unauthorized",
|
||||
CreateError::CustomAuthenticationError(..) => "unauthorized",
|
||||
CreateError::SlugCollision => "invalid_input",
|
||||
CreateError::ValidationError(..) => "invalid_input",
|
||||
CreateError::FileValidationError(..) => "invalid_input",
|
||||
CreateError::ImageError(..) => "invalid_image",
|
||||
CreateError::LimitReached => "limit_reached",
|
||||
},
|
||||
description: self.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_project_type() -> String {
|
||||
"mod".to_string()
|
||||
}
|
||||
@@ -359,34 +249,29 @@ async fn project_create_inner(
|
||||
|
||||
let project_id: ProjectId =
|
||||
models::generate_project_id(transaction).await?.into();
|
||||
let all_loaders =
|
||||
models::loader_fields::Loader::list(&mut **transaction, redis).await?;
|
||||
let all_loaders = Loader::list(&mut **transaction, redis).await?;
|
||||
|
||||
let project_create_data: ProjectCreateData;
|
||||
let mut versions;
|
||||
let mut versions_map = std::collections::HashMap::new();
|
||||
let mut versions_map = HashMap::new();
|
||||
let mut gallery_urls = Vec::new();
|
||||
{
|
||||
// The first multipart field must be named "data" and contain a
|
||||
// JSON `ProjectCreateData` object.
|
||||
|
||||
let mut field = payload.next().await.map_or_else(
|
||||
|| {
|
||||
Err(CreateError::MissingValueError(String::from(
|
||||
"No `data` field in multipart upload",
|
||||
)))
|
||||
},
|
||||
|| Err(CreateError::MissingValueError(MissingValuePart::DataField)),
|
||||
|m| m.map_err(CreateError::MultipartError),
|
||||
)?;
|
||||
|
||||
let name = field.name().ok_or_else(|| {
|
||||
CreateError::MissingValueError(String::from("Missing content name"))
|
||||
CreateError::MissingValueError(MissingValuePart::ContentName)
|
||||
})?;
|
||||
|
||||
if name != "data" {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"`data` field must come before file fields",
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::DataFieldOutOfOrder,
|
||||
));
|
||||
}
|
||||
|
||||
let mut data = Vec::new();
|
||||
@@ -398,7 +283,9 @@ async fn project_create_inner(
|
||||
let create_data: ProjectCreateData = serde_json::from_slice(&data)?;
|
||||
|
||||
create_data.validate().map_err(|err| {
|
||||
CreateError::InvalidInput(validation_errors_to_string(err, None))
|
||||
CreateError::InvalidInput(CreationInvalidInput::Validation(
|
||||
validation_errors_to_string(err, None),
|
||||
))
|
||||
})?;
|
||||
|
||||
let slug_project_id_option: Option<ProjectId> =
|
||||
@@ -445,9 +332,9 @@ async fn project_create_inner(
|
||||
for name in &data.file_parts {
|
||||
if versions_map.insert(name.to_owned(), i).is_some() {
|
||||
// If the name is already used
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Duplicate multipart field name",
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::DuplicateMultipartField,
|
||||
));
|
||||
}
|
||||
}
|
||||
versions.push(
|
||||
@@ -477,10 +364,11 @@ async fn project_create_inner(
|
||||
}
|
||||
|
||||
let result = async {
|
||||
let content_disposition = field.content_disposition().unwrap().clone();
|
||||
let content_disposition =
|
||||
field.content_disposition().unwrap().clone();
|
||||
|
||||
let name = content_disposition.get_name().ok_or_else(|| {
|
||||
CreateError::MissingValueError("Missing content name".to_string())
|
||||
CreateError::MissingValueError(MissingValuePart::ContentName)
|
||||
})?;
|
||||
|
||||
let (file_name, file_extension) =
|
||||
@@ -488,9 +376,9 @@ async fn project_create_inner(
|
||||
|
||||
if name == "icon" {
|
||||
if icon_data.is_some() {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Projects can only have one icon",
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::MultipleIcons,
|
||||
));
|
||||
}
|
||||
// Upload the icon to the cdn
|
||||
icon_data = Some(
|
||||
@@ -507,20 +395,24 @@ async fn project_create_inner(
|
||||
}
|
||||
if let Some(gallery_items) = &project_create_data.gallery_items {
|
||||
if gallery_items.iter().filter(|a| a.featured).count() > 1 {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Only one gallery image can be featured.",
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::MultipleFeaturedGallery,
|
||||
));
|
||||
}
|
||||
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
|
||||
if let Some(item) =
|
||||
gallery_items.iter().find(|x| x.item == name)
|
||||
{
|
||||
let data = read_from_field(
|
||||
&mut field,
|
||||
5 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 5MiB.",
|
||||
CreationInvalidInput::GalleryImageTooLarge("5 MiB"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (_, file_extension) =
|
||||
super::version_creation::get_name_ext(&content_disposition)?;
|
||||
super::version_creation::get_name_ext(
|
||||
&content_disposition,
|
||||
)?;
|
||||
|
||||
let url = format!("data/{project_id}/images");
|
||||
let upload_result = upload_image_optimized(
|
||||
@@ -533,7 +425,7 @@ async fn project_create_inner(
|
||||
file_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
|
||||
.map_err(CreateError::InvalidIconFormat)?;
|
||||
|
||||
uploaded_files.push(UploadedFile {
|
||||
name: upload_result.raw_url_path,
|
||||
@@ -555,9 +447,15 @@ async fn project_create_inner(
|
||||
let index = if let Some(i) = versions_map.get(name) {
|
||||
*i
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"File `{file_name}` (field {name}) isn't specified in the versions data"
|
||||
)));
|
||||
// return Err(CreateError::InvalidInput(format!(
|
||||
// "File `{file_name}` (field {name}) isn't specified in the versions data"
|
||||
// )));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::FileNotSpecified(
|
||||
file_name.to_string(),
|
||||
name.to_string(),
|
||||
),
|
||||
));
|
||||
};
|
||||
// `index` is always valid for these lists
|
||||
let created_version = &mut versions[index];
|
||||
@@ -613,9 +511,9 @@ async fn project_create_inner(
|
||||
.zip(versions.iter())
|
||||
{
|
||||
if version_data.file_parts.len() != builder.files.len() {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Some files were specified in initial_versions but not uploaded",
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::InitialVersionsFilesMissing,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,7 +562,7 @@ async fn project_create_inner(
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"Invalid organization ID specified!".to_string(),
|
||||
CreationInvalidInput::InvalidOrganizationId,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -683,9 +581,8 @@ async fn project_create_inner(
|
||||
if !perms.is_some_and(|x| {
|
||||
x.contains(OrganizationPermissions::ADD_PROJECT)
|
||||
}) {
|
||||
return Err(CreateError::CustomAuthenticationError(
|
||||
"You do not have the permissions to create projects in this organization!"
|
||||
.to_string(),
|
||||
return Err(CreateError::CreationAuthenticationError(
|
||||
CreationAuthenticationError::CreateProjectsInOrganization,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -710,20 +607,15 @@ async fn project_create_inner(
|
||||
} else {
|
||||
status = ProjectStatus::Processing;
|
||||
if project_create_data.initial_versions.is_empty() {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Project submitted for review with no initial versions",
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::NoInitialVersions,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let license_id = spdx::Expression::parse(
|
||||
&project_create_data.license_id,
|
||||
)
|
||||
.map_err(|err| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Invalid SPDX license identifier: {err}"
|
||||
))
|
||||
})?;
|
||||
let license_id =
|
||||
spdx::Expression::parse(&project_create_data.license_id)
|
||||
.map_err(|err| CreateError::InvalidInput(err.into()))?;
|
||||
|
||||
let mut link_urls = vec![];
|
||||
|
||||
@@ -737,19 +629,21 @@ async fn project_create_inner(
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Link platform {} does not exist.",
|
||||
platform.clone()
|
||||
))
|
||||
CreateError::InvalidInput(
|
||||
CreationInvalidInput::NonexistentLinkPlatform(
|
||||
platform.clone(),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let link_platform = link_platforms
|
||||
.iter()
|
||||
.find(|x| x.id == platform_id)
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Link platform {} does not exist.",
|
||||
platform.clone()
|
||||
))
|
||||
CreateError::InvalidInput(
|
||||
CreationInvalidInput::NonexistentLinkPlatform(
|
||||
platform.clone(),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
link_urls.push(models::project_item::LinkUrl {
|
||||
platform_id,
|
||||
@@ -814,9 +708,11 @@ async fn project_create_inner(
|
||||
if !matches!(image.context, ImageContext::Project { .. })
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {image_id} is not unused and in the 'project' context"
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::ImproperContextImage(
|
||||
image_id, "project",
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -834,9 +730,9 @@ async fn project_create_inner(
|
||||
image_item::DBImage::clear_cache(image.id.into(), redis)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::NonexistentImage(image_id),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -926,14 +822,14 @@ async fn create_initial_version(
|
||||
version_data: &InitialVersionData,
|
||||
project_id: ProjectId,
|
||||
author: UserId,
|
||||
all_loaders: &[models::loader_fields::Loader],
|
||||
all_loaders: &[Loader],
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
redis: &RedisPool,
|
||||
) -> Result<models::version_item::VersionBuilder, CreateError> {
|
||||
if version_data.project_id.is_some() {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Found project id in initial version for new project",
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::ProjectIdInInitialVersion,
|
||||
));
|
||||
}
|
||||
|
||||
version_data.validate().map_err(|err| {
|
||||
@@ -1015,10 +911,10 @@ async fn process_icon_upload(
|
||||
let data = read_from_field(
|
||||
&mut field,
|
||||
262144,
|
||||
"Icons must be smaller than 256KiB",
|
||||
CreationInvalidInput::IconTooLarge("256 KiB"),
|
||||
)
|
||||
.await?;
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", to_base62(id)),
|
||||
FileHostPublicity::Public,
|
||||
data.freeze(),
|
||||
@@ -1028,7 +924,7 @@ async fn process_icon_upload(
|
||||
file_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
|
||||
.map_err(CreateError::InvalidIconFormat)?;
|
||||
|
||||
uploaded_files.push(UploadedFile {
|
||||
name: upload_result.raw_url_path,
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::models::teams::ProjectPermissions;
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::search::indexing::remove_documents;
|
||||
use crate::search::{SearchConfig, SearchError, search_for_project};
|
||||
use crate::util::img;
|
||||
@@ -107,7 +107,7 @@ pub async fn random_projects_get(
|
||||
LIMIT $2",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
count.count as i32,
|
||||
)
|
||||
@@ -289,7 +289,7 @@ pub async fn project_edit(
|
||||
let id = project_item.inner.id;
|
||||
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
DBTeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -301,8 +301,8 @@ pub async fn project_edit(
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
) else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to edit this project!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProject,
|
||||
));
|
||||
};
|
||||
|
||||
@@ -310,9 +310,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(name) = &new_project.name {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the name of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectName,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -331,9 +330,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(summary) = &new_project.summary {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the summary of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectSummary,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -352,9 +350,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(status) = &new_project.status {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the status of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectStatus,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -364,8 +361,8 @@ pub async fn project_edit(
|
||||
|| project_item.inner.status.is_approved()
|
||||
&& status.can_be_requested())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to set this status!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::RestrictedProjectStatus,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -445,10 +442,9 @@ pub async fn project_edit(
|
||||
dotenvy::var("SITE_URL")?,
|
||||
user.username,
|
||||
user.username,
|
||||
&project_item.inner.status.as_friendly_str(),
|
||||
status.as_friendly_str(),
|
||||
)
|
||||
.to_string(),
|
||||
&project_item.inner.status,
|
||||
status,
|
||||
),
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -523,9 +519,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(requested_status) = &new_project.requested_status {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the requested status of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectRequestedStatus,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -581,7 +576,7 @@ pub async fn project_edit(
|
||||
edit_project_categories(
|
||||
categories,
|
||||
&perms,
|
||||
id as db_ids::DBProjectId,
|
||||
id,
|
||||
false,
|
||||
&mut transaction,
|
||||
)
|
||||
@@ -589,21 +584,14 @@ pub async fn project_edit(
|
||||
}
|
||||
|
||||
if let Some(categories) = &new_project.additional_categories {
|
||||
edit_project_categories(
|
||||
categories,
|
||||
&perms,
|
||||
id as db_ids::DBProjectId,
|
||||
true,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
edit_project_categories(categories, &perms, id, true, &mut transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(license_url) = &new_project.license_url {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the license URL of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectLicenseUrl,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -622,9 +610,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(slug) = &new_project.slug {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the slug of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectSlug,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -680,9 +667,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(license) = &new_project.license_id {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the license of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectLicense,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -715,10 +701,9 @@ pub async fn project_edit(
|
||||
&& !links.is_empty()
|
||||
{
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the links of this project!"
|
||||
.to_string(),
|
||||
));
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectLinks,
|
||||
));
|
||||
}
|
||||
|
||||
let ids_to_delete = links.keys().cloned().collect::<Vec<String>>();
|
||||
@@ -768,9 +753,8 @@ pub async fn project_edit(
|
||||
&& (!project_item.inner.status.is_approved()
|
||||
|| moderation_message.is_some())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the moderation message of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectModerationMessage,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -793,9 +777,8 @@ pub async fn project_edit(
|
||||
&& (!project_item.inner.status.is_approved()
|
||||
|| moderation_message_body.is_some())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the moderation message body of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectModerationMessageBody,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -814,9 +797,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(description) = &new_project.description {
|
||||
if !perms.contains(ProjectPermissions::EDIT_BODY) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the description (body) of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectDescription,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -835,9 +817,8 @@ pub async fn project_edit(
|
||||
|
||||
if let Some(monetization_status) = &new_project.monetization_status {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the monetization status of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectMonetizationStatus,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -846,9 +827,8 @@ pub async fn project_edit(
|
||||
== MonetizationStatus::ForceDemonetized)
|
||||
&& !user.role.is_mod()
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the monetization status of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectMonetizationStatus,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -869,9 +849,8 @@ pub async fn project_edit(
|
||||
&new_project.side_types_migration_review_status
|
||||
{
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the side types migration review status of this project!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectSideTypesMigrationReviewStatus,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -977,10 +956,11 @@ pub async fn edit_project_categories(
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), ApiError> {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
let additional_str = if is_additional { "additional " } else { "" };
|
||||
return Err(ApiError::CustomAuthentication(format!(
|
||||
"You do not have the permissions to edit the {additional_str}categories of this project!"
|
||||
)));
|
||||
return Err(ApiError::SpecificAuthentication(if is_additional {
|
||||
SpecificAuthenticationError::EditProjectAdditionalCategories
|
||||
} else {
|
||||
SpecificAuthenticationError::EditProjectCategories
|
||||
}));
|
||||
}
|
||||
|
||||
let mut mod_categories = Vec::new();
|
||||
@@ -1225,10 +1205,9 @@ pub async fn projects_edit(
|
||||
.iter()
|
||||
.map(|x| x.inner.team_id)
|
||||
.collect::<Vec<db_models::DBTeamId>>();
|
||||
let team_members = db_models::DBTeamMember::get_from_team_full_many(
|
||||
&team_ids, &**pool, &redis,
|
||||
)
|
||||
.await?;
|
||||
let team_members =
|
||||
DBTeamMember::get_from_team_full_many(&team_ids, &**pool, &redis)
|
||||
.await?;
|
||||
|
||||
let organization_ids = projects_data
|
||||
.iter()
|
||||
@@ -1245,13 +1224,12 @@ pub async fn projects_edit(
|
||||
.iter()
|
||||
.map(|x| x.team_id)
|
||||
.collect::<Vec<db_models::DBTeamId>>();
|
||||
let organization_team_members =
|
||||
db_models::DBTeamMember::get_from_team_full_many(
|
||||
&organization_team_ids,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
let organization_team_members = DBTeamMember::get_from_team_full_many(
|
||||
&organization_team_ids,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let categories =
|
||||
db_models::categories::Category::list(&**pool, &redis).await?;
|
||||
@@ -1291,10 +1269,11 @@ pub async fn projects_edit(
|
||||
|
||||
if team_member.is_some() {
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(format!(
|
||||
"You do not have the permissions to bulk edit project {}!",
|
||||
project.inner.name
|
||||
)));
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::BulkEditProject(
|
||||
project.inner.name,
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if project.inner.status.is_hidden() {
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
@@ -1302,17 +1281,18 @@ pub async fn projects_edit(
|
||||
ProjectId(project.inner.id.0 as u64)
|
||||
)));
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(format!(
|
||||
"You are not a member of project {}!",
|
||||
project.inner.name
|
||||
)));
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::NotMemberOfProject(
|
||||
project.inner.name,
|
||||
),
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
bulk_edit_project_categories(
|
||||
&categories,
|
||||
&project.categories,
|
||||
project.inner.id as db_ids::DBProjectId,
|
||||
project.inner.id,
|
||||
CategoryChanges {
|
||||
categories: &bulk_edit_project.categories,
|
||||
add_categories: &bulk_edit_project.add_categories,
|
||||
@@ -1327,7 +1307,7 @@ pub async fn projects_edit(
|
||||
bulk_edit_project_categories(
|
||||
&categories,
|
||||
&project.additional_categories,
|
||||
project.inner.id as db_ids::DBProjectId,
|
||||
project.inner.id,
|
||||
CategoryChanges {
|
||||
categories: &bulk_edit_project.additional_categories,
|
||||
add_categories: &bulk_edit_project.add_additional_categories,
|
||||
@@ -1498,6 +1478,8 @@ pub async fn project_icon_edit(
|
||||
let project_item = db_models::DBProject::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
// FIXME: Change this to a 404 but also make sure it doesn't break existing clients
|
||||
// (also applies to several other routes)
|
||||
ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
)
|
||||
@@ -1505,7 +1487,7 @@ pub async fn project_icon_edit(
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
DBTeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -1514,7 +1496,7 @@ pub async fn project_icon_edit(
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -1527,9 +1509,8 @@ pub async fn project_icon_edit(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectIcon,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1618,7 +1599,7 @@ pub async fn delete_project_icon(
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
DBTeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -1627,7 +1608,7 @@ pub async fn delete_project_icon(
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -1639,9 +1620,8 @@ pub async fn delete_project_icon(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectIcon,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1724,16 +1704,9 @@ pub async fn add_gallery_item(
|
||||
)
|
||||
})?;
|
||||
|
||||
if project_item.gallery_items.len() > 64 {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You have reached the maximum of gallery images to upload."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !user.role.is_admin() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
DBTeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -1742,11 +1715,17 @@ pub async fn add_gallery_item(
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if project_item.gallery_items.len() > 64 {
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::MaximumGalleryImages,
|
||||
));
|
||||
}
|
||||
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
@@ -1755,9 +1734,8 @@ pub async fn add_gallery_item(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectGallery,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1807,7 +1785,7 @@ pub async fn add_gallery_item(
|
||||
.await?;
|
||||
}
|
||||
|
||||
let gallery_item = vec![db_models::project_item::DBGalleryItem {
|
||||
let gallery_item = vec![DBGalleryItem {
|
||||
image_url: upload_result.url,
|
||||
raw_image_url: upload_result.raw_url,
|
||||
featured: item.featured,
|
||||
@@ -1908,7 +1886,7 @@ pub async fn edit_gallery_item(
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
DBTeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -1917,7 +1895,7 @@ pub async fn edit_gallery_item(
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -1929,9 +1907,8 @@ pub async fn edit_gallery_item(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectGallery,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2071,7 +2048,7 @@ pub async fn delete_gallery_item(
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
DBTeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -2080,7 +2057,7 @@ pub async fn delete_gallery_item(
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -2093,9 +2070,8 @@ pub async fn delete_gallery_item(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditProjectGallery,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2162,7 +2138,7 @@ pub async fn project_delete(
|
||||
|
||||
if !user.role.is_admin() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::DBTeamMember::get_for_project_permissions(
|
||||
DBTeamMember::get_for_project_permissions(
|
||||
&project.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
@@ -2171,7 +2147,7 @@ pub async fn project_delete(
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -2184,8 +2160,8 @@ pub async fn project_delete(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::DELETE_PROJECT) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to delete this project!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteProject,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2442,7 +2418,7 @@ pub async fn project_get_organization(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let users = crate::database::models::DBUser::get_many_ids(
|
||||
let users = database::models::DBUser::get_many_ids(
|
||||
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -2461,13 +2437,13 @@ pub async fn project_get_organization(
|
||||
.filter(|x| {
|
||||
logged_in
|
||||
|| x.accepted
|
||||
|| user_id.is_some_and(
|
||||
|y: crate::database::models::DBUserId| y == x.user_id,
|
||||
)
|
||||
|| user_id.is_some_and(|y: database::models::DBUserId| {
|
||||
y == x.user_id
|
||||
})
|
||||
})
|
||||
.filter_map(|data| {
|
||||
users.iter().find(|x| x.id == data.user_id).map(|user| {
|
||||
crate::models::teams::TeamMember::from(
|
||||
models::teams::TeamMember::from(
|
||||
data,
|
||||
user.clone(),
|
||||
!logged_in,
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::reports::{ItemType, Report};
|
||||
use crate::models::threads::{MessageBody, ThreadType};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use crate::util::img;
|
||||
use crate::util::routes::read_typed_from_payload;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::models::shared_instances::{
|
||||
SharedInstanceUserPermissions, SharedInstanceVersion,
|
||||
};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::routes::v3::project_creation::UploadedFile;
|
||||
use crate::util::ext::MRPACK_MIME_TYPE;
|
||||
use actix_web::http::header::ContentLength;
|
||||
@@ -127,8 +127,8 @@ async fn shared_instance_version_create_inner(
|
||||
if !permissions
|
||||
.contains(SharedInstanceUserPermissions::UPLOAD_VERSION)
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to upload a version for this shared instance.".to_string()
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::SharedInstanceUploadVersion,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::models::shared_instances::{
|
||||
};
|
||||
use crate::models::users::User;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::util::routes::read_typed_from_payload;
|
||||
use actix_web::web::{Data, Redirect};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
@@ -257,9 +257,8 @@ pub async fn shared_instance_edit(
|
||||
.await?;
|
||||
if let Some(permissions) = permissions {
|
||||
if !permissions.contains(SharedInstanceUserPermissions::EDIT) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to edit this shared instance."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditSharedInstance,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -332,8 +331,8 @@ pub async fn shared_instance_delete(
|
||||
.await?;
|
||||
if let Some(permissions) = permissions {
|
||||
if !permissions.contains(SharedInstanceUserPermissions::DELETE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to delete this shared instance.".to_string()
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteSharedInstance,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -503,8 +502,8 @@ pub async fn shared_instance_version_delete(
|
||||
if !permissions
|
||||
.contains(SharedInstanceUserPermissions::DELETE)
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to delete this shared instance version.".to_string()
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteSharedInstanceVersion
|
||||
));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::ApiError;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use sqlx::PgPool;
|
||||
|
||||
@@ -25,7 +25,7 @@ pub async fn get_stats(
|
||||
",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
@@ -40,7 +40,7 @@ pub async fn get_stats(
|
||||
",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
&*crate::models::projects::VersionStatus::iterator()
|
||||
.filter(|x| x.is_listed())
|
||||
@@ -59,7 +59,7 @@ pub async fn get_stats(
|
||||
",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
@@ -73,7 +73,7 @@ pub async fn get_stats(
|
||||
",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
&*crate::models::projects::VersionStatus::iterator()
|
||||
.filter(|x| x.is_listed())
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ApiError;
|
||||
use crate::database::models::categories::{
|
||||
Category, LinkPlatform, ProjectType, ReportType,
|
||||
};
|
||||
@@ -8,6 +7,7 @@ use crate::database::models::loader_fields::{
|
||||
Game, Loader, LoaderField, LoaderFieldEnumValue, LoaderFieldType,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::routes::error::ApiError;
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use ariadne::ids::UserId;
|
||||
use rust_decimal::Decimal;
|
||||
@@ -47,9 +47,7 @@ pub async fn team_members_get_project(
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
let project_data =
|
||||
crate::database::models::DBProject::get(&string, &**pool, &redis)
|
||||
.await?;
|
||||
let project_data = DBProject::get(&string, &**pool, &redis).await?;
|
||||
|
||||
if let Some(project) = project_data {
|
||||
let current_user = get_user_from_headers(
|
||||
@@ -131,8 +129,7 @@ pub async fn team_members_get_organization(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
let organization_data =
|
||||
crate::database::models::DBOrganization::get(&string, &**pool, &redis)
|
||||
.await?;
|
||||
DBOrganization::get(&string, &**pool, &redis).await?;
|
||||
|
||||
let current_user = get_user_from_headers(
|
||||
&req,
|
||||
@@ -155,7 +152,7 @@ pub async fn team_members_get_organization(
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
let users = crate::database::models::DBUser::get_many_ids(
|
||||
let users = DBUser::get_many_ids(
|
||||
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -209,7 +206,7 @@ pub async fn team_members_get(
|
||||
let id = info.into_inner().0;
|
||||
let members_data =
|
||||
DBTeamMember::get_from_team_full(id.into(), &**pool, &redis).await?;
|
||||
let users = crate::database::models::DBUser::get_many_ids(
|
||||
let users = DBUser::get_many_ids(
|
||||
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -281,7 +278,7 @@ pub async fn teams_get(
|
||||
let teams_data =
|
||||
DBTeamMember::get_from_team_full_many(&team_ids, &**pool, &redis)
|
||||
.await?;
|
||||
let users = crate::database::models::DBUser::get_many_ids(
|
||||
let users = DBUser::get_many_ids(
|
||||
&teams_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -480,9 +477,8 @@ pub async fn add_team_member(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::MANAGE_INVITES) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to invite users to this team"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::InviteUsersToTeam,
|
||||
));
|
||||
}
|
||||
if !permissions.contains(new_member.permissions) {
|
||||
@@ -510,8 +506,8 @@ pub async fn add_team_member(
|
||||
if !organization_permissions
|
||||
.contains(OrganizationPermissions::MANAGE_INVITES)
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to invite users to this organization".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::InviteUsersToOrg,
|
||||
));
|
||||
}
|
||||
if !organization_permissions.contains(
|
||||
@@ -525,9 +521,8 @@ pub async fn add_team_member(
|
||||
OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS,
|
||||
) && !new_member.permissions.is_empty()
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to give this user default project permissions. Ensure 'permissions' is set if it is not, and empty (0)."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::GiveUserDefaultProjectPermissions,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -560,15 +555,11 @@ pub async fn add_team_member(
|
||||
));
|
||||
}
|
||||
}
|
||||
let new_user = crate::database::models::DBUser::get_id(
|
||||
new_member.user_id.into(),
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("An invalid User ID specified".to_string())
|
||||
})?;
|
||||
let new_user = DBUser::get_id(new_member.user_id.into(), &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("An invalid User ID specified".to_string())
|
||||
})?;
|
||||
|
||||
let mut force_accepted = false;
|
||||
if let TeamAssociationId::Project(pid) = team_association {
|
||||
@@ -708,9 +699,8 @@ pub async fn edit_team_member(
|
||||
DBTeamMember::get_from_user_id_pending(id, user_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditTeamMembers,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -751,9 +741,8 @@ pub async fn edit_team_member(
|
||||
.permissions
|
||||
.is_some_and(|x| x != ProjectPermissions::all())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You cannot override the project permissions of the organization owner!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::OverrideOrgOwnerDefaultProjectPermissions,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -764,9 +753,8 @@ pub async fn edit_team_member(
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if !permissions.contains(ProjectPermissions::EDIT_MEMBER) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditTeamMembers,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -797,9 +785,8 @@ pub async fn edit_team_member(
|
||||
if !organization_permissions
|
||||
.contains(OrganizationPermissions::EDIT_MEMBER)
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditTeamMembers,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -817,9 +804,8 @@ pub async fn edit_team_member(
|
||||
OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS,
|
||||
)
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to give this user default project permissions."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::GiveUserDefaultProjectPermissions,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -904,16 +890,14 @@ pub async fn transfer_ownership(
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit members of this team"
|
||||
.to_string(),
|
||||
ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditTeamMembers,
|
||||
)
|
||||
})?;
|
||||
|
||||
if !member.is_owner {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit the ownership of this team"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditTeamOwnership,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1081,8 +1065,8 @@ pub async fn remove_team_member(
|
||||
if let Some(delete_member) = delete_member {
|
||||
if delete_member.is_owner {
|
||||
// The owner cannot be removed from a team
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The owner can't be removed from a team".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::RemoveOwnerFromTeam,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1126,9 +1110,8 @@ pub async fn remove_team_member(
|
||||
DBTeamMember::delete(id, user_id, &mut transaction)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to remove a member from this team"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::RemoveTeamMember,
|
||||
));
|
||||
}
|
||||
} else if Some(delete_member.user_id)
|
||||
@@ -1141,9 +1124,8 @@ pub async fn remove_team_member(
|
||||
// permission can remove it.
|
||||
DBTeamMember::delete(id, user_id, &mut transaction).await?;
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to cancel a team invite"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::CancelTeamInvite,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1165,9 +1147,8 @@ pub async fn remove_team_member(
|
||||
DBTeamMember::delete(id, user_id, &mut transaction)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to remove a member from this organization"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::RemoveOrgMember,
|
||||
));
|
||||
}
|
||||
} else if Some(delete_member.user_id)
|
||||
@@ -1180,8 +1161,8 @@ pub async fn remove_team_member(
|
||||
// permission can remove it.
|
||||
DBTeamMember::delete(id, user_id, &mut transaction).await?;
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to cancel an organization invite".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::CancelOrgInvite,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::models::projects::ProjectStatus;
|
||||
use crate::models::threads::{MessageBody, Thread, ThreadType};
|
||||
use crate::models::users::User;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use futures::TryStreamExt;
|
||||
use serde::Deserialize;
|
||||
@@ -602,8 +602,8 @@ pub async fn message_delete(
|
||||
|
||||
if let Some(thread) = result {
|
||||
if !user.role.is_mod() && thread.author_id != Some(user.id.into()) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You cannot delete this message!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteMessage,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use super::{ApiError, oauth_clients::get_user_clients};
|
||||
use super::oauth_clients::get_user_clients;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::{
|
||||
auth::{
|
||||
checks::is_visible_organization, filter_visible_collections,
|
||||
@@ -70,9 +71,8 @@ pub async fn admin_user_email(
|
||||
.map(|x| x.1)?;
|
||||
|
||||
if !user.role.is_admin() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to get a user from their email!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::GetUserFromEmail,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -447,9 +447,8 @@ pub async fn user_edit(
|
||||
|
||||
if let Some(role) = &new_user.role {
|
||||
if !user.role.is_admin() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the role of this user!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditUserRole,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -470,9 +469,8 @@ pub async fn user_edit(
|
||||
|
||||
if let Some(badges) = &new_user.badges {
|
||||
if !user.role.is_admin() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the badges of this user!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditUserBadges,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -491,9 +489,8 @@ pub async fn user_edit(
|
||||
|
||||
if let Some(venmo_handle) = &new_user.venmo_handle {
|
||||
if !scopes.contains(Scopes::PAYOUTS_WRITE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the venmo handle of this user!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditUserVenmoHandle,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -529,8 +526,8 @@ pub async fn user_edit(
|
||||
.await?;
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to edit this user!".to_string(),
|
||||
Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditUser,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
@@ -567,9 +564,8 @@ pub async fn user_icon_edit(
|
||||
|
||||
if let Some(actual_user) = id_option {
|
||||
if user.id != actual_user.id.into() && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this user's icon."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditUserIcon,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -641,9 +637,8 @@ pub async fn user_icon_delete(
|
||||
|
||||
if let Some(actual_user) = id_option {
|
||||
if user.id != actual_user.id.into() && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this user's icon."
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditUserIcon,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -694,8 +689,8 @@ pub async fn user_delete(
|
||||
|
||||
if let Some(id) = id_option.map(|x| x.id) {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to delete this user!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteUser,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -735,8 +730,8 @@ pub async fn user_follows(
|
||||
|
||||
if let Some(id) = id_option.map(|x| x.id) {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to see the projects this user follows!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::SeeUserFollows,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -777,8 +772,8 @@ pub async fn user_notifications(
|
||||
|
||||
if let Some(id) = id_option.map(|x| x.id) {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to see the notifications of this user!".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::SeeUserNotifications,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::project_creation::{CreateError, UploadedFile};
|
||||
use super::project_creation::UploadedFile;
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::loader_fields::{
|
||||
LoaderField, LoaderFieldEnumValue, VersionField,
|
||||
@@ -23,6 +23,10 @@ use crate::models::projects::{DependencyType, ProjectStatus, skip_nulls};
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::create_error::{
|
||||
CreateError, CreationAuthenticationError, CreationInvalidInput,
|
||||
MissingValuePart,
|
||||
};
|
||||
use crate::util::routes::read_from_field;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::validate::{ValidationResult, validate_file};
|
||||
@@ -109,7 +113,7 @@ pub async fn version_create(
|
||||
redis: Data<RedisPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
moderation_queue: web::Data<AutomatedModerationQueue>,
|
||||
moderation_queue: Data<AutomatedModerationQueue>,
|
||||
) -> Result<HttpResponse, CreateError> {
|
||||
let mut transaction = client.begin().await?;
|
||||
let mut uploaded_files = Vec::new();
|
||||
@@ -183,9 +187,10 @@ async fn version_create_inner(
|
||||
}
|
||||
|
||||
let result = async {
|
||||
let content_disposition = field.content_disposition().unwrap().clone();
|
||||
let content_disposition =
|
||||
field.content_disposition().unwrap().clone();
|
||||
let name = content_disposition.get_name().ok_or_else(|| {
|
||||
CreateError::MissingValueError("Missing content name".to_string())
|
||||
CreateError::MissingValueError(MissingValuePart::ContentName)
|
||||
})?;
|
||||
|
||||
if name == "data" {
|
||||
@@ -194,64 +199,78 @@ async fn version_create_inner(
|
||||
data.extend_from_slice(&chunk?);
|
||||
}
|
||||
|
||||
let version_create_data: InitialVersionData = serde_json::from_slice(&data)?;
|
||||
let version_create_data: InitialVersionData =
|
||||
serde_json::from_slice(&data)?;
|
||||
initial_version_data = Some(version_create_data);
|
||||
let version_create_data = initial_version_data.as_ref().unwrap();
|
||||
let version_create_data =
|
||||
initial_version_data.as_ref().unwrap();
|
||||
if version_create_data.project_id.is_none() {
|
||||
return Err(CreateError::MissingValueError(
|
||||
"Missing project id".to_string(),
|
||||
MissingValuePart::ProjectId,
|
||||
));
|
||||
}
|
||||
|
||||
version_create_data.validate().map_err(|err| {
|
||||
CreateError::ValidationError(validation_errors_to_string(err, None))
|
||||
CreateError::ValidationError(validation_errors_to_string(
|
||||
err, None,
|
||||
))
|
||||
})?;
|
||||
|
||||
if !version_create_data.status.can_be_requested() {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Status specified cannot be requested".to_string(),
|
||||
CreationInvalidInput::CannotRequestStatus(
|
||||
version_create_data.status,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let project_id: models::DBProjectId = version_create_data.project_id.unwrap().into();
|
||||
let project_id: models::DBProjectId =
|
||||
version_create_data.project_id.unwrap().into();
|
||||
|
||||
// Ensure that the project this version is being added to exists
|
||||
if models::DBProject::get_id(project_id, &mut **transaction, redis)
|
||||
.await?
|
||||
.is_none()
|
||||
if models::DBProject::get_id(
|
||||
project_id,
|
||||
&mut **transaction,
|
||||
redis,
|
||||
)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(
|
||||
"An invalid project id was supplied".to_string(),
|
||||
CreationInvalidInput::InvalidProjectId,
|
||||
));
|
||||
}
|
||||
|
||||
// Check that the user creating this version is a team member
|
||||
// of the project the version is being added to.
|
||||
let team_member = models::DBTeamMember::get_from_user_id_project(
|
||||
project_id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&mut **transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Get organization attached, if exists, and the member project permissions
|
||||
let organization = models::DBOrganization::get_associated_organization_project_id(
|
||||
project_id,
|
||||
&mut **transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let organization_team_member = if let Some(organization) = &organization {
|
||||
models::DBTeamMember::get_from_user_id(
|
||||
organization.team_id,
|
||||
let team_member =
|
||||
models::DBTeamMember::get_from_user_id_project(
|
||||
project_id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&mut **transaction,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
.await?;
|
||||
|
||||
// Get organization attached, if exists, and the member project permissions
|
||||
let organization =
|
||||
DBOrganization::get_associated_organization_project_id(
|
||||
project_id,
|
||||
&mut **transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let organization_team_member =
|
||||
if let Some(organization) = &organization {
|
||||
models::DBTeamMember::get_from_user_id(
|
||||
organization.team_id,
|
||||
user.id.into(),
|
||||
&mut **transaction,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
@@ -261,15 +280,19 @@ async fn version_create_inner(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::UPLOAD_VERSION) {
|
||||
return Err(CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload this version!".to_string(),
|
||||
return Err(CreateError::CreationAuthenticationError(
|
||||
CreationAuthenticationError::UploadVersion,
|
||||
));
|
||||
}
|
||||
|
||||
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
|
||||
let version_id: VersionId =
|
||||
models::generate_version_id(transaction).await?.into();
|
||||
|
||||
let all_loaders =
|
||||
models::loader_fields::Loader::list(&mut **transaction, redis).await?;
|
||||
let all_loaders = models::loader_fields::Loader::list(
|
||||
&mut **transaction,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
let loaders = version_create_data
|
||||
.loaders
|
||||
.iter()
|
||||
@@ -278,20 +301,28 @@ async fn version_create_inner(
|
||||
.iter()
|
||||
.find(|y| y.loader == x.0)
|
||||
.cloned()
|
||||
.ok_or_else(|| CreateError::InvalidLoader(x.0.clone()))
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidLoader(x.0.clone())
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
selected_loaders = Some(loaders.clone());
|
||||
let loader_ids: Vec<models::LoaderId> = loaders.iter().map(|y| y.id).collect_vec();
|
||||
let loader_ids: Vec<models::LoaderId> =
|
||||
loaders.iter().map(|y| y.id).collect_vec();
|
||||
|
||||
let loader_fields =
|
||||
LoaderField::get_fields(&loader_ids, &mut **transaction, redis).await?;
|
||||
let mut loader_field_enum_values = LoaderFieldEnumValue::list_many_loader_fields(
|
||||
&loader_fields,
|
||||
let loader_fields = LoaderField::get_fields(
|
||||
&loader_ids,
|
||||
&mut **transaction,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
let mut loader_field_enum_values =
|
||||
LoaderFieldEnumValue::list_many_loader_fields(
|
||||
&loader_fields,
|
||||
&mut **transaction,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
let version_fields = try_create_version_fields(
|
||||
version_id,
|
||||
&version_create_data.fields,
|
||||
@@ -302,7 +333,7 @@ async fn version_create_inner(
|
||||
let dependencies = version_create_data
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|d| models::version_item::DependencyBuilder {
|
||||
.map(|d| DependencyBuilder {
|
||||
version_id: d.version_id.map(|x| x.into()),
|
||||
project_id: d.project_id.map(|x| x.into()),
|
||||
dependency_type: d.dependency_type.to_string(),
|
||||
@@ -316,12 +347,17 @@ async fn version_create_inner(
|
||||
author_id: user.id.into(),
|
||||
name: version_create_data.version_title.clone(),
|
||||
version_number: version_create_data.version_number.clone(),
|
||||
changelog: version_create_data.version_body.clone().unwrap_or_default(),
|
||||
changelog: version_create_data
|
||||
.version_body
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
files: Vec::new(),
|
||||
dependencies,
|
||||
loaders: loader_ids,
|
||||
version_fields,
|
||||
version_type: version_create_data.release_channel.to_string(),
|
||||
version_type: version_create_data
|
||||
.release_channel
|
||||
.to_string(),
|
||||
featured: version_create_data.featured,
|
||||
status: version_create_data.status,
|
||||
requested_status: None,
|
||||
@@ -332,21 +368,29 @@ async fn version_create_inner(
|
||||
}
|
||||
|
||||
let version = version_builder.as_mut().ok_or_else(|| {
|
||||
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
|
||||
CreateError::InvalidInput(
|
||||
CreationInvalidInput::DataFieldOutOfOrder,
|
||||
)
|
||||
})?;
|
||||
let loaders = selected_loaders.as_ref().ok_or_else(|| {
|
||||
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
|
||||
CreateError::InvalidInput(
|
||||
CreationInvalidInput::DataFieldOutOfOrder,
|
||||
)
|
||||
})?;
|
||||
let loaders = loaders
|
||||
.iter()
|
||||
.map(|x| Loader(x.loader.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let version_data = initial_version_data
|
||||
.clone()
|
||||
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
|
||||
let version_data =
|
||||
initial_version_data.clone().ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
CreationInvalidInput::MissingDataField,
|
||||
)
|
||||
})?;
|
||||
|
||||
let existing_file_names = version.files.iter().map(|x| x.filename.clone()).collect();
|
||||
let existing_file_names =
|
||||
version.files.iter().map(|x| x.filename.clone()).collect();
|
||||
|
||||
upload_file(
|
||||
&mut field,
|
||||
@@ -384,15 +428,15 @@ async fn version_create_inner(
|
||||
}
|
||||
|
||||
let version_data = initial_version_data.ok_or_else(|| {
|
||||
CreateError::InvalidInput("`data` field is required".to_string())
|
||||
CreateError::InvalidInput(CreationInvalidInput::MissingDataField)
|
||||
})?;
|
||||
let builder = version_builder.ok_or_else(|| {
|
||||
CreateError::InvalidInput("`data` field is required".to_string())
|
||||
CreateError::InvalidInput(CreationInvalidInput::MissingDataField)
|
||||
})?;
|
||||
|
||||
if builder.files.is_empty() {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Versions must have at least one file uploaded to them".to_string(),
|
||||
CreationInvalidInput::MissingAnyFiles,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -404,7 +448,7 @@ async fn version_create_inner(
|
||||
INNER JOIN users u ON u.id = follower_id
|
||||
WHERE mf.mod_id = $1
|
||||
",
|
||||
builder.project_id as crate::database::models::ids::DBProjectId
|
||||
builder.project_id as models::ids::DBProjectId
|
||||
)
|
||||
.fetch(&mut **transaction)
|
||||
.map_ok(|m| models::ids::DBUserId(m.follower_id))
|
||||
@@ -489,9 +533,11 @@ async fn version_create_inner(
|
||||
if !matches!(image.context, ImageContext::Report { .. })
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {image_id} is not unused and in the 'version' context"
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::ImproperContextImage(
|
||||
image_id, "report",
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -508,9 +554,9 @@ async fn version_create_inner(
|
||||
|
||||
image_item::DBImage::clear_cache(image.id.into(), redis).await?;
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::NonexistentImage(image_id),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,7 +585,7 @@ pub async fn upload_file_to_version(
|
||||
client: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, CreateError> {
|
||||
let mut transaction = client.begin().await?;
|
||||
let mut uploaded_files = Vec::new();
|
||||
@@ -609,7 +655,7 @@ async fn upload_file_to_version_inner(
|
||||
|
||||
let Some(version) = result else {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"An invalid version id was supplied".to_string(),
|
||||
CreationInvalidInput::InvalidVersionId,
|
||||
));
|
||||
};
|
||||
|
||||
@@ -636,7 +682,7 @@ async fn upload_file_to_version_inner(
|
||||
.is_none()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(
|
||||
"An invalid project id was supplied".to_string(),
|
||||
CreationInvalidInput::InvalidProjectId,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -676,9 +722,8 @@ async fn upload_file_to_version_inner(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::UPLOAD_VERSION) {
|
||||
return Err(CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload files to this version!"
|
||||
.to_string(),
|
||||
return Err(CreateError::CreationAuthenticationError(
|
||||
CreationAuthenticationError::UploadFilesToVersion,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -696,9 +741,7 @@ async fn upload_file_to_version_inner(
|
||||
let content_disposition =
|
||||
field.content_disposition().unwrap().clone();
|
||||
let name = content_disposition.get_name().ok_or_else(|| {
|
||||
CreateError::MissingValueError(
|
||||
"Missing content name".to_string(),
|
||||
)
|
||||
CreateError::MissingValueError(MissingValuePart::ContentName)
|
||||
})?;
|
||||
|
||||
if name == "data" {
|
||||
@@ -713,9 +756,9 @@ async fn upload_file_to_version_inner(
|
||||
}
|
||||
|
||||
let file_data = initial_file_data.as_ref().ok_or_else(|| {
|
||||
CreateError::InvalidInput(String::from(
|
||||
"`data` field must come before file fields",
|
||||
))
|
||||
CreateError::InvalidInput(
|
||||
CreationInvalidInput::DataFieldOutOfOrder,
|
||||
)
|
||||
})?;
|
||||
|
||||
let loaders = selected_loaders
|
||||
@@ -771,7 +814,7 @@ async fn upload_file_to_version_inner(
|
||||
|
||||
if file_builders.is_empty() {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"At least one file must be specified".to_string(),
|
||||
CreationInvalidInput::NoFilesSpecified,
|
||||
));
|
||||
} else {
|
||||
for file in file_builders {
|
||||
@@ -812,14 +855,13 @@ pub async fn upload_file(
|
||||
|
||||
if other_file_names.contains(&format!("{file_name}.{file_extension}")) {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!"
|
||||
.to_string(),
|
||||
CreationInvalidInput::DuplicateFiles,
|
||||
));
|
||||
}
|
||||
|
||||
if file_name.contains('/') {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"File names must not contain slashes!".to_string(),
|
||||
CreationInvalidInput::FileNameHasSlashes,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -829,9 +871,11 @@ pub async fn upload_file(
|
||||
})?;
|
||||
|
||||
let data = read_from_field(
|
||||
field, 500 * (1 << 20),
|
||||
"Project file exceeds the maximum of 500MiB. Contact a moderator or admin to request permission to upload larger files."
|
||||
).await?;
|
||||
field,
|
||||
500 * (1 << 20),
|
||||
CreationInvalidInput::ProjectFileTooLarge("500 MiB"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let hash = sha1::Sha1::digest(&data).encode_hex::<String>();
|
||||
let exists = sqlx::query!(
|
||||
@@ -852,8 +896,7 @@ pub async fn upload_file(
|
||||
|
||||
if exists {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!"
|
||||
.to_string(),
|
||||
CreationInvalidInput::DuplicateFiles,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -969,15 +1012,16 @@ pub async fn upload_file(
|
||||
.any(|y| y.hash == sha1_bytes || y.hash == sha512_bytes)
|
||||
}) {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!"
|
||||
.to_string(),
|
||||
CreationInvalidInput::DuplicateFiles,
|
||||
));
|
||||
}
|
||||
|
||||
if let ValidationResult::Warning(msg) = validation_result
|
||||
if let ValidationResult::Warning(warning) = validation_result
|
||||
&& primary
|
||||
{
|
||||
return Err(CreateError::InvalidInput(msg.to_string()));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::FileValidation(warning),
|
||||
));
|
||||
}
|
||||
|
||||
let url = format!("{cdn_url}/{file_path_encode}");
|
||||
@@ -1033,13 +1077,13 @@ pub fn get_name_ext(
|
||||
content_disposition: &actix_web::http::header::ContentDisposition,
|
||||
) -> Result<(&str, &str), CreateError> {
|
||||
let file_name = content_disposition.get_filename().ok_or_else(|| {
|
||||
CreateError::MissingValueError("Missing content file name".to_string())
|
||||
CreateError::MissingValueError(MissingValuePart::ContentFileName)
|
||||
})?;
|
||||
let file_extension = if let Some(last_period) = file_name.rfind('.') {
|
||||
file_name.get((last_period + 1)..).unwrap_or("")
|
||||
} else {
|
||||
return Err(CreateError::MissingValueError(
|
||||
"Missing content file extension".to_string(),
|
||||
MissingValuePart::ContentFileExtension,
|
||||
));
|
||||
};
|
||||
Ok((file_name, file_extension))
|
||||
@@ -1067,30 +1111,32 @@ pub fn try_create_version_fields(
|
||||
.iter()
|
||||
.find(|lf| &lf.field == key)
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Loader field '{key}' does not exist for any loaders supplied,"
|
||||
))
|
||||
CreateError::InvalidInput(
|
||||
CreationInvalidInput::NonexistentLoaderField(
|
||||
key.to_string(),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
remaining_mandatory_loader_fields.remove(&loader_field.field);
|
||||
let enum_variants = loader_field_enum_values
|
||||
.remove(&loader_field.id)
|
||||
.unwrap_or_default();
|
||||
|
||||
let vf: VersionField = VersionField::check_parse(
|
||||
let vf = VersionField::check_parse(
|
||||
version_id.into(),
|
||||
loader_field.clone(),
|
||||
value.clone(),
|
||||
enum_variants,
|
||||
)
|
||||
.map_err(CreateError::InvalidInput)?;
|
||||
)?;
|
||||
version_fields.push(vf);
|
||||
}
|
||||
|
||||
if !remaining_mandatory_loader_fields.is_empty() {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Missing mandatory loader fields: {}",
|
||||
remaining_mandatory_loader_fields.iter().join(", ")
|
||||
)));
|
||||
return Err(CreateError::InvalidInput(
|
||||
CreationInvalidInput::MissingLoaderFields(
|
||||
remaining_mandatory_loader_fields.iter().join(", "),
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(version_fields)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::checks::{filter_visible_versions, is_visible_version};
|
||||
use crate::auth::{filter_visible_projects, get_user_from_headers};
|
||||
use crate::database::ReadOnlyPgPool;
|
||||
@@ -8,6 +7,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::projects::VersionType;
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::{database, models};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use dashmap::DashMap;
|
||||
@@ -610,9 +610,8 @@ pub async fn delete_file(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::DELETE_VERSION) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to delete this file!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteFile,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ApiError;
|
||||
use crate::auth::checks::{
|
||||
filter_visible_versions, is_visible_project, is_visible_version,
|
||||
};
|
||||
@@ -24,6 +23,7 @@ use crate::models::projects::{
|
||||
use crate::models::projects::{Loader, skip_nulls};
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::error::{ApiError, SpecificAuthenticationError};
|
||||
use crate::search::SearchConfig;
|
||||
use crate::search::indexing::remove_documents;
|
||||
use crate::util::img;
|
||||
@@ -130,11 +130,10 @@ pub async fn versions_get(
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let version_ids =
|
||||
serde_json::from_str::<Vec<models::ids::VersionId>>(&ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<database::models::DBVersionId>>();
|
||||
let version_ids = serde_json::from_str::<Vec<VersionId>>(&ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<database::models::DBVersionId>>();
|
||||
let versions_data =
|
||||
database::models::DBVersion::get_many(&version_ids, &**pool, &redis)
|
||||
.await?;
|
||||
@@ -159,7 +158,7 @@ pub async fn versions_get(
|
||||
|
||||
pub async fn version_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::VersionId,)>,
|
||||
info: web::Path<(VersionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
@@ -170,7 +169,7 @@ pub async fn version_get(
|
||||
|
||||
pub async fn version_get_helper(
|
||||
req: HttpRequest,
|
||||
id: models::ids::VersionId,
|
||||
id: VersionId,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
@@ -214,7 +213,7 @@ pub struct EditVersion {
|
||||
pub version_number: Option<String>,
|
||||
#[validate(length(max = 65536))]
|
||||
pub changelog: Option<String>,
|
||||
pub version_type: Option<models::projects::VersionType>,
|
||||
pub version_type: Option<VersionType>,
|
||||
#[validate(
|
||||
length(min = 0, max = 4096),
|
||||
custom(function = "crate::util::validate::validate_deps")
|
||||
@@ -331,9 +330,8 @@ pub async fn version_edit_helper(
|
||||
|
||||
if let Some(perms) = permissions {
|
||||
if !perms.contains(ProjectPermissions::UPLOAD_VERSION) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit this version!"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditVersion,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -393,13 +391,13 @@ pub async fn version_edit_helper(
|
||||
|
||||
let builders = dependencies
|
||||
.iter()
|
||||
.map(|x| database::models::version_item::DependencyBuilder {
|
||||
.map(|x| DependencyBuilder {
|
||||
project_id: x.project_id.map(|x| x.into()),
|
||||
version_id: x.version_id.map(|x| x.into()),
|
||||
file_name: x.file_name.clone(),
|
||||
dependency_type: x.dependency_type.to_string(),
|
||||
})
|
||||
.collect::<Vec<database::models::version_item::DependencyBuilder>>();
|
||||
.collect::<Vec<DependencyBuilder>>();
|
||||
|
||||
DependencyBuilder::insert_many(
|
||||
builders,
|
||||
@@ -477,13 +475,12 @@ pub async fn version_edit_helper(
|
||||
let enum_variants = loader_field_enum_values
|
||||
.remove(&loader_field.id)
|
||||
.unwrap_or_default();
|
||||
let vf: VersionField = VersionField::check_parse(
|
||||
let vf = VersionField::check_parse(
|
||||
version_id,
|
||||
loader_field.clone(),
|
||||
vf_value.clone(),
|
||||
enum_variants,
|
||||
)
|
||||
.map_err(ApiError::InvalidInput)?;
|
||||
)?;
|
||||
version_fields.push(vf);
|
||||
}
|
||||
VersionField::insert_many(version_fields, &mut transaction)
|
||||
@@ -502,19 +499,18 @@ pub async fn version_edit_helper(
|
||||
|
||||
let mut loader_versions = Vec::new();
|
||||
for loader in loaders {
|
||||
let loader_id =
|
||||
database::models::loader_fields::Loader::get_id(
|
||||
&loader.0,
|
||||
&mut *transaction,
|
||||
&redis,
|
||||
let loader_id = loader_fields::Loader::get_id(
|
||||
&loader.0,
|
||||
&mut *transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"No database entry for loader provided."
|
||||
.to_string(),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"No database entry for loader provided."
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
})?;
|
||||
loader_versions.push(DBLoaderVersion {
|
||||
loader_id,
|
||||
version_id,
|
||||
@@ -523,7 +519,7 @@ pub async fn version_edit_helper(
|
||||
DBLoaderVersion::insert_many(loader_versions, &mut transaction)
|
||||
.await?;
|
||||
|
||||
crate::database::models::DBProject::clear_cache(
|
||||
database::models::DBProject::clear_cache(
|
||||
version_item.inner.project_id,
|
||||
None,
|
||||
None,
|
||||
@@ -562,8 +558,8 @@ pub async fn version_edit_helper(
|
||||
|
||||
if let Some(downloads) = &new_version.downloads {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to set the downloads of this mod".to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::SetModDownloads,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -692,8 +688,8 @@ pub async fn version_edit_helper(
|
||||
.await?;
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to edit this version!".to_string(),
|
||||
Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::EditVersion,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
@@ -810,7 +806,7 @@ pub async fn version_list(
|
||||
// TODO: This is a bandaid fix for detecting auto-featured versions.
|
||||
// In the future, not all versions will have 'game_versions' fields, so this will need to be changed.
|
||||
let (loaders, game_versions) = futures::future::try_join(
|
||||
database::models::loader_fields::Loader::list(&**pool, &redis),
|
||||
loader_fields::Loader::list(&**pool, &redis),
|
||||
database::models::legacy_loader_fields::MinecraftGameVersion::list(
|
||||
None,
|
||||
Some(true),
|
||||
@@ -930,9 +926,8 @@ pub async fn version_delete(
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::DELETE_VERSION) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to delete versions in this team"
|
||||
.to_string(),
|
||||
return Err(ApiError::SpecificAuthentication(
|
||||
SpecificAuthenticationError::DeleteVersionsInTeam,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub async fn index_local(
|
||||
",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.map(|x| x.as_str().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch(pool)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user