Compare commits

...
Author SHA1 Message Date
Josiah Glosson e0839657b2 Initial InvalidInput as an I18nEnum 2025-10-16 00:55:46 -05:00
Josiah Glosson 13b53a4395 Tombi 2025-10-16 00:50:05 -05:00
Josiah Glosson b93c6f9321 Merge branch 'main' into josiah/localized-labrinth
# Conflicts:
#	.idea/code.iml
#	Cargo.lock
#	Cargo.toml
#	apps/app/Cargo.toml
#	apps/daedalus_client/Cargo.toml
#	apps/labrinth/Cargo.toml
#	apps/labrinth/src/models/v3/notifications.rs
#	apps/labrinth/src/queue/email/templates.rs
#	apps/labrinth/src/routes/internal/affiliate.rs
#	apps/labrinth/src/routes/internal/external_notifications.rs
#	apps/labrinth/src/routes/mod.rs
#	apps/labrinth/src/routes/v3/analytics_get.rs
#	apps/labrinth/src/routes/v3/collections.rs
#	packages/app-lib/Cargo.toml
#	packages/ariadne/Cargo.toml
#	packages/daedalus/Cargo.toml
#	rust-toolchain.toml
2025-10-16 00:48:36 -05:00
Josiah Glosson 3713ab4d6f Add tombi from main 2025-10-16 00:00:36 -05:00
Josiah Glosson 9ee85ecf34 Merge branch 'main' into josiah/localized-labrinth
# Conflicts:
#	Cargo.toml
#	apps/labrinth/src/auth/checks.rs
#	apps/labrinth/src/models/v3/notifications.rs
#	apps/labrinth/src/routes/internal/affiliate.rs
#	apps/labrinth/src/routes/internal/billing.rs
#	apps/labrinth/src/routes/internal/medal.rs
#	apps/labrinth/src/routes/mod.rs
#	apps/labrinth/src/routes/v3/organizations.rs
#	apps/labrinth/src/routes/v3/project_creation.rs
2025-10-02 12:22:37 -05:00
Josiah Glosson 6cc660b70d Merge commit '4def0e84073c3d37c753ac9b7811d440efbe7376' into josiah/localized-labrinth 2025-09-19 16:25:04 -05:00
Josiah Glosson 2515dd95cc Change Organization in SpecificAuthenticationError to Org 2025-09-19 11:55:21 -05:00
Josiah Glosson 8777543f1a Move ApiError::CustomAuthentication to I18nEnum 2025-09-19 11:51:49 -05:00
Josiah Glosson cb96a4135a Run intl:extract 2025-09-17 18:55:13 -05:00
Josiah Glosson 80431c1ec1 Merge branch 'main' into josiah/localized-labrinth
# Conflicts:
#	apps/labrinth/src/models/v3/notifications.rs
#	apps/labrinth/src/queue/email/templates.rs
2025-09-17 17:23:35 -05:00
Josiah Glosson d6b8b9eb16 Replace yaserde with serde-xml-rs 2025-09-17 16:41:25 -05:00
Josiah Glosson 9e441c0fb3 Migrate CreateError::CustomAuthenticationError to I18nEnum 2025-09-17 10:40:47 -05:00
Josiah Glosson 97408bb529 Migrate ProjectStatus to I18nEnum 2025-09-17 08:42:25 -05:00
Josiah Glosson 6c7f8cd3e0 Fix failing test 2025-09-16 13:54:43 -05:00
Josiah Glosson 5e79114c6a Run extract 2025-09-16 09:56:03 -05:00
Josiah Glosson 2ed1d95dd0 Migrate Notification to I18nEnum 2025-09-16 09:38:03 -05:00
Josiah Glosson 55a6ecca80 Fix lint 2025-09-15 15:32:35 -05:00
Josiah Glosson 1d5beb8a6b Merge branch 'main' into josiah/localized-labrinth
# Conflicts:
#	apps/labrinth/src/auth/email/mod.rs
#	apps/labrinth/src/routes/internal/flows.rs
#	apps/labrinth/src/routes/mod.rs
#	apps/labrinth/src/routes/v2/version_file.rs
2025-09-15 15:29:38 -05:00
Josiah Glosson 40ba23b012 Move ValidationResult::Warning to I18nEnum 2025-09-15 15:13:56 -05:00
Josiah Glosson f7bb0946cf Add CreationInvalidInput::Validation 2025-09-15 14:34:34 -05:00
Josiah Glosson 0cb84b029f Add ApiError::InvalidLoaderField 2025-09-15 14:20:47 -05:00
Josiah Glosson 4217f4bdf6 Make VersionField*::parse return a Result<_, VersionFieldParseError> 2025-09-15 13:34:33 -05:00
Josiah Glosson df221504b7 Mostly migrate CreateError::InvalidInput to I18nEnum 2025-09-15 13:03:43 -05:00
Josiah Glosson 77d936cec9 Rename labrinth.error.project_creation to labrinth.error.creation 2025-09-15 09:37:16 -05:00
Josiah Glosson 33fcdeda09 Move CreateError to create_error.rs 2025-09-15 09:31:07 -05:00
Josiah Glosson 665742c6e9 Move CreateError::MissingValueError's field into its own enum 2025-09-12 15:39:05 -05:00
Josiah Glosson 98c8e45d02 Move FileHostingError::S3Error's first field into its own enum 2025-09-12 12:09:44 -05:00
Josiah Glosson 85884493e3 Should fix build and lint 2025-09-12 10:41:34 -05:00
Josiah Glosson 948e8af368 Fix lint 2025-09-11 18:28:52 -05:00
Josiah Glosson dfb2dafb21 Merge branch 'main' into josiah/localized-labrinth
# Conflicts:
#	apps/labrinth/src/queue/payouts.rs
#	apps/labrinth/src/routes/mod.rs
2025-09-11 17:19:20 -05:00
Josiah Glosson 3d43c07c75 Make CI pay attention to labrinth.json 2025-09-11 17:11:55 -05:00
Josiah Glosson 1e2f55bda0 Move DatabaseError::SchemaError into its own I18nEnum 2025-09-11 17:01:34 -05:00
Josiah Glosson 2481479317 Make labrinth extraction go into a frontend labrinth.json for now 2025-09-11 15:45:11 -05:00
Josiah Glosson 4c7048e58f Mark some error translations as transparent 2025-09-11 14:07:18 -05:00
Josiah Glosson 5ba0bf55d4 Prefix all labrinth error root_keys with labrinth. and fix an error 2025-09-11 14:04:36 -05:00
Josiah Glosson c460db1145 Add even more errors 2025-09-11 13:54:20 -05:00
Josiah Glosson ee79a883ab Fix improper handling of Unicode in transform_format_string 2025-09-11 10:55:02 -05:00
Josiah Glosson dfb9bb0259 Only process src directory 2025-09-11 09:50:12 -05:00
Josiah Glosson 0aeb0dc916 Improve ariadne-extract errors with miette 2025-09-11 09:48:18 -05:00
Josiah Glosson 48b1fb9b11 Create ariadne-extract to extract i18n data 2025-09-10 21:18:16 -05:00
Josiah Glosson f863f6c9ac Add I18nEnum implementations for more error types 2025-09-10 14:47:54 -05:00
Josiah Glosson e0299968c4 Revert some accidental added blank lines 2025-09-10 11:20:43 -05:00
Josiah Glosson 617e3418dd Remove actual translation from the backend, instead generating objects containing information on how to translate 2025-09-10 11:10:38 -05:00
Josiah Glosson febf5faa54 Make some small fixes 2025-09-09 12:59:35 -05:00
Josiah Glosson 8bcb593723 Get rid of I18nEnum proc macro in favor of regular macro 2025-09-09 12:41:50 -05:00
Josiah Glosson e0015d531d Move labrinth error stuff to its own module 2025-09-02 15:03:29 -05:00
Josiah Glosson 3a83373695 Migrate project_creation::CreateError over to I18nError 2025-09-02 14:58:24 -05:00
Josiah Glosson 25b8452cb2 Cargo fmt 2025-08-28 20:37:35 -05:00
Josiah Glosson 06c647a162 Fix misc compile errors 2025-08-28 20:35:41 -05:00
Josiah Glosson 50671fc2ac Clean up I18nEnum generated code a bit 2025-08-28 19:53:54 -05:00
Josiah Glosson 3916a49309 Do too many things. Why did I wait so long to commit? 2025-08-28 19:08:40 -05:00
Josiah Glosson 2174cc1f2b Add localized_api_error for marking a function as having localized ApiError returns 2025-08-26 15:09:14 -05:00
131 changed files with 4866 additions and 1956 deletions
+2
View File
@@ -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]
+3 -1
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 = [
+1
View File
@@ -0,0 +1 @@
**/*.rs
+19
View File
@@ -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
+11
View File
@@ -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"
}
+14
View File
@@ -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>;
+657
View File
@@ -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);
}
+60
View File
@@ -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))
}
+1 -1
View File
@@ -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
@@ -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"
}
+5 -4
View File
@@ -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 }
+1
View File
@@ -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"
+7 -8
View File
@@ -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
+24 -28
View File
@@ -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())
}
}
+37 -34
View File
@@ -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()
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ impl ErrorPage {
}
}
impl actix_web::ResponseError for ErrorPage {
impl ResponseError for ErrorPage {
fn status_code(&self) -> StatusCode {
self.code
}
+4 -3
View File
@@ -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
+7
View File
@@ -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)
+204 -147
View File
@@ -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",
);
+44 -1
View File
@@ -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,
+33 -2
View File
@@ -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,
+15 -5
View File
@@ -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(),
+5 -4
View File
@@ -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()))
+25 -2
View File
@@ -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(),
}
}
}
+178 -242
View File
@@ -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 &notif.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 &notif.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,
}
+32 -40
View File
@@ -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 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+13 -1
View File
@@ -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,
+1 -1
View File
@@ -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;
+4 -4
View File
@@ -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
+4 -4
View File
@@ -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)
+8 -13
View File
@@ -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 -1
View File
@@ -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};
+542
View File
@@ -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())
}
}
+6 -12
View File
@@ -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 -12
View File
@@ -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,
));
}
+94 -115
View File
@@ -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,
));
}
+70 -55
View File
@@ -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(
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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};
+4 -4
View File
@@ -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,
+21 -26
View File
@@ -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 -161
View 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())
}
}
+4 -9
View File
@@ -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()
}
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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 -1
View File
@@ -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};
+1 -1
View File
@@ -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);
}
+2 -1
View File
@@ -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 -1
View File
@@ -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;
+2 -1
View File
@@ -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 -1
View File
@@ -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};
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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};
+8 -16
View File
@@ -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 =
+3 -4
View File
@@ -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(|| {
+28 -26
View File
@@ -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,
+270
View File
@@ -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())
}
}
+1 -1
View File
@@ -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,
};
+9 -10
View File
@@ -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,
));
}
}
+1 -1
View File
@@ -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;
+2 -1
View File
@@ -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;
+6 -11
View File
@@ -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(
&notification_ids,
&**pool,
)
.await?;
DBNotification::get_many(&notification_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 {
+4 -4
View File
@@ -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,
));
}
+145 -181
View File
@@ -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(
&current_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,
+1 -1
View File
@@ -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};
+82 -186
View File
@@ -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,
+106 -130
View File
@@ -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,
+1 -1
View File
@@ -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 {
+5 -5
View File
@@ -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 -1
View File
@@ -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;
+41 -60
View File
@@ -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,
));
}
}
+3 -3
View File
@@ -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,
));
}
+22 -27
View File
@@ -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,
));
}
+149 -103
View File
@@ -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)
}
+3 -4
View File
@@ -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,
));
}
}
+33 -38
View File
@@ -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